Browse Source

Core outliner operations refactoring (#4880)

* Add outliner nested transact!

Copied the code mostly from https://github.com/logseq/logseq/pull/4671
by zhiyuan

* refactor: insert-blocks

* fix: insert-blocks

* fix: move cursor to the last block when inserting

* fix: replace the current block when inserting and its content is empty

* keep only :insert-blocks

* expose only :delete-blocks

* Use existing implementations for move-nodes-up-down and

indent/outdent.

* fix editing state not updated immediately

* fix editing status

* fix: avoid recursive copy

* fix: inserting blocks after an empty block

* Implement move-blocks with insert-blocks

* fix: block left

* Implement move-blocks-up-down with move-blocks

* fix: paste text

* Implement indent-outdent-blocks with move-blocks

* fix: indent/outdent

* feat: multiple blocks drag && drop

* fix: indent/outdent blocks

* fix: drag drop

* Port unit tests for outliner.core

* enhance: open collapsed parent when indenting blocks

* refactor: block selection

* fix: indent/outdent blocks with different levels

* Add instrument on invalid outliner structure

* fix: can't write a block if the page has any outdated blocks

* fix: editing status for empty page

* fix: multiple drag & drop

* fix: drag & drop disallows moving from parents to its child

* fix: public property

* fix: can't delete first empty block

* Remove unused code

* fix: e2e tests

A workaround is to not select/highlight the block when pressing esc if it has
fenced code.

* remove unused code

* Add batch transaction test

* fix: update :block/page when dragging targets' children to another page

* Add more tests

* Simplify extract

* Replace db/get-conn with db/get-db

* Simplify extracting blocks from ast

* Code cleanup

* Code cleanup

* Add outliner core fuzzy tests

* Remove unused code

* fix: cursor not jump to the upper block when pressing Enter in the beginning

* fix: Enter in the beginning of a non-empty block

* Fix lint warnings

* Add editor random e2e tests

* Fix typo

* enhance: move some fns and add some comments

* enhance(outliner): add page-block? util

* fix: increase td width to prevent content overflow

Signed-off-by: Yue Yang <[email protected]>

* First pass at file tests for file-sync

Each action usually passes by 5th try

* Fix two incorrect calls caught by tests

* More test improvements

- Easier auth setup
- subdirectory is configurable
- list graphs api also exercised

* Address cleanup from #3839

- Remove unused translation key
- Delete or TODO commented code
- Capitalize notifications to users

* fix quick capture template not working

* enhance(sync): add logout

* enhance: add logout i18n

* fix(plugin): sometimes plugin settings of gui not work when entry from app settings

* enable show-brackets? toggle for orgmode [[file:./pages/demo.org][demo]]

* fix(sync): fix unfinishable sync loop

* feature: logseq protocol; refactor persistGraph

* fix: deeplink support

* fix: broadcast persist graph on opening new graph with logseq protocol

* feat: logseq protocol open action for page-name and uuid

* fix: logseq protocol graph param validation

* ux: copy logseq URL of block

* enhance: remove the redundant 'open' from logseq protocol (v0.1)

* ux: page dropdown button for copy page URL

* chore: logseq protocol comments

* don't create new contents file when changing format

Logseq now creates a new contents file when users try to toggle the
preferred format, which causes file duplications error.

* fix pasting in src block not working on iOS

close https://github.com/logseq/logseq/issues/4914

* fix playing video goes into editing mode on iOS

* fix copy to clipboard failure on iOS

* add Podfile item

* fix mobile toolbar order not persisting after restart

* test(e2e): add test for backspace and cursor pos (#4896)

* test(e2e): add test for backspace and cursor pos
* fix(test): refine, fix wrong helper

* fix(ui): warn about illegal git commit interval

* enhance(editor): allow global git cmd shortcut

* style(settings): line-space of general/journals

* enhance(editor): accept enter in dummy block

Fix #4931

* fix editing state not updated immediately

* fix: can't write a block if the page has any outdated blocks

TODO: clean outdated blocks

* fix: editing status for empty page

* Random tree for outliner core tests

* Add pre assertions and fn docs based on Zhiyuan's suggestions

* Made some changes based on Gabriel's suggestions

* fix: tests

* fix: save current block before moving

* Updated the timeout to 100ms based on llcc's suggestion

https://github.com/logseq/logseq/pull/4880#discussion_r851966301

* api-insert-new-block! supports replace-empty-target?

* fix: replace all :reuse-last-block? usage

Co-authored-by: rcmerci <[email protected]>
Co-authored-by: Yue Yang <[email protected]>
Co-authored-by: Gabriel Horner <[email protected]>
Co-authored-by: llcc <[email protected]>
Co-authored-by: charlie <[email protected]>
Co-authored-by: Junyi Du <[email protected]>
Co-authored-by: Andelf <[email protected]>
Tienson Qin 3 years ago
parent
commit
904eff6d9d
44 changed files with 1820 additions and 1763 deletions
  1. 41 0
      e2e-tests/editor-random.spec.ts
  2. 1 1
      e2e-tests/editor.spec.ts
  3. 97 0
      e2e-tests/utils.ts
  4. 1 1
      src/main/electron/listener.cljs
  5. 19 11
      src/main/frontend/components/block.cljs
  6. 0 13
      src/main/frontend/components/content.cljs
  7. 3 5
      src/main/frontend/components/page.cljs
  8. 6 7
      src/main/frontend/db.cljs
  9. 4 4
      src/main/frontend/db/conn.cljs
  10. 1 1
      src/main/frontend/db/debug.cljs
  11. 153 158
      src/main/frontend/db/model.cljs
  12. 0 6
      src/main/frontend/db/outliner.cljs
  13. 9 9
      src/main/frontend/db/react.cljs
  14. 8 14
      src/main/frontend/db/utils.cljs
  15. 0 7
      src/main/frontend/db_schema.cljs
  16. 12 13
      src/main/frontend/extensions/code.cljs
  17. 1 1
      src/main/frontend/extensions/srs.cljs
  18. 6 6
      src/main/frontend/external/roam_export.cljs
  19. 95 117
      src/main/frontend/format/block.cljs
  20. 2 2
      src/main/frontend/handler.cljs
  21. 1 1
      src/main/frontend/handler/block.cljs
  22. 27 70
      src/main/frontend/handler/dnd.cljs
  23. 259 495
      src/main/frontend/handler/editor.cljs
  24. 22 23
      src/main/frontend/handler/export.cljs
  25. 5 7
      src/main/frontend/handler/external.cljs
  26. 4 6
      src/main/frontend/handler/page.cljs
  27. 2 2
      src/main/frontend/handler/repo.cljs
  28. 1 2
      src/main/frontend/mobile/footer.cljs
  29. 3 3
      src/main/frontend/mobile/intent.cljs
  30. 1 1
      src/main/frontend/mobile/record.cljs
  31. 1 1
      src/main/frontend/modules/editor/undo_redo.cljs
  32. 516 408
      src/main/frontend/modules/outliner/core.cljs
  33. 27 50
      src/main/frontend/modules/outliner/datascript.cljc
  34. 35 0
      src/main/frontend/modules/outliner/transaction.cljc
  35. 2 2
      src/main/frontend/modules/outliner/utils.cljs
  36. 26 13
      src/main/frontend/state.cljs
  37. 0 31
      src/main/frontend/util.cljc
  38. 7 5
      src/main/frontend/util/page_property.cljs
  39. 9 9
      src/main/logseq/api.cljs
  40. 1 5
      src/test/frontend/core_test.cljs
  41. 2 2
      src/test/frontend/db/query_dsl_test.cljs
  42. 406 215
      src/test/frontend/modules/outliner/core_test.cljs
  43. 0 33
      src/test/frontend/modules/outliner/ds_test.cljs
  44. 4 3
      src/test/frontend/test/fixtures.cljs

+ 41 - 0
e2e-tests/editor-random.spec.ts

@@ -0,0 +1,41 @@
+import { expect } from '@playwright/test'
+import { test } from './fixtures'
+import { createRandomPage, enterNextBlock, editFirstBlock, randomInt, IsMac,
+         randomInsert, randomEditDelete, randomEditMoveUpDown,
+         editRandomBlock, randomSelectBlocks, randomIndentOutdent} from './utils'
+
+test('Random editor operations', async ({page, block}) => {
+  var ops = [
+    randomInsert,
+    randomEditMoveUpDown,
+    randomEditDelete,
+
+    // Errors:
+    // locator.waitFor: Timeout 1000ms exceeded.
+    //   =========================== logs ===========================
+    //   waiting for selector "textarea >> nth=0" to be visible
+    // selector resolved to hidden <textarea tabindex="-1" aria-hidden="true"></textarea>
+
+    // editRandomBlock,
+
+    // randomSelectBlocks,
+
+    // randomIndentOutdent,
+  ]
+
+  await createRandomPage(page)
+
+  await block.mustType('Random tests start!')
+  await randomInsert(page, block)
+
+    for (let i = 0; i < 100; i++) {
+    let n = randomInt(0, ops.length - 1)
+
+    var f = ops[n]
+    if (f.toString() == randomInsert.toString()) {
+      await f(page, block)
+    } else {
+      await f(page)
+    }
+  }
+})

+ 1 - 1
e2e-tests/editor.spec.ts

@@ -1,6 +1,6 @@
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
-import { createRandomPage, enterNextBlock, editFirstBlock, IsMac } from './utils'
+import { createRandomPage, enterNextBlock, editFirstBlock, randomInt, IsMac } from './utils'
 import { dispatch_kb_events } from './util/keyboard-events'
 import * as kb_events from './util/keyboard-events'
 

+ 97 - 0
e2e-tests/utils.ts

@@ -205,3 +205,100 @@ export async function activateNewPage(page: Page) {
 export async function editFirstBlock(page: Page) {
   await page.click('.ls-block .block-content >> nth=0')
 }
+
+export function randomInt(min: number, max: number): number {
+  return Math.floor(Math.random() * (max - min + 1) + min)
+}
+
+export function randomBoolean(): bool {
+  return Math.random() < 0.5;
+}
+
+export async function randomInsert( page, block ) {
+  let n = randomInt(0, 100)
+  await block.mustFill(n.toString())
+
+  // random indent
+  if (randomBoolean ()) {
+    await block.indent()
+  } else {
+    await block.unindent()
+  }
+
+  await page.waitForSelector('textarea >> nth=0', { state: 'visible' })
+
+  await page.press('textarea >> nth=0', 'Enter')
+}
+
+export async function randomEditDelete( page: Page ) {
+  let n = randomInt(3)
+
+  for (let i = 0; i < n; i++) {
+    await page.keyboard.press('Backspace')
+  }
+}
+
+export async function randomEditMoveUpDown( page: Page ) {
+  let n = randomInt(3, 10)
+
+  for (let i = 0; i < n; i++) {
+    if (randomBoolean ()) {
+      await page.keyboard.press('Meta+Shift+ArrowUp')
+    } else {
+      await page.keyboard.press('Meta+Shift+ArrowDown')
+    }
+  }
+}
+
+async function scrollOnElement(page, selector) {
+  await page.$eval(selector, (element) => {
+    element.scrollIntoView();
+  });
+}
+
+export async function editRandomBlock( page: Page ) {
+  let blockCount = await page.locator('.page-blocks-inner .ls-block').count()
+  let n = randomInt(0, blockCount - 1)
+
+  // discard any popups
+  await page.keyboard.press('Escape')
+  // click last block
+  if (await page.locator('text="Click here to edit..."').isVisible()) {
+    await page.click('text="Click here to edit..."')
+  } else {
+    await page.click(`.ls-block .block-content >> nth=${n}`)
+  }
+
+  // wait for textarea
+  await page.waitForSelector('textarea >> nth=0', { state: 'visible', timeout: 1000 })
+
+  await scrollOnElement(page, 'textarea >> nth=0');
+
+  const randomContent = randomString(10)
+
+  const locator: Locator = page.locator('textarea >> nth=0')
+
+  await locator.type(randomContent)
+
+  return locator
+}
+
+export async function randomSelectBlocks( page: Page ) {
+  await editRandomBlock(page)
+
+  let n = randomInt(1, 10)
+
+  for (let i = 0; i < n; i++) {
+    await page.keyboard.press('Shift+ArrowUp')
+  }
+}
+
+export async function randomIndentOutdent( page: Page ) {
+  await randomSelectBlocks(page)
+
+  if (randomBoolean ()) {
+    await page.keyboard.press('Tab', { delay: 100 })
+  } else {
+    await page.keyboard.press('Shift+Tab', { delay: 100 })
+  }
+}

+ 1 - 1
src/main/electron/listener.cljs

@@ -94,7 +94,7 @@
                      (fn [data]
                        (let [{:keys [graph tx-data]} (bean/->clj data)
                              tx-data (db/string->db (:data tx-data))]
-                         (when-let [conn (db/get-conn graph false)]
+                         (when-let [conn (db/get-db graph false)]
                            (d/transact! conn tx-data {:dbsync? true}))
                          (ui-handler/re-render-root!))))
 

+ 19 - 11
src/main/frontend/components/block.cljs

@@ -259,7 +259,7 @@
         href (config/get-local-asset-absolute-path href)]
     (when (or granted? (util/electron?) (mobile-util/is-native-platform?))
       (p/then (editor-handler/make-asset-url href) #(reset! src %)))
-    
+
     (when @src
       (let [ext (util/get-file-ext @src)]
         (if (contains? (set (map name config/audio-formats)) ext)
@@ -1311,7 +1311,7 @@
          (when (not html-export?)
            [:span {:dangerouslySetInnerHTML
                    {:__html s}}])
-         
+
          ["Inline_Hiccup" s] ;; String to hiccup
          (ui/catch-error
           [:div.warning {:title "Invalid hiccup"} s]
@@ -1377,7 +1377,6 @@
   (.setData (gobj/get event "dataTransfer")
             "block-dom-id"
             block-id)
-  (state/clear-selection!)
   (reset! *dragging? true)
   (reset! *dragging-block block))
 
@@ -1991,7 +1990,8 @@
                      :on-hide (fn [value event]
                                 (when (= event :esc)
                                   (editor-handler/save-block! (editor-handler/get-state) value)
-                                  (editor-handler/escape-editing)))}
+                                  (let [select? (not (string/includes? value "```"))]
+                                    (editor-handler/escape-editing select?))))}
                     edit-input-id
                     config))]
       [:div.flex.flex-row.block-content-wrapper
@@ -2116,12 +2116,14 @@
   (reset! *move-to nil))
 
 (defn- block-drop
-  [event uuid block *move-to]
+  [event uuid target-block *move-to]
   (util/stop event)
   (when-not (dnd-same-block? uuid)
-    (dnd/move-block event @*dragging-block
-                    block
-                    @*move-to))
+    (let [block-uuids (state/get-selection-block-ids)
+          lookup-refs (map (fn [id] [:block/uuid id]) block-uuids)
+          selected (db/pull-many (state/get-current-repo) '[*] lookup-refs)
+          blocks (if (seq selected) selected [@*dragging-block])]
+      (dnd/move-blocks event blocks target-block @*move-to)))
   (reset! *dragging? false)
   (reset! *dragging-block nil)
   (reset! *drag-to-block nil)
@@ -2218,7 +2220,7 @@
              (assoc state ::control-show? (atom false))))
    :should-update (fn [old-state new-state]
                     (let [compare-keys [:block/uuid :block/content :block/parent :block/collapsed?
-                                        :block/properties :block/left :block/children :block/_refs]
+                                        :block/properties :block/left :block/children :block/_refs :ui/selected?]
                           config-compare-keys [:show-cloze?]
                           b1 (second (:rum/args old-state))
                           b2 (second (:rum/args new-state))
@@ -2275,7 +2277,8 @@
         :data-collapsed (and collapsed? has-child?)
         :class (str uuid
                     (when pre-block? " pre-block")
-                    (when (and card? (not review-cards?)) " shadow-xl"))
+                    (when (and card? (not review-cards?)) " shadow-xl")
+                    (when (:ui/selected? block) " selected noselect"))
         :blockid (str uuid)
         :haschild (str has-child?)}
 
@@ -2917,6 +2920,11 @@
            (assoc state ::id (str (random-uuid))))}
   [state config flat-blocks blocks->vec-tree]
   (let [db-id (:db/id config)
+        selected-blocks (set (state/get-selection-block-ids))
+        flat-blocks (if (seq selected-blocks)
+                      (map (fn [b]
+                             (assoc b :ui/selected? (contains? selected-blocks (:block/uuid b)))) flat-blocks)
+                      flat-blocks)
         blocks (blocks->vec-tree flat-blocks)]
     (if-not db-id
       (block-list config blocks)
@@ -2926,7 +2934,7 @@
                                (load-more-blocks! config flat-blocks)))
             has-more? (and
                        (> (count flat-blocks) model/initial-blocks-length)
-                       (some? (model/get-next-open-block (db/get-conn) (last flat-blocks) db-id)))
+                       (some? (model/get-next-open-block (db/get-db) (last flat-blocks) db-id)))
             dom-id (str "lazy-blocks-" (::id state))]
         [:div {:id dom-id}
          (ui/infinite-list

+ 0 - 13
src/main/frontend/components/content.cljs

@@ -338,19 +338,6 @@
 (rum/defc hiccup-content < rum/static
   (mixins/event-mixin
    (fn [state]
-     (mixins/listen state js/window "mouseup"
-                    (fn [_e]
-                      (when-not (state/in-selection-mode?)
-                        (when-let [blocks (seq (util/get-selected-nodes "ls-block"))]
-                          (let [blocks (remove nil? blocks)
-                                blocks (remove #(d/has-class? % "dummy") blocks)]
-                            (when (seq blocks)
-                              (util/select-highlight! blocks)
-                              ;; TODO: We delay this so the following "click" event won't clear the selections.
-                              ;; Needs more thinking.
-                              (js/setTimeout #(state/set-selection-blocks! blocks)
-                                             200)))))))
-
      (mixins/listen state js/window "contextmenu"
                     (fn [e]
                       (let [target (gobj/get e "target")

+ 3 - 5
src/main/frontend/components/page.cljs

@@ -82,8 +82,8 @@
 (rum/defc dummy-block
   [page-name]
   (let [handler-fn (fn []
-                     (let [block (editor-handler/insert-first-page-block-if-not-exists! page-name)]
-                       (js/setTimeout #(editor-handler/edit-block! block :max (:block/uuid block)) 100)))]
+                     (let [block (editor-handler/insert-first-page-block-if-not-exists! page-name :check-empty-page? false)]
+                       (js/setTimeout #(editor-handler/edit-block! block :max (:block/uuid block)) 0)))]
     [:div.ls-block.flex-1.flex-col.rounded-sm {:style {:width "100%"}}
      [:div.flex.flex-row
       [:div.flex.flex-row.items-center.mr-2.ml-1 {:style {:height 24}}
@@ -100,9 +100,7 @@
 (rum/defc add-button
   [args]
   [:div.flex-1.flex-col.rounded-sm.add-button-link-wrap
-   {:on-click (fn []
-                (when-let [block (editor-handler/api-insert-new-block! "" args)]
-                  (js/setTimeout #(editor-handler/edit-block! block :max (:block/uuid block)) 100)))}
+   {:on-click (fn [] (editor-handler/api-insert-new-block! "" args))}
    [:div.flex.flex-row
     [:div.block {:style {:height      20
                          :width       20

+ 6 - 7
src/main/frontend/db.cljs

@@ -26,13 +26,13 @@
   get-repo-name
   get-short-repo-name
   datascript-db
-  get-conn
+  get-db
   me-tx
   remove-conn!]
 
  [frontend.db.utils
   date->int db->json db->edn-str db->string get-max-tx-id get-tx-id
-  group-by-page seq-flatten sort-by-pos
+  group-by-page seq-flatten
   string->db
 
   entity pull pull-many transact! get-key-value]
@@ -88,10 +88,9 @@
 ;; persisting DBs between page reloads
 (defn persist! [repo]
   (let [key (datascript-db repo)
-        conn (get-conn repo false)]
-    (when conn
-      (let [db (d/db conn)
-            db-str (if db (db->string db) "")]
+        db (get-db repo)]
+    (when db
+      (let [db-str (if db (db->string db) "")]
         (p/let [_ (db-persist/save-graph! key db-str)])))))
 
 (defonce persistent-jobs (atom {}))
@@ -150,7 +149,7 @@
 
 (defn listen-and-persist!
   [repo]
-  (when-let [conn (get-conn repo false)]
+  (when-let [conn (get-db repo false)]
     (repo-listen-to-tx! repo conn)))
 
 (defn start-db-conn!

+ 4 - 4
src/main/frontend/db/conn.cljs

@@ -46,13 +46,13 @@
       (str (if (util/electron?) "" config/idb-db-prefix)
            path))))
 
-(defn get-conn
+(defn get-db
   ([]
-   (get-conn (state/get-current-repo) true))
+   (get-db (state/get-current-repo) true))
   ([repo-or-deref?]
    (if (boolean? repo-or-deref?)
-     (get-conn (state/get-current-repo) repo-or-deref?)
-     (get-conn repo-or-deref? true)))
+     (get-db (state/get-current-repo) repo-or-deref?)
+     (get-db repo-or-deref? true)))
   ([repo deref?]
    (let [repo (if repo repo (state/get-current-repo))]
      (when-let [conn (get @conns (datascript-db repo))]

+ 1 - 1
src/main/frontend/db/debug.cljs

@@ -12,7 +12,7 @@
 
 (defn check-left-id-conflicts
   []
-  (let [db (db/get-conn)
+  (let [db (db/get-db)
         blocks (->> (d/datoms db :avet :block/uuid)
                     (map :v)
                     (map (fn [id]

+ 153 - 158
src/main/frontend/db/model.cljs

@@ -65,13 +65,13 @@
                         (remove nil?)
                         (map #(dissoc % :file/handle :file/type)))]
        (when (seq tx-data)
-         (when-let [conn (conn/get-conn repo-url false)]
+         (when-let [conn (conn/get-db repo-url false)]
            (d/transact! conn (vec tx-data))))))))
 
 (defn pull-block
   [id]
   (let [repo (state/get-current-repo)]
-    (when (conn/get-conn repo)
+    (when (conn/get-db repo)
       (->
        (react/q repo [:frontend.db.react/block id] {}
          '[:find [(pull ?block ?block-attrs) ...]
@@ -93,7 +93,7 @@
            [?page :block/tags ?e]
            [?page :block/original-name ?original-name]
            [?page :block/name ?name]]
-         (conn/get-conn repo)
+         (conn/get-db repo)
          (util/page-name-sanity-lc tag-name))))
 
 (defn get-all-tagged-pages
@@ -103,7 +103,7 @@
          [?page :block/tags ?e]
          [?e :block/name ?tag]
          [?page :block/name ?page-name]]
-    (conn/get-conn repo)))
+    (conn/get-db repo)))
 
 (defn get-all-namespace-relation
   [repo]
@@ -112,7 +112,7 @@
          [?page :block/name ?page-name]
          [?page :block/namespace ?e]
          [?e :block/name ?parent]]
-    (conn/get-conn repo)))
+    (conn/get-db repo)))
 
 (defn get-pages
   [repo]
@@ -121,7 +121,7 @@
           :where
           [?page :block/name ?page-name]
           [(get-else $ ?page :block/original-name ?page-name) ?page-original-name]]
-        (conn/get-conn repo))
+        (conn/get-db repo))
        (map first)))
 
 (defn get-all-pages
@@ -130,24 +130,24 @@
     '[:find [(pull ?page [*]) ...]
       :where
       [?page :block/name]]
-    (conn/get-conn repo)))
+    (conn/get-db repo)))
 
 (defn get-page-alias
   [repo page-name]
-  (when-let [conn (and repo (conn/get-conn repo))]
+  (when-let [db (and repo (conn/get-db repo))]
     (some->> (d/q '[:find ?alias
                     :in $ ?page-name
                     :where
                     [?page :block/name ?page-name]
                     [?page :block/alias ?alias]]
-                  conn
+                  db
                   (util/page-name-sanity-lc page-name))
              db-utils/seq-flatten
              distinct)))
 
 (defn get-alias-source-page
   [repo alias]
-  (when-let [conn (and repo (conn/get-conn repo))]
+  (when-let [db (and repo (conn/get-db repo))]
     (let [alias (util/page-name-sanity-lc alias)
           pages (->>
                  (d/q '[:find (pull ?p [*])
@@ -155,7 +155,7 @@
                         :where
                         [?a :block/name ?alias]
                         [?p :block/alias ?a]]
-                      conn
+                      db
                       alias)
                  (db-utils/seq-flatten))]
       (when (seq pages)
@@ -169,15 +169,15 @@
 
 (defn get-files
   [repo]
-  (when-let [conn (conn/get-conn repo)]
+  (when-let [db (conn/get-db repo)]
     (->> (d/q
-          '[:find ?path
+           '[:find ?path
              ;; ?modified-at
-            :where
-            [?file :file/path ?path]
-            ;; [?file :file/last-modified-at ?modified-at]
-            ]
-          conn)
+             :where
+             [?file :file/path ?path]
+             ;; [?file :file/last-modified-at ?modified-at]
+             ]
+           db)
          (seq)
          ;; (sort-by last)
          (reverse))))
@@ -194,7 +194,7 @@
                [(?pred $ ?path)]
                [?p :block/file ?file]
                [?block :block/page ?p]]
-             (conn/get-conn repo-url) pred)
+             (conn/get-db repo-url) pred)
         db-utils/seq-flatten)))
 
 (defn get-file-blocks
@@ -205,7 +205,7 @@
              [?file :file/path ?path]
              [?p :block/file ?file]
              [?block :block/page ?p]]
-           (conn/get-conn repo-url) path)
+           (conn/get-db repo-url) path)
       db-utils/seq-flatten))
 
 (defn get-file-pages
@@ -215,13 +215,13 @@
              :where
              [?file :file/path ?path]
              [?page :block/file ?file]]
-           (conn/get-conn repo-url) path)
+           (conn/get-db repo-url) path)
       db-utils/seq-flatten))
 
 (defn set-file-last-modified-at!
   [repo path last-modified-at]
   (when (and repo path last-modified-at)
-    (when-let [conn (conn/get-conn repo false)]
+    (when-let [conn (conn/get-db repo false)]
       (d/transact! conn
         [{:file/path path
           :file/last-modified-at last-modified-at}]
@@ -230,38 +230,38 @@
 (defn get-file-last-modified-at
   [repo path]
   (when (and repo path)
-    (when-let [conn (conn/get-conn repo false)]
-      (-> (d/entity (d/db conn) [:file/path path])
+    (when-let [db (conn/get-db repo)]
+      (-> (d/entity db [:file/path path])
           :file/last-modified-at))))
 
 (defn file-exists?
   [repo path]
   (when (and repo path)
-    (when-let [conn (conn/get-conn repo false)]
-      (d/entity (d/db conn) [:file/path path]))))
+    (when-let [db (conn/get-db repo)]
+      (d/entity db [:file/path path]))))
 
 (defn get-file-contents
   [repo]
-  (when-let [conn (conn/get-conn repo)]
+  (when-let [db (conn/get-db repo)]
     (->>
      (d/q
       '[:find ?path ?content
         :where
         [?file :file/path ?path]
         [?file :file/content ?content]]
-      conn)
+       db)
      (into {}))))
 
 
 (defn get-files-full
   [repo]
-  (when-let [conn (conn/get-conn repo)]
+  (when-let [db (conn/get-db repo)]
     (->>
      (d/q
-      '[:find (pull ?file [*])
-        :where
-        [?file :file/path]]
-      conn)
+       '[:find (pull ?file [*])
+         :where
+         [?file :file/path]]
+       db)
      (flatten))))
 
 (defn get-file
@@ -269,8 +269,8 @@
    (get-file (state/get-current-repo) path))
   ([repo path]
    (when (and repo path)
-     (when-let [conn (conn/get-conn repo)]
-       (:file/content (d/entity conn [:file/path path]))))))
+     (when-let [db (conn/get-db repo)]
+       (:file/content (d/entity db [:file/path path]))))))
 
 (defn get-custom-css
   []
@@ -306,7 +306,7 @@
             :where
             [?page :block/name ?page-name]
             (alias ?page ?e)]
-          (conn/get-conn repo-url)
+          (conn/get-db repo-url)
           (util/safe-page-name-sanity-lc page)
           '[[(alias ?e2 ?e1)
              [?e2 :block/alias ?e1]]
@@ -411,7 +411,7 @@
 
 (defn has-children?
   ([block-id]
-   (has-children? (conn/get-conn) block-id))
+   (has-children? (conn/get-db) block-id))
   ([db block-id]
    (some? (:block/_parent (d/entity db [:block/uuid block-id])))))
 
@@ -449,8 +449,8 @@
   ([block-id]
    (get-block-parent (state/get-current-repo) block-id))
   ([repo block-id]
-   (when-let [conn (conn/get-conn repo)]
-     (when-let [block (d/entity conn [:block/uuid block-id])]
+   (when-let [db (conn/get-db repo)]
+     (when-let [block (d/entity db [:block/uuid block-id])]
        (:block/parent block)))))
 
 ;; non recursive query
@@ -458,27 +458,26 @@
   ([repo block-id]
    (get-block-parents repo block-id 100))
   ([repo block-id depth]
-   (when-let [conn (conn/get-conn repo)]
-     (loop [block-id block-id
-            parents (list)
-            d 1]
-       (if (> d depth)
-         parents
-         (if-let [parent (get-block-parent repo block-id)]
-           (recur (:block/uuid parent) (conj parents parent) (inc d))
-           parents))))))
+   (loop [block-id block-id
+          parents (list)
+          d 1]
+     (if (> d depth)
+       parents
+       (if-let [parent (get-block-parent repo block-id)]
+         (recur (:block/uuid parent) (conj parents parent) (inc d))
+         parents)))))
 
 (comment
   (defn get-immediate-children-v2
     [repo block-id]
-    (d/pull (conn/get-conn repo)
+    (d/pull (conn/get-db repo)
             '[:block/_parent]
             [:block/uuid block-id])))
 
 ;; Use built-in recursive
 (defn get-block-parents-v2
   [repo block-id]
-  (d/pull (conn/get-conn repo)
+  (d/pull (conn/get-db repo)
           '[:db/id :block/collapsed? :block/properties {:block/parent ...}]
           [:block/uuid block-id]))
 
@@ -533,13 +532,18 @@
         result))))
 
 (defn get-block-last-direct-child
-  [db db-id]
-  (when-let [block (d/entity db db-id)]
-    (when-not (collapsed-and-has-children? db block)
-      (let [children (:block/_parent block)
-            all-left (set (concat (map (comp :db/id :block/left) children) [db-id]))
-            all-ids (set (map :db/id children))]
-        (first (set/difference all-ids all-left))))))
+  "Notice: if `not-collapsed?` is true, will skip searching for any collapsed block."
+  ([db db-id]
+   (get-block-last-direct-child db db-id true))
+  ([db db-id not-collapsed?]
+   (when-let [block (d/entity db db-id)]
+     (when (if not-collapsed?
+             (not (collapsed-and-has-children? db block))
+             true)
+       (let [children (:block/_parent block)
+             all-left (set (concat (map (comp :db/id :block/left) children) [db-id]))
+             all-ids (set (map :db/id children))]
+         (first (set/difference all-ids all-left)))))))
 
 (defn get-block-last-child
   [db db-id]
@@ -577,7 +581,7 @@
   (let [db-before (or db-before current-db)
         cached-ids (map :db/id @result)
         cached-ids-set (set (conj cached-ids page-id))
-        first-changed-id (if (= outliner-op :move-subtree)
+        first-changed-id (if (= outliner-op :move-blocks)
                            (let [{:keys [move-blocks target from-page to-page]} tx-meta]
                              (cond
                                (= page-id target) ; move to the first block
@@ -597,7 +601,7 @@
                                        id
                                        (recur others))
                                      nil)))))
-                           (let [insert? (contains? #{:insert-node :insert-nodes :save-and-insert-node} outliner-op)]
+                           (let [insert? (= :insert-blocks outliner-op)]
                              (some #(when (and (or (and insert? (not (contains? cached-ids-set %)))
                                                    true)
                                                (recursive-child? repo-url % block-id))
@@ -606,19 +610,16 @@
       (or (get-prev-open-block db-before first-changed-id)
           (get-prev-open-block current-db first-changed-id)))))
 
-;; TODO: outliners ops should be merged to :save-nodes, :insert-nodes,
-;; :delete-nodes and :move-nodes
 (defn- build-paginated-blocks-from-cache
   "Notice: tx-report could be nil."
   [repo-url tx-report result outliner-op page-id block-id tx-block-ids scoped-block-id]
   (let [{:keys [tx-meta]} tx-report
-        current-db (conn/get-conn repo-url)]
+        current-db (conn/get-db repo-url)]
     (cond
-      (contains? #{:save-node :delete-node :delete-nodes} outliner-op)
+      (contains? #{:save-block :delete-blocks} outliner-op)
       @result
 
-      (contains? #{:insert-node :insert-nodes :save-and-insert-node
-                   :collapse-expand-blocks :indent-outdent-nodes :move-subtree} outliner-op)
+      (contains? #{:insert-blocks :collapse-expand-blocks :move-blocks} outliner-op)
       (when-let [start-id (get-start-id-for-pagination-query
                            repo-url current-db tx-report result outliner-op page-id block-id tx-block-ids)]
         (let [start-page? (:block/name (db-utils/entity start-id))]
@@ -677,12 +678,12 @@
                            outliner-op (get-in tx-report [:tx-meta :outliner-op])
                            blocks (build-paginated-blocks-from-cache repo-url tx-report result outliner-op page-id block-id tx-block-ids scoped-block-id)
                            blocks (or blocks
-                                      (get-paginated-blocks-no-cache (conn/get-conn repo-url) block-id {:limit limit
-                                                                                                        :include-start? (not page?)
-                                                                                                        :scoped-block-id scoped-block-id}))
+                                      (get-paginated-blocks-no-cache (conn/get-db repo-url) block-id {:limit limit
+                                                                                                      :include-start? (not page?)
+                                                                                                      :scoped-block-id scoped-block-id}))
                            block-eids (map :db/id blocks)
                            blocks (if (and (seq tx-id->block)
-                                           (not (contains? #{:indent-outdent-nodes :move-subtree} outliner-op)))
+                                           (not (contains? #{:move-blocks} outliner-op)))
                                     (map (fn [id]
                                            (or (get tx-id->block id)
                                                (get cached-id->block id)
@@ -703,7 +704,7 @@
    (when page
      (let [page (util/page-name-sanity-lc page)
            page-id (:db/id (db-utils/entity repo-url [:block/name page]))
-           db (conn/get-conn repo-url)]
+           db (conn/get-db repo-url)]
        (when page-id
          (let [datoms (d/datoms db :avet :block/page page-id)
                block-eids (mapv :e datoms)]
@@ -711,26 +712,23 @@
 
 (defn get-page-blocks-count
   [repo page-id]
-  (when-let [db (conn/get-conn repo)]
+  (when-let [db (conn/get-db repo)]
     (count (d/datoms db :avet :block/page page-id))))
 
 (defn page-empty?
   [repo page-id]
-  (when-let [db (conn/get-conn repo)]
+  (when-let [db (conn/get-db repo)]
     (let [page-id (if (string? page-id)
                     [:block/name (util/safe-page-name-sanity-lc page-id)]
                     page-id)
           page (d/entity db page-id)]
-      ;; NOTE: when page is nil, it means the page does not exist
-      (if page
-        (nil? (first (d/datoms db :avet :block/page (:db/id page))))
-        true))))
+      (nil? (:block/_left page)))))
 
 (defn page-empty-or-dummy?
   [repo page-id]
   (or
    (page-empty? repo page-id)
-   (when-let [db (conn/get-conn repo)]
+   (when-let [db (conn/get-db repo)]
      (let [datoms (d/datoms db :avet :block/page page-id)]
        (and (= (count datoms) 1)
             (= "" (:block/content (db-utils/pull (:e (first datoms))))))))))
@@ -755,10 +753,10 @@
 
 (defn get-pages-by-name-partition
   [repo partition]
-  (when-let [conn (conn/get-conn repo)]
+  (when-let [db (conn/get-db repo)]
     (when-not (string/blank? partition)
       (let [partition (util/page-name-sanity-lc (string/trim partition))
-            ids (->> (d/datoms conn :aevt :block/name)
+            ids (->> (d/datoms db :aevt :block/name)
                      (filter (fn [datom]
                                (let [page (:v datom)]
                                  (string/includes? page partition))))
@@ -771,7 +769,7 @@
 
 (defn get-block-children-ids
   [repo block-uuid]
-  (when-let [conn (conn/get-conn repo)]
+  (when-let [db (conn/get-db repo)]
     (let [eid (:db/id (db-utils/entity repo [:block/uuid block-uuid]))]
       (->> (d/q
             '[:find ?id
@@ -779,7 +777,7 @@
               :where
               (child ?p ?c)
               [?c :block/uuid ?id]]
-            conn
+            db
             eid
             rules)
            (apply concat)))))
@@ -787,25 +785,24 @@
 (defn get-block-immediate-children
   "Doesn't include nested children."
   [repo block-uuid]
-  (when-let [conn (conn/get-conn repo)]
+  (when-let [db (conn/get-db repo)]
     (-> (d/q
           '[:find [(pull ?b [*]) ...]
             :in $ ?parent-id
             :where
             [?parent :block/uuid ?parent-id]
             [?b :block/parent ?parent]]
-          conn
+          db
           block-uuid)
         (sort-by-left (db-utils/entity [:block/uuid block-uuid])))))
 
 (defn get-block-children
   "Including nested children."
   [repo block-uuid]
-  (when-let [conn (conn/get-conn repo)]
-    (let [ids (get-block-children-ids repo block-uuid)
-          ids (map (fn [id] [:block/uuid id]) ids)]
-      (when (seq ids)
-        (db-utils/pull-many repo '[*] ids)))))
+  (let [ids (get-block-children-ids repo block-uuid)
+        ids (map (fn [id] [:block/uuid id]) ids)]
+    (when (seq ids)
+      (db-utils/pull-many repo '[*] ids))))
 
 ;; TODO: use the tree directly
 (defn- flatten-tree
@@ -821,7 +818,7 @@
               :in $ ?id ?block-attrs
               :where
               [?block :block/uuid ?id]]
-            (conn/get-conn repo)
+            (conn/get-db repo)
             block-uuid
             block-attrs)
           first
@@ -832,7 +829,7 @@
    (get-file-page file-path true))
   ([file-path original-name?]
    (when-let [repo (state/get-current-repo)]
-     (when-let [conn (conn/get-conn repo)]
+     (when-let [db (conn/get-db repo)]
        (some->
         (d/q
          (if original-name?
@@ -848,7 +845,7 @@
              [?file :file/path ?path]
              [?page :block/file ?file]
              [?page :block/name ?page-name]])
-         conn file-path)
+         db file-path)
         db-utils/seq-flatten
         first)))))
 
@@ -868,7 +865,7 @@
 (defn get-file-page-id
   [file-path]
   (when-let [repo (state/get-current-repo)]
-    (when-let [conn (conn/get-conn repo)]
+    (when-let [db (conn/get-db repo)]
       (some->
        (d/q
         '[:find ?page
@@ -877,7 +874,7 @@
           [?file :file/path ?path]
           [?page :block/name]
           [?page :block/file ?file]]
-        conn file-path)
+        db file-path)
        db-utils/seq-flatten
        first))))
 
@@ -924,14 +921,14 @@
            [?page :block/journal? true]
            [?page :block/journal-day ?journal-day]
            [(<= ?journal-day ?today)]]
-         (conn/get-conn (state/get-current-repo))
+         (conn/get-db (state/get-current-repo))
          today)))
 
 (defn get-latest-journals
   ([n]
    (get-latest-journals (state/get-current-repo) n))
   ([repo-url n]
-   (when (conn/get-conn repo-url)
+   (when (conn/get-db repo-url)
      (let [date (js/Date.)
            _ (.setDate date (- (.getDate date) (dec n)))
            today (db-utils/date->int (js/Date.))]
@@ -957,13 +954,13 @@
       :in $ ?day
       :where
       [?p :block/journal-day ?day]]
-    (conn/get-conn graph)
+    (conn/get-db graph)
     day))
 
 ;; get pages that this page referenced
 (defn get-page-referenced-pages
   [repo page]
-  (when-let [db (conn/get-conn repo)]
+  (when-let [db (conn/get-db repo)]
     (let [page-name (util/safe-page-name-sanity-lc page)
           pages (page-alias-set repo page)
           page-id (:db/id (db-utils/entity [:block/name page-name]))
@@ -981,7 +978,7 @@
 
 (defn get-page-linked-refs-refed-pages
   [repo page]
-  (when-let [conn (conn/get-conn repo)]
+  (when-let [db (conn/get-db repo)]
     (->
      (d/q
       '[:find [?ref-page ...]
@@ -992,7 +989,7 @@
         [?b :block/refs ?other-p]
         [(not= ?p ?other-p)]
         [?other-p :block/original-name ?ref-page]]
-      conn
+      db
       rules
       (util/safe-page-name-sanity-lc page))
      (distinct))))
@@ -1000,7 +997,7 @@
 ;; Ignore files with empty blocks for now
 (defn get-pages-relation
   [repo with-journal?]
-  (when-let [conn (conn/get-conn repo)]
+  (when-let [db (conn/get-db repo)]
     (let [q (if with-journal?
               '[:find ?page ?ref-page-name
                 :where
@@ -1016,7 +1013,7 @@
                 [?block :block/refs ?ref-page]
                 [?ref-page :block/name ?ref-page-name]])]
       (->>
-       (d/q q conn)
+       (d/q q db)
        (map (fn [[page ref-page-name]]
               [page ref-page-name]))))))
 
@@ -1024,7 +1021,7 @@
 ;; TODO: use :block/_refs
 (defn get-pages-that-mentioned-page
   [repo page]
-  (when (conn/get-conn repo)
+  (when (conn/get-db repo)
     (let [page-id (:db/id (db-utils/entity [:block/name (util/safe-page-name-sanity-lc page)]))
           pages (page-alias-set repo page)
           mentioned-pages (->> (react/q repo [:frontend.db.react/page<-pages page-id] {:use-cache? false}
@@ -1049,7 +1046,7 @@
             :in $ ?page-id
             :where
             [?b :block/refs ?page-id]]
-          (conn/get-conn repo)
+          (conn/get-db repo)
           page-id)
      (flatten))))
 
@@ -1058,7 +1055,7 @@
    (get-page-referenced-blocks (state/get-current-repo) page))
   ([repo page]
    (when repo
-     (when (conn/get-conn repo)
+     (when (conn/get-db repo)
        (let [page-id (:db/id (db-utils/entity [:block/name (util/safe-page-name-sanity-lc page)]))
              pages (page-alias-set repo page)
              aliases (set/difference pages #{page-id})
@@ -1108,7 +1105,7 @@
    (get-page-referenced-blocks-ids (state/get-current-repo) page))
   ([repo page]
    (when repo
-     (when-let [conn (conn/get-conn repo)]
+     (when-let [db (conn/get-db repo)]
        (let [page-id (:db/id (db-utils/entity [:block/name (util/safe-page-name-sanity-lc page)]))
              pages (page-alias-set repo page)
              aliases (set/difference pages #{page-id})
@@ -1124,7 +1121,7 @@
                                   :in $ % ?pages ?aliases ?block-attrs
                                   :where
                                   (find-blocks ?block ?ref-page ?pages ?alias ?aliases)]
-                                conn
+                                db
                                 rules
                                 pages
                                 aliases
@@ -1134,7 +1131,7 @@
                                 :in $ ?page ?block-attrs
                                 :where
                                 [?ref-block :block/refs ?page]]
-                              conn
+                              db
                               page-id
                               block-attrs))]
          query-result)))))
@@ -1144,29 +1141,28 @@
   (when-let [date (date/journal-title->int journal-title)]
     (let [future-days (state/get-scheduled-future-days)]
       (when-let [repo (state/get-current-repo)]
-        (when-let [conn (conn/get-conn repo)]
-          (->> (react/q repo [:custom :scheduled-deadline journal-title] {}
-                 '[:find [(pull ?block ?block-attrs) ...]
-                   :in $ ?day ?future ?block-attrs
-                   :where
-                   (or
-                    [?block :block/scheduled ?d]
-                    [?block :block/deadline ?d])
-                   [(get-else $ ?block :block/repeated? false) ?repeated]
-                   [(get-else $ ?block :block/marker "NIL") ?marker]
-                   [(not= ?marker "DONE")]
-                   [(not= ?marker "CANCELED")]
-                   [(not= ?marker "CANCELLED")]
-                   [(<= ?d ?future)]
-                   (or-join [?repeated ?d ?day]
-                            [(true? ?repeated)]
-                            [(>= ?d ?day)])]
-                 date
-                 (+ date future-days)
-                 block-attrs)
-               react
-               (sort-by-left-recursive)
-               db-utils/group-by-page))))))
+        (->> (react/q repo [:custom :scheduled-deadline journal-title] {}
+               '[:find [(pull ?block ?block-attrs) ...]
+                 :in $ ?day ?future ?block-attrs
+                 :where
+                 (or
+                  [?block :block/scheduled ?d]
+                  [?block :block/deadline ?d])
+                 [(get-else $ ?block :block/repeated? false) ?repeated]
+                 [(get-else $ ?block :block/marker "NIL") ?marker]
+                 [(not= ?marker "DONE")]
+                 [(not= ?marker "CANCELED")]
+                 [(not= ?marker "CANCELLED")]
+                 [(<= ?d ?future)]
+                 (or-join [?repeated ?d ?day]
+                          [(true? ?repeated)]
+                          [(>= ?d ?day)])]
+               date
+               (+ date future-days)
+               block-attrs)
+             react
+             (sort-by-left-recursive)
+             db-utils/group-by-page)))))
 
 (defn- pattern [name]
   (re-pattern (str "(?i)(^|[^\\[#0-9a-zA-Z]|((^|[^\\[])\\[))"
@@ -1176,7 +1172,7 @@
 (defn get-page-unlinked-references
   [page]
   (when-let [repo (state/get-current-repo)]
-    (when (conn/get-conn repo)
+    (when (conn/get-db repo)
       (let [page (util/safe-page-name-sanity-lc page)
             page-id     (:db/id (db-utils/entity [:block/name page]))
             alias-names (get-page-alias-names repo page)
@@ -1205,7 +1201,7 @@
 (defn get-block-referenced-blocks
   [block-uuid]
   (when-let [repo (state/get-current-repo)]
-    (when (conn/get-conn repo)
+    (when (conn/get-db repo)
       (let [block (db-utils/entity [:block/uuid block-uuid])]
         (->> (react/q repo [:frontend.db.react/page<-blocks-or-block<-blocks
                             (:db/id block)]
@@ -1224,18 +1220,17 @@
 (defn get-block-referenced-blocks-ids
   [block-uuid]
   (when-let [repo (state/get-current-repo)]
-    (when-let [conn (conn/get-conn repo)]
-      (let [block (db-utils/entity [:block/uuid block-uuid])]
-        (->> (react/q repo [:frontend.db.react/block<-block-ids
-                            (:db/id block)] {}
-                      '[:find ?ref-block
-                        :in $ ?block-uuid ?block-attrs
-                        :where
-                        [?block :block/uuid ?block-uuid]
-                        [?ref-block :block/refs ?block]]
-                      block-uuid
-                      block-attrs)
-             react)))))
+    (let [block (db-utils/entity [:block/uuid block-uuid])]
+      (->> (react/q repo [:frontend.db.react/block<-block-ids
+                          (:db/id block)] {}
+             '[:find ?ref-block
+               :in $ ?block-uuid ?block-attrs
+               :where
+               [?block :block/uuid ?block-uuid]
+               [?ref-block :block/refs ?block]]
+             block-uuid
+             block-attrs)
+           react))))
 
 (defn get-referenced-blocks-ids
   [page-name-or-block-uuid]
@@ -1255,7 +1250,7 @@
               :where
               [?block :block/content ?content]
               [(?pred $ ?content)]]
-            (conn/get-conn)
+            (conn/get-db)
             pred)
            (take limit)
            db-utils/seq-flatten
@@ -1268,7 +1263,7 @@
 ;; TODO: Does the result preserves the order of the arguments?
 (defn get-blocks-contents
   [repo block-uuids]
-  (let [db (conn/get-conn repo)]
+  (let [db (conn/get-db repo)]
     (db-utils/pull-many repo '[:block/content]
                         (mapv (fn [id] [:block/uuid id]) block-uuids))))
 
@@ -1285,14 +1280,14 @@
 
 (defn cloned?
   [repo-url]
-  (when-let [conn (conn/get-conn repo-url)]
+  (when-let [db (conn/get-db repo-url)]
     (->
      (d/q '[:find ?cloned
             :in $ ?repo-url
             :where
             [?repo :repo/url ?repo-url]
             [?repo :repo/cloned? ?cloned]]
-          conn
+          db
           repo-url)
      ffirst)))
 
@@ -1347,7 +1342,7 @@
             :where
             [?b :block/properties ?p]
             [(?pred $ ?p)]]
-          (conn/get-conn)
+          (conn/get-db)
           pred)
          (map (fn [[e m]]
                 [(get m :template) e]))
@@ -1363,7 +1358,7 @@
              [?b :block/properties ?p]
              [(get ?p :template) ?t]
              [(= ?t ?name)]]
-           (conn/get-conn)
+           (conn/get-db)
            name)
          ffirst)))
 
@@ -1375,16 +1370,16 @@
   ([cache?]
    (if (and cache? @blocks-count-cache)
      @blocks-count-cache
-     (when-let [conn (conn/get-conn)]
-       (let [n (count (d/datoms conn :avet :block/uuid))]
+     (when-let [db (conn/get-db)]
+       (let [n (count (d/datoms db :avet :block/uuid))]
          (reset! blocks-count-cache n)
          n)))))
 
 ;; block/uuid and block/content
 (defn get-all-block-contents
   []
-  (when-let [conn (conn/get-conn)]
-    (->> (d/datoms conn :avet :block/uuid)
+  (when-let [db (conn/get-db)]
+    (->> (d/datoms db :avet :block/uuid)
          (map :v)
          (map (fn [id]
                 (let [e (db-utils/entity [:block/uuid id])]
@@ -1466,7 +1461,7 @@
 (defn delete-page-blocks
   [repo-url page]
   (when page
-    (when-let [db (conn/get-conn repo-url)]
+    (when-let [db (conn/get-db repo-url)]
       (let [page (db-utils/pull [:block/name (util/page-name-sanity-lc page)])]
         (when page
           (let [datoms (d/datoms db :avet :block/page (:db/id page))
@@ -1512,7 +1507,7 @@
              :where
              [?b :block/page ?page]
              [?b :block/pre-block? true]]
-           (conn/get-conn repo)
+           (conn/get-db repo)
            page-id)
       ffirst))
 
@@ -1529,7 +1524,7 @@
         :where
         [?p :block/name ?namespace]
         (namespace ?p ?c)]
-      (conn/get-conn repo)
+      (conn/get-db repo)
       rules
       namespace)))
 
@@ -1559,7 +1554,7 @@
 (defn get-page-namespace-routes
   [repo page]
   (assert (string? page))
-  (when-let [db (conn/get-conn repo)]
+  (when-let [db (conn/get-db repo)]
     (when-not (string/blank? page)
       (let [page (util/page-name-sanity-lc (string/trim page))
             page-exist? (db-utils/entity repo [:block/name page])

+ 0 - 6
src/main/frontend/db/outliner.cljs

@@ -18,9 +18,3 @@
 (defn del-block
   [conn id-or-look-ref]
   (d/transact! conn [[:db.fn/retractEntity id-or-look-ref]]))
-
-(defn del-blocks
-  [ids-or-look-refs]
-  (mapv (fn [id-or-look-ref]
-         [:db.fn/retractEntity id-or-look-ref])
-    ids-or-look-refs))

+ 9 - 9
src/main/frontend/db/react.cljs

@@ -143,7 +143,7 @@
          result-atom (:result (get @query-state k))]
      (when-let [component *query-component*]
        (add-query-component! k component))
-     (when-let [db (conn/get-conn repo)]
+     (when-let [db (conn/get-db repo)]
        (let [result (d/entity db id-or-lookup-ref)
              result-atom (or result-atom (atom nil))]
          (set! (.-state result-atom) result)
@@ -160,7 +160,7 @@
   {:pre [(s/valid? ::react-query-keys k)]}
   (let [kv? (and (vector? k) (= :kv (first k)))
         k (vec (cons repo k))]
-    (when-let [conn (conn/get-conn repo)]
+    (when-let [db (conn/get-db repo)]
       (let [result-atom (get-query-cached-result k)]
         (when-let [component *query-component*]
           (add-query-component! k component))
@@ -168,20 +168,20 @@
           result-atom
           (let [result (cond
                          query-fn
-                         (query-fn conn nil nil)
+                         (query-fn db nil nil)
 
                          inputs-fn
                          (let [inputs (inputs-fn)]
-                           (apply d/q query conn inputs))
+                           (apply d/q query db inputs))
 
                          kv?
-                         (d/entity conn (last k))
+                         (d/entity db (last k))
 
                          (seq inputs)
-                         (apply d/q query conn inputs)
+                         (apply d/q query db inputs)
 
                          :else
-                         (d/q query conn))
+                         (d/q query db))
                 result (transform-fn result)
                 result-atom (or result-atom (atom nil))]
             ;; Don't notify watches now
@@ -296,7 +296,7 @@
   (when (and repo-url
              (seq tx-data)
              (not (:skip-refresh? tx-meta)))
-    (let [db (conn/get-conn repo-url)
+    (let [db (conn/get-db repo-url)
           affected-keys (get-affected-queries-keys tx)]
       (doseq [[k cache] @query-state]
         (let [custom? (= :custom (second k))
@@ -330,7 +330,7 @@
   ([key]
    (sub-key-value (state/get-current-repo) key))
   ([repo-url key]
-   (when (conn/get-conn repo-url)
+   (when (conn/get-db repo-url)
      (let [m (some-> (q repo-url [:kv key] {} key key) react)]
        (if-let [result (get m key)]
          result

+ 8 - 14
src/main/frontend/db/utils.cljs

@@ -29,12 +29,6 @@
 (defn seq-flatten [col]
   (flatten (seq col)))
 
-(defn sort-by-pos
-  [blocks]
-  (sort-by
-   #(get-in % [:block/meta :start-pos])
-   blocks))
-
 (defn group-by-page
   [blocks]
   (if (:block/page (first blocks))
@@ -58,7 +52,7 @@
   ([id-or-lookup-ref]
    (entity (state/get-current-repo) id-or-lookup-ref))
   ([repo id-or-lookup-ref]
-   (when-let [db (conn/get-conn repo)]
+   (when-let [db (conn/get-db repo)]
      (d/entity db id-or-lookup-ref))))
 
 (defn pull
@@ -67,9 +61,9 @@
   ([selector eid]
    (pull (state/get-current-repo) selector eid))
   ([repo selector eid]
-   (when-let [conn (conn/get-conn repo)]
+   (when-let [db (conn/get-db repo)]
      (try
-       (d/pull conn
+       (d/pull db
                selector
                eid)
        (catch js/Error _e
@@ -81,9 +75,9 @@
   ([selector eids]
    (pull-many (state/get-current-repo) selector eids))
   ([repo selector eids]
-   (when-let [conn (conn/get-conn repo)]
+   (when-let [db (conn/get-db repo)]
      (try
-       (d/pull-many conn selector eids)
+       (d/pull-many db selector eids)
        (catch js/Error e
          (js/console.error e))))))
 
@@ -97,7 +91,7 @@
      (let [tx-data (->> (util/remove-nils tx-data)
                         (remove nil?))]
        (when (seq tx-data)
-         (when-let [conn (conn/get-conn repo-url false)]
+         (when-let [conn (conn/get-db repo-url false)]
            (if tx-meta
              (d/transact! conn (vec tx-data) tx-meta)
              (d/transact! conn (vec tx-data)))))))))
@@ -106,11 +100,11 @@
   ([key]
    (get-key-value (state/get-current-repo) key))
   ([repo-url key]
-   (when-let [db (conn/get-conn repo-url)]
+   (when-let [db (conn/get-db repo-url)]
      (some-> (d/entity db key)
              key))))
 
 (defn q
   [query & inputs]
   (when-let [repo (state/get-current-repo)]
-    (apply d/q query (conn/get-conn repo) inputs)))
+    (apply d/q query (conn/get-db repo) inputs)))

+ 0 - 7
src/main/frontend/db_schema.cljs

@@ -60,12 +60,6 @@
    ;; "A", "B", "C"
    :block/priority {}
 
-   ;; TODO: remove
-   ;; 1, 2, 3, etc.
-   :block/level {}
-   ;; TODO: remove
-   :block/meta {}
-
    ;; block key value properties
    :block/properties {}
    ;; vector
@@ -134,7 +128,6 @@
     :block/deadline
     :block/repeated?
     :block/pre-block?
-    :block/level
     :block/heading-level
     :block/type
     :block/properties

+ 12 - 13
src/main/frontend/extensions/code.cljs

@@ -126,7 +126,6 @@
             ["codemirror/mode/yaml-frontmatter/yaml-frontmatter"]
             ["codemirror/mode/yaml/yaml"]
             ["codemirror/mode/z80/z80"]
-            [dommy.core :as dom]
             [frontend.commands :as commands]
             [frontend.db :as db]
             [frontend.extensions.calc :as calc]
@@ -258,10 +257,10 @@
                               (state/set-block-component-editing-mode! true)))
         (.addEventListener element "mousedown"
                            (fn [e]
+                             (util/stop e)
                              (state/clear-selection!)
                              (when-let [block (and (:block/uuid config) (into {} (db/get-block-by-uuid (:block/uuid config))))]
-                               (state/set-editing! id (.getValue editor) block nil false))
-                             (util/stop e)))
+                               (state/set-editing! id (.getValue editor) block nil false))))
         (.save editor)
         (.refresh editor)
         (when default-open?
@@ -318,13 +317,13 @@
   ;; you're trying to focus doesn't yet exist. Adding the requestAnimationFrame
   ;; ensures that the React component re-renders before the :codemirror/focus
   ;; command is run. It's not elegant... open to suggestions for how to fix it!
-  (js/window.requestAnimationFrame
-   (fn []
-     (let [block (state/get-edit-block)
-           block-uuid (:block/uuid block)
-           block-node (util/get-first-block-by-id block-uuid)]
-       (editor-handler/select-block! (:block/uuid block))
-       (let [textarea-ref (.querySelector block-node "textarea")]
-         (.focus (gobj/get textarea-ref codemirror-ref-name)))
-       (util/select-unhighlight! (dom/by-class "selected"))
-       (state/clear-selection!)))))
+  (let [block (state/get-edit-block)
+        block-uuid (:block/uuid block)]
+    (state/clear-edit!)
+    (js/setTimeout
+     (fn []
+       (let [block-node (util/get-first-block-by-id block-uuid)
+             textarea-ref (.querySelector block-node "textarea")]
+         (when-let [codemirror-ref (gobj/get textarea-ref codemirror-ref-name)]
+           (.focus codemirror-ref))))
+     100)))

+ 1 - 1
src/main/frontend/extensions/srs.cljs

@@ -348,7 +348,7 @@
           review-cards-count (count review-cards)
           score-5-count (count (get review-records 5))
           score-1-count (count (get review-records 1))]
-      (editor-handler/paste-block-tree-after-target
+      (editor-handler/insert-block-tree-after-target
        (:db/id card-query-block) false
        [{:content (util/format "Summary: %d items, %d review counts [[%s]]"
                                review-cards-count review-count (date/today))

+ 6 - 6
src/main/frontend/external/roam_export.cljs

@@ -21,12 +21,12 @@
        (str/join)))
 
 (defn uuid->uid-map []
-  (let [conn (db/get-conn (state/get-current-repo))]
-    (->> conn
-     (d/q '[:find (pull ?r [:block/uuid])
-            :in $
-            :where
-            [?b :block/refs ?r]])
+  (let [db (db/get-db (state/get-current-repo))]
+    (->>
+     (d/q db '[:find (pull ?r [:block/uuid])
+               :in $
+               :where
+               [?b :block/refs ?r]])
      (map (comp :block/uuid first))
      (distinct)
      (map (fn [uuid] [uuid (nano-id)]))

+ 95 - 117
src/main/frontend/format/block.cljs

@@ -341,7 +341,7 @@
           refs (distinct (concat (:refs block) ref-blocks))]
       (assoc block :refs refs))))
 
-(defn block-keywordize
+(defn- block-keywordize
   [block]
   (medley/map-keys
    (fn [k]
@@ -350,7 +350,7 @@
        (keyword "block" k)))
    block))
 
-(defn safe-blocks
+(defn- sanity-blocks-data
   [blocks]
   (map (fn [block]
          (if (map? block)
@@ -414,15 +414,13 @@
     block))
 
 (defn- get-block-content
-  [utf8-content block format block-content]
-  (let [meta (:meta block)
-        content (or block-content
-                    (if-let [end-pos (:end-pos meta)]
-                      (utf8/substring utf8-content
-                                      (:start-pos meta)
-                                      end-pos)
-                      (utf8/substring utf8-content
-                                      (:start-pos meta))))
+  [utf8-content block format meta]
+  (let [content (if-let [end-pos (:end_pos meta)]
+                  (utf8/substring utf8-content
+                                  (:start_pos meta)
+                                  end-pos)
+                  (utf8/substring utf8-content
+                                  (:start_pos meta)))
         content (when content
                   (let [content (text/remove-level-spaces content format)]
                     (if (or (:pre-block? block)
@@ -465,25 +463,24 @@
           block-tags->pages
           (update :refs (fn [col] (remove nil? col)))))
 
-(defn extract-blocks*
-  [blocks body pre-block-properties encoded-content with-body?]
+(defn with-pre-block-if-exists
+  [blocks body pre-block-properties encoded-content]
   (let [first-block (first blocks)
-        first-block-start-pos (get-in first-block [:block/meta :start-pos])
+        first-block-start-pos (get-in first-block [:block/meta :start_pos])
+
+        ;; Add pre-block
         blocks (if (or (> first-block-start-pos 0)
                        (empty? blocks))
                  (cons
                   (merge
                    (let [content (utf8/substring encoded-content 0 first-block-start-pos)
-                         {:keys [properties properties-order]} @pre-block-properties
+                         {:keys [properties properties-order]} pre-block-properties
                          id (get-custom-id-or-new-id {:properties properties})
                          property-refs (->> (get-page-refs-from-properties properties)
                                             (map :block/original-name))
                          block {:uuid id
                                 :content content
                                 :level 1
-                                :meta {:start-pos 0
-                                       :end-pos (or first-block-start-pos
-                                                    (utf8/length encoded-content))}
                                 :properties properties
                                 :properties-order properties-order
                                 :refs property-refs
@@ -494,120 +491,102 @@
                      (block-keywordize block))
                    (select-keys first-block [:block/format :block/page]))
                   blocks)
-                 blocks)
-        blocks (map (fn [block]
-                      (if with-body?
-                        block
-                        (dissoc block :block/body))) blocks)]
+                 blocks)]
     (with-path-refs blocks)))
 
-(defn ^:large-vars/cleanup-todo extract-blocks
-  ([blocks content with-id? format]
-   (extract-blocks blocks content with-id? format false))
-  ([blocks content with-id? format with-body?]
-   (try
+(defn- construct-block
+  [block properties timestamps body encoded-content format pos-meta with-id?]
+  (let [id (get-custom-id-or-new-id properties)
+        ref-pages-in-properties (->> (:page-refs properties)
+                                     (remove string/blank?))
+        block (second block)
+        unordered? (:unordered block)
+        markdown-heading? (and (:size block) (= :markdown format))
+        block (if markdown-heading?
+                (assoc block
+                       :type :heading
+                       :level (if unordered? (:level block) 1)
+                       :heading-level (or (:size block) 6))
+                block)
+        block (cond->
+                (assoc block
+                       :uuid id
+                       :refs ref-pages-in-properties
+                       :format format
+                       :meta pos-meta)
+                (seq (:properties properties))
+                (assoc :properties (:properties properties))
+
+                (seq (:properties-order properties))
+                (assoc :properties-order (:properties-order properties)))
+        block (if (get-in block [:properties :collapsed])
+                (assoc block :collapsed? true)
+                block)
+        block (assoc block
+                     :content (get-block-content encoded-content block format pos-meta))
+        block (if (seq timestamps)
+                (merge block (timestamps->scheduled-and-deadline timestamps))
+                block)
+        block (assoc block :body body)
+        block (with-page-block-refs block with-id?)
+        {:keys [created-at updated-at]} (:properties properties)
+        block (cond-> block
+                (and created-at (integer? created-at))
+                (assoc :block/created-at created-at)
+
+                (and updated-at (integer? updated-at))
+                (assoc :block/updated-at updated-at))]
+    (dissoc block :title :body :anchor)))
+
+(defn extract-blocks
+  "Extract headings from mldoc ast.
+  Args:
+    `blocks`: mldoc ast.
+    `content`: markdown or org-mode text.
+    `with-id?`: If `with-id?` equals to true, all the referenced pages will have new db ids.
+    `format`: content's format, it could be either :markdown or :org-mode."
+  [blocks content with-id? format]
+  {:pre [(seq blocks) (string? content) (boolean? with-id?) (contains? #{:markdown :org} format)]}
+  (try
     (let [encoded-content (utf8/encode content)
-          last-pos (utf8/length encoded-content)
-          pre-block-properties (atom nil)
-          [blocks body]
+          [blocks body pre-block-properties]
           (loop [headings []
                  blocks (reverse blocks)
                  timestamps {}
                  properties {}
-                 last-pos last-pos
-                 last-level 1000
-                 children []
-                 block-all-content []
                  body []]
             (if (seq blocks)
-              (let [[block {:keys [start_pos _end_pos] :as block-content}] (first blocks)
-                    block-content (when (string? block-content) block-content)
-                    unordered? (:unordered (second block))
-                    markdown-heading? (and (:size (second block)) (= :markdown format))]
+              (let [[block pos-meta] (first blocks)
+                    ;; fix start_pos
+                    pos-meta (assoc pos-meta :end_pos
+                                    (if (seq headings)
+                                      (get-in (last headings) [:meta :start_pos])
+                                      nil))]
                 (cond
                   (paragraph-timestamp-block? block)
                   (let [timestamps (extract-timestamps block)
                         timestamps' (merge timestamps timestamps)]
-                    (recur headings (rest blocks) timestamps' properties last-pos last-level children (conj block-all-content block-content) body))
+                    (recur headings (rest blocks) timestamps' properties body))
 
                   (property/properties-ast? block)
                   (let [properties (extract-properties format (second block))]
-                    (recur headings (rest blocks) timestamps properties last-pos last-level children (conj block-all-content block-content) body))
+                    (recur headings (rest blocks) timestamps properties body))
 
                   (heading-block? block)
-                  (let [id (get-custom-id-or-new-id properties)
-                        ref-pages-in-properties (->> (:page-refs properties)
-                                                     (remove string/blank?))
-                        block (second block)
-                        block (if markdown-heading?
-                                (assoc block
-                                       :type :heading
-                                       :level (if unordered? (:level block) 1)
-                                       :heading-level (or (:size block) 6))
-                                block)
-                        level (:level block)
-                        [children current-block-children]
-                        (cond
-                          (< level last-level)
-                          (let [current-block-children (set (->> (filter #(< level (second %)) children)
-                                                                 (map first)
-                                                                 (map (fn [id]
-                                                                        [:block/uuid id]))))
-                                others (vec (remove #(< level (second %)) children))]
-                            [(conj others [id level])
-                             current-block-children])
-
-                          (>= level last-level)
-                          [(conj children [id level])
-                           #{}])
-                        block (cond->
-                                (assoc block
-                                       :uuid id
-                                       :refs ref-pages-in-properties
-                                       :children (or current-block-children [])
-                                       :format format)
-                                (seq (:properties properties))
-                                (assoc :properties (:properties properties))
-
-                                (seq (:properties-order properties))
-                                (assoc :properties-order (:properties-order properties)))
-                        block (if (get-in block [:properties :collapsed])
-                                (assoc block :collapsed? true)
-                                block)
-                        block (-> block
-                                  (assoc-in [:meta :start-pos] start_pos)
-                                  (assoc-in [:meta :end-pos] last-pos)
-                                  ((fn [block]
-                                     (assoc block
-                                            :content (get-block-content encoded-content block format (and block-content (string/join "\n" (reverse (conj block-all-content block-content)))))))))
-                        block (if (seq timestamps)
-                                (merge block (timestamps->scheduled-and-deadline timestamps))
-                                block)
-                        block (assoc block :body body)
-                        block (with-page-block-refs block with-id?)
-                        last-pos' (get-in block [:meta :start-pos])
-                        {:keys [created-at updated-at]} (:properties properties)
-                        block (cond-> block
-                                (and created-at (integer? created-at))
-                                (assoc :block/created-at created-at)
-
-                                (and updated-at (integer? updated-at))
-                                (assoc :block/updated-at updated-at))]
-                    (recur (conj headings block) (rest blocks) {} {} last-pos' (:level block) children [] []))
+                  (let [block (construct-block block properties timestamps body encoded-content format pos-meta with-id?)]
+                    (recur (conj headings block) (rest blocks) {} {} []))
 
                   :else
-                  (recur headings (rest blocks) timestamps properties last-pos last-level children
-                         (conj block-all-content block-content)
-                         (conj body block))))
-              (do
-                (when (seq properties)
-                  (reset! pre-block-properties properties))
-                [(-> (reverse headings)
-                     safe-blocks) body])))]
-      (extract-blocks* blocks body pre-block-properties encoded-content with-body?))
+                  (recur headings (rest blocks) timestamps properties (conj body block))))
+              [(-> (reverse headings)
+                   sanity-blocks-data)
+               body
+               properties]))
+          result (with-pre-block-if-exists blocks body pre-block-properties encoded-content)]
+      (map #(dissoc % :block/meta) result))
     (catch js/Error e
       (js/console.error "extract-blocks-failed")
-      (log/error :exception e)))))
+      (log/error :exception e))))
 
 (defn with-parent-and-left
   [page-id blocks]
@@ -615,7 +594,6 @@
          parents [{:page/id page-id     ; db id or a map {:block/name "xxx"}
                    :block/level 0
                    :block/level-spaces 0}]
-         _sibling nil
          result []]
     (if (empty? blocks)
       (map #(dissoc % :block/level-spaces) result)
@@ -623,7 +601,7 @@
             level-spaces (:block/level-spaces block)
             {:block/keys [uuid level parent] :as last-parent} (last parents)
             parent-spaces (:block/level-spaces last-parent)
-            [blocks parents sibling result]
+            [blocks parents result]
             (cond
               (= level-spaces parent-spaces)        ; sibling
               (let [block (assoc block
@@ -632,7 +610,7 @@
                                  :block/level level)
                     parents' (conj (vec (butlast parents)) block)
                     result' (conj result block)]
-                [others parents' block result'])
+                [others parents' result'])
 
               (> level-spaces parent-spaces)         ; child
               (let [parent (if uuid [:block/uuid uuid] (:page/id last-parent))
@@ -649,7 +627,7 @@
                             (assoc :block/level (inc level)))
                     parents' (conj parents block)
                     result' (conj result block)]
-                [others parents' block result'])
+                [others parents' result'])
 
               (< level-spaces parent-spaces)
               (cond
@@ -660,7 +638,7 @@
                                           :block/level (dec level)
                                           :block/left [:block/uuid (:block/uuid left)])
                                    (rest blocks))]
-                  [blocks parents' left result])
+                  [blocks parents' result])
 
                 :else
                 (let [[f r] (split-with (fn [p] (<= (:block/level-spaces p) level-spaces)) parents)
@@ -677,8 +655,8 @@
 
                       parents' (->> (concat f [block]) vec)
                       result' (conj result block)]
-                  [others parents' block result'])))]
-        (recur blocks parents sibling result)))))
+                  [others parents' result'])))]
+        (recur blocks parents result)))))
 
 (defn parse-block
   ([block]

+ 2 - 2
src/main/frontend/handler.cljs

@@ -157,13 +157,13 @@
   (js/document.documentElement.setAttribute "__datalog-console-remote-installed__" true)
   (.addEventListener js/window "message"
                      (fn [event]
-                       (let [conn (conn/get-conn)]
+                       (let [db (conn/get-db)]
                          (when-let [devtool-message (gobj/getValueByKeys event "data" ":datalog-console.client/devtool-message")]
                            (let [msg-type (:type (read-string devtool-message))]
                              (case msg-type
 
                                :datalog-console.client/request-whole-database-as-string
-                               (.postMessage js/window #js {":datalog-console.remote/remote-message" (pr-str conn)} "*")
+                               (.postMessage js/window #js {":datalog-console.remote/remote-message" (pr-str db)} "*")
 
                                nil)))))))
 (defn- get-repos

+ 1 - 1
src/main/frontend/handler/block.cljs

@@ -98,7 +98,7 @@
 (defn load-more!
   [db-id start-id]
   (let [repo (state/get-current-repo)
-        db (db/get-conn repo)
+        db (db/get-db repo)
         block (db/entity repo db-id)
         block? (not (:block/name block))
         k (if block?

+ 27 - 70
src/main/frontend/handler/dnd.cljs

@@ -1,66 +1,29 @@
 (ns frontend.handler.dnd
-  (:require [frontend.db :as db]
-            [frontend.handler.editor :as editor-handler]
+  (:require [frontend.handler.editor :as editor-handler]
             [frontend.modules.outliner.core :as outliner-core]
             [frontend.modules.outliner.tree :as tree]
+            [frontend.modules.outliner.transaction :as outliner-tx]
             [frontend.state :as state]
             [frontend.util :as util]))
 
-(defn- ancestor?
-  "Whether current-block is an ancestor of the target-block."
-  [current-block-uuid target-block]
-  (loop [loc target-block]
-    (if-let [parent (db/entity (:db/id (:block/parent loc)))]
-      (if (= (:block/uuid parent) current-block-uuid)
-        true
-        (recur parent))
-      false)))
-
-(defn- movable?
-  [current-block target-block move-to]
-  (let [current-block-uuid (:block/uuid current-block)]
-    (not
-     (or
-      (= current-block-uuid (:block/uuid target-block)) ; same block
-
-      (ancestor? current-block-uuid target-block)
-
-      (and (= move-to :nested)
-           ;; current block is already the first child of target-block
-           (= (:db/id (:block/left current-block))
-              (:db/id (:block/parent current-block))
-              (:db/id target-block)))
-
-      (and (= move-to :sibling)
-           ;; current block is already the next sibling of target-block
-           (= (:db/id (:block/left current-block))
-              (:db/id target-block)))))))
-
-(defn move-block
-  "There can be two possible situations:
-  1. Move a block in the same file (either top-to-bottom or bottom-to-top).
-  2. Move a block between two different files.
-
-  move-to: :sibling :nested :top nil
-
-  Notes:
-  Sometimes we might need to move a parent block to it's own child.
-  "
-  [^js event current-block target-block move-to]
-  (let [top? (= move-to :top)
+(defn move-blocks
+  [^js event blocks target-block move-to]
+  (let [blocks' (map #(dissoc % :block/level :block/children) blocks)
+        first-block (first blocks')
+        top? (= move-to :top)
         nested? (= move-to :nested)
         alt-key? (and event (.-altKey event))
-        current-format (:block/format current-block)
+        current-format (:block/format first-block)
         target-format (:block/format target-block)]
     (cond
       ;; alt pressed, make a block-ref
-      alt-key?
+      (and alt-key? (= (count blocks) 1))
       (do
-        (editor-handler/set-block-property! (:block/uuid current-block)
+        (editor-handler/set-block-property! (:block/uuid first-block)
                                             :id
-                                            (str (:block/uuid current-block)))
+                                            (str (:block/uuid first-block)))
         (editor-handler/api-insert-new-block!
-         (util/format "((%s))" (str (:block/uuid current-block)))
+         (util/format "((%s))" (str (:block/uuid first-block)))
          {:block-uuid (:block/uuid target-block)
           :sibling? (not nested?)
           :before? top?}))
@@ -73,27 +36,21 @@
                           :clear? true}])
 
 
-      ;; movable
-      (and (every? map? [current-block target-block])
-           (movable? current-block target-block move-to))
-      (let [[current-node target-node]
-            (mapv outliner-core/block [current-block target-block])]
-        (cond
-          top?
-          (let [first-child?
-                (= (tree/-get-parent-id target-node)
-                   (tree/-get-left-id target-node))]
-            (if first-child?
-              (let [parent (tree/-get-parent target-node)]
-                (outliner-core/move-subtree current-node parent false))
-              (let [before-node (tree/-get-left target-node)]
-                (outliner-core/move-subtree current-node before-node true))))
-
-          nested?
-          (outliner-core/move-subtree current-node target-node false)
-
-          :else ;; :sibling
-          (outliner-core/move-subtree current-node target-node true)))
+      (every? map? (conj blocks target-block))
+      (let [target-node (outliner-core/block target-block)]
+        (outliner-tx/transact!
+          {:outliner-op :move-blocks}
+          (editor-handler/save-current-block!)
+          (if top?
+            (let [first-child?
+                  (= (tree/-get-parent-id target-node)
+                     (tree/-get-left-id target-node))]
+              (if first-child?
+                (let [parent (tree/-get-parent target-node)]
+                  (outliner-core/move-blocks! blocks (:data parent) false))
+                (let [before-node (tree/-get-left target-node)]
+                  (outliner-core/move-blocks! blocks (:data before-node) true))))
+            (outliner-core/move-blocks! blocks target-block (not nested?)))))
 
       :else
       nil)))

File diff suppressed because it is too large
+ 259 - 495
src/main/frontend/handler/editor.cljs


+ 22 - 23
src/main/frontend/handler/export.cljs

@@ -35,7 +35,7 @@
                           [?p :block/file ?f]
                           [?p :block/name ?pn]
                           [?f :file/path ?path]]
-                        (db/get-conn repo) file-path))]
+                        (db/get-db repo) file-path))]
     (get-page-content repo page-name)
     (ffirst
      (d/q '[:find ?content
@@ -43,7 +43,7 @@
             :where
             [?f :file/path ?path]
             [?f :file/content ?content]]
-          (db/get-conn repo) file-path))))
+          (db/get-db repo) file-path))))
 
 (defn- get-blocks-contents
   [repo root-block-uuid]
@@ -73,7 +73,7 @@
 
 (defn export-repo-as-html!
   [repo]
-  (when-let [db (db/get-conn repo)]
+  (when-let [db (db/get-db repo)]
     (let [[db asset-filenames]           (if (state/all-pages-public?)
                                            (db/clean-export! db)
                                            (db/filter-only-public-pages-and-blocks db))
@@ -105,12 +105,12 @@
   ([repo]
    (get-file-contents repo {:init-level 1}))
   ([repo file-opts]
-   (let [conn (db/get-conn repo)]
+   (let [db (db/get-db repo)]
      (->> (d/q '[:find ?n ?fp
                  :where
                  [?e :block/file ?f]
                  [?f :file/path ?fp]
-                 [?e :block/name ?n]] conn)
+                 [?e :block/name ?n]] db)
           (mapv (fn [[page-name file-path]]
                   [file-path
                    (outliner-file/tree->file-content
@@ -133,12 +133,11 @@
 (defn get-md-file-contents
   [repo]
   #_:clj-kondo/ignore
-  (let [conn (db/get-conn repo)]
-    (filter (fn [[path _]]
-              (let [path (string/lower-case path)]
-                (re-find #"\.(?:md|markdown)$" path)))
-            (get-file-contents repo {:init-level 1
-                                     :heading-to-list? true}))))
+  (filter (fn [[path _]]
+            (let [path (string/lower-case path)]
+              (re-find #"\.(?:md|markdown)$" path)))
+          (get-file-contents repo {:init-level 1
+                                   :heading-to-list? true})))
 
 
 (defn- get-embed-pages-from-ast [ast]
@@ -368,7 +367,7 @@
 
 (defn- get-file-contents-with-suffix
   [repo]
-  (let [conn (db/get-conn repo)
+  (let [db (db/get-db repo)
         md-files (get-md-file-contents repo)]
     (->>
      md-files
@@ -378,7 +377,7 @@
                                               :where [?e :file/path ?p]
                                               [?e2 :block/file ?e]
                                               [?e2 :block/name ?n]
-                                              [?e2 :block/original-name ?n2]] conn path)
+                                              [?e2 :block/original-name ?n2]] db path)
                                 :format (f/get-format path)})))))
 
 
@@ -431,7 +430,7 @@
        x))
    vec-tree))
 
-(defn- blocks [conn]
+(defn- blocks [db]
   {:version 1
    :blocks
    (->> (d/q '[:find (pull ?b [*])
@@ -439,7 +438,7 @@
                :where
                [?b :block/file]
                [?b :block/original-name]
-               [?b :block/name]] conn)
+               [?b :block/name]] db)
 
         (map (fn [[{:block/keys [name] :as page}]]
                (assoc page
@@ -466,9 +465,9 @@
       (str "." (string/lower-case (name extension)))))
 
 (defn- export-repo-as-edn-str [repo]
-  (when-let [conn (db/get-conn repo)]
+  (when-let [db (db/get-db repo)]
     (let [sb (StringBuffer.)]
-      (pprint/pprint (blocks conn) (StringBufferWriter. sb))
+      (pprint/pprint (blocks db) (StringBufferWriter. sb))
       (str sb))))
 
 (defn export-repo-as-edn-v2!
@@ -492,9 +491,9 @@
 
 (defn export-repo-as-json-v2!
   [repo]
-  (when-let [conn (db/get-conn repo)]
+  (when-let [db (db/get-db repo)]
     (let [json-str
-          (-> (blocks conn)
+          (-> (blocks db)
               nested-update-id
               clj->js
               js/JSON.stringify)
@@ -511,13 +510,13 @@
 
 ;; https://roamresearch.com/#/app/help/page/Nxz8u0vXU
 ;; export to roam json according to above spec
-(defn- roam-json [conn]
+(defn- roam-json [db]
   (->> (d/q '[:find (pull ?b [*])
               :in $
               :where
               [?b :block/file]
               [?b :block/original-name]
-              [?b :block/name]] conn)
+              [?b :block/name]] db)
 
        (map (fn [[{:block/keys [name] :as page}]]
               (assoc page
@@ -536,9 +535,9 @@
 
 (defn export-repo-as-roam-json!
   [repo]
-  (when-let [conn (db/get-conn repo)]
+  (when-let [db (db/get-db repo)]
     (let [json-str
-          (-> (roam-json conn)
+          (-> (roam-json db)
               clj->js
               js/JSON.stringify)
           data-str (str "data:text/json;charset=utf-8,"

+ 5 - 7
src/main/frontend/handler/external.cljs

@@ -89,11 +89,9 @@
                                       [last-block true]
                                       (if snd-last-block
                                         [snd-last-block true]
-                                        [page-block false]))
-            tree (editor/blocks->tree-by-level parsed-blocks)]
-        (editor/paste-block-vec-tree-at-target
-         tree []
-         {:get-pos-fn #(editor/get-block-tree-insert-pos-after-target
-                        (:db/id target-block) sibling?)
-          :page-block page-block})
+                                        [page-block false]))]
+        (editor/paste-blocks
+         parsed-blocks
+         {:target target-block
+          :sibling? sibling?})
         (finished-ok-handler [page-name])))))

+ 4 - 6
src/main/frontend/handler/page.cljs

@@ -374,7 +374,7 @@
                                   :block/original-name new-name}]
             page-txs            (if properties-block-tx (conj page-txs properties-block-tx) page-txs)]
 
-        (d/transact! (db/get-conn repo false) page-txs)
+        (d/transact! (db/get-db repo false) page-txs)
 
         ;; If page name changed after sanitization
         (when (or (util/create-title-property? new-page-name)
@@ -471,9 +471,9 @@
                                     (outliner-core/block)
                                     (outliner-tree/-get-down)
                                     (outliner-core/get-data))
-          to-last-direct-child-id (model/get-block-last-direct-child (db/get-conn) to-id)
+          to-last-direct-child-id (model/get-block-last-direct-child (db/get-db) to-id false)
           repo (state/get-current-repo)
-          conn (conn/get-conn repo false)
+          conn (conn/get-db repo false)
           datoms (d/datoms @conn :avet :block/page from-id)
           block-eids (mapv :e datoms)
           blocks (db-utils/pull-many repo '[:db/id :block/page :block/refs :block/path-refs :block/left :block/parent] block-eids)
@@ -737,9 +737,7 @@
                   (editor-handler/insert-template!
                    nil
                    template
-                   {:get-pos-fn (fn []
-                                  [page false false false])
-                    :page-block page})))
+                   {:target page})))
               (ui-handler/re-render-root!))))))))
 
 (defn open-today-in-sidebar

+ 2 - 2
src/main/frontend/handler/repo.cljs

@@ -351,7 +351,7 @@
              :as opts}]
   (spec/validate :repos/url repo-url)
   (when (and
-         (db/get-conn repo-url true)
+         (db/get-db repo-url true)
          (db/cloned? repo-url))
     (p/let [remote-latest-commit (common-handler/get-remote-ref repo-url)
             local-latest-commit (common-handler/get-ref repo-url)
@@ -519,7 +519,7 @@
   [{:keys [id url] :as repo}]
   ;; (spec/validate :repos/repo repo)
   (let [delete-db-f (fn []
-                      (let [graph-exists? (db/get-conn url)]
+                      (let [graph-exists? (db/get-db url)]
                         (db/remove-conn! url)
                         (db-persist/delete-graph! url)
                         (search/remove-db! url)

+ 1 - 2
src/main/frontend/mobile/footer.cljs

@@ -58,11 +58,10 @@
              block (editor-handler/api-insert-new-block!
                     ""
                     {:page page
-                     :reuse-last-block? true})]
+                     :replace-empty-target? true})]
          (js/setTimeout
           (fn [] (editor-handler/edit-block!
                   block
                   :max
                   (:block/uuid block))) 100))
       "edit")]))
-

+ 3 - 3
src/main/frontend/mobile/intent.cljs

@@ -53,7 +53,7 @@
     (if (state/get-edit-block)
       (state/append-current-edit-content! values)
       (editor-handler/api-insert-new-block! values {:page page
-                                                    :reuse-last-block? true}))))
+                                                    :replace-empty-target? true}))))
 
 (defn- embed-asset-file [url format]
   (p/let [basename (path/basename url)
@@ -99,7 +99,7 @@
     (if (state/get-edit-block)
       (state/append-current-edit-content! content)
       (editor-handler/api-insert-new-block! content {:page page
-                                                     :reuse-last-block? true}))))
+                                                     :replace-empty-target? true}))))
 
 (defn- handle-received-application [result]
   (p/let [{:keys [title url type]} result
@@ -124,7 +124,7 @@
     (if (state/get-edit-block)
       (state/append-current-edit-content! content)
       (editor-handler/api-insert-new-block! content {:page page
-                                                     :reuse-last-block? true}))))
+                                                     :replace-empty-target? true}))))
 
 (defn decode-received-result [m]
   (into {} (for [[k v] m]

+ 1 - 1
src/main/frontend/mobile/record.cljs

@@ -60,7 +60,7 @@
     (if edit-block
       (state/append-current-edit-content! file-link)
       (editor-handler/api-insert-new-block! file-link {:page page
-                                                       :reuse-last-block? true}))))
+                                                       :replace-empty-target? true}))))
 
 (defn stop-recording []
   (p/catch

+ 1 - 1
src/main/frontend/modules/editor/undo_redo.cljs

@@ -93,7 +93,7 @@
 
 (defn- transact!
   [txs]
-  (let [conn (conn/get-conn false)
+  (let [conn (conn/get-db false)
         db-report (d/transact! conn txs)]
     (pipelines/invoke-hooks db-report)))
 

+ 516 - 408
src/main/frontend/modules/outliner/core.cljs

@@ -1,6 +1,7 @@
 (ns frontend.modules.outliner.core
   (:require [clojure.set :as set]
-            [clojure.zip :as zip]
+            [clojure.string :as string]
+            [datascript.impl.entity :as de]
             [frontend.db :as db]
             [frontend.db.model :as db-model]
             [frontend.db-schema :as db-schema]
@@ -10,7 +11,14 @@
             [frontend.modules.outliner.tree :as tree]
             [frontend.modules.outliner.utils :as outliner-u]
             [frontend.state :as state]
-            [frontend.util :as util]))
+            [frontend.util :as util]
+            [cljs.spec.alpha :as s]))
+
+(s/def ::block-map (s/keys :req [:db/id :block/uuid]
+                           :opt [:block/page :block/left :block/parent]))
+
+(s/def ::block-map-or-entity (s/or :entity de/entity?
+                                   :map ::block-map))
 
 (defrecord Block [data])
 
@@ -25,7 +33,7 @@
 
 (defn get-block-by-id
   [id]
-  (let [c (conn/get-conn false)
+  (let [c (conn/get-db false)
         r (db-outliner/get-by-id c (outliner-u/->block-lookup-ref id))]
     (when r (->Block r))))
 
@@ -34,7 +42,7 @@
   (let [parent-id (:db/id (db/entity [:block/uuid parent-uuid]))
         left-id (:db/id (db/entity [:block/uuid left-uuid]))]
     (some->
-     (db-model/get-by-parent-&-left (conn/get-conn) parent-id left-id)
+     (db-model/get-by-parent-&-left (conn/get-db) parent-id left-id)
      :db/id
      db/pull
      block)))
@@ -192,229 +200,83 @@
           children (db-model/get-block-immediate-children (state/get-current-repo) parent-id)]
       (map block children))))
 
-(defn set-block-collapsed! [txs-state id collapsed?]
-  (swap! txs-state concat [{:db/id id
-                            :block/collapsed? collapsed?}]))
-
-(defn save-node
-  ([node]
-   (save-node node nil))
-  ([node {:keys [txs-state]}]
-   (if txs-state
-     (tree/-save node txs-state)
-     (ds/auto-transact!
-      [db (ds/new-outliner-txs-state)] {:outliner-op :save-node}
-      (tree/-save node db)))))
-
-(defn insert-node-as-first-child
-  "Insert a node as first child."
-  [txs-state new-node parent-node]
-  {:pre [(every? tree/satisfied-inode? [new-node parent-node])]}
-  (let [parent-id (tree/-get-id parent-node)
-        node (-> (tree/-set-left-id new-node parent-id)
-               (tree/-set-parent-id parent-id))
-        right-node (tree/-get-down parent-node)]
-    (if (tree/satisfied-inode? right-node)
-      (let [new-right-node (tree/-set-left-id right-node (tree/-get-id new-node))
-            saved-new-node (tree/-save node txs-state)]
-        (tree/-save new-right-node txs-state)
-        [saved-new-node new-right-node])
-      (do
-        (tree/-save node txs-state)
-        [node]))))
-
-(defn insert-node-as-sibling
-  "Insert a node as sibling."
-  [txs-state new-node left-node]
-  {:pre [(every? tree/satisfied-inode? [new-node left-node])]}
-  (when-let [left-id (tree/-get-id left-node)]
-    (let [node (-> (tree/-set-left-id new-node left-id)
-                   (tree/-set-parent-id (tree/-get-parent-id left-node)))
-          right-node (tree/-get-right left-node)]
-      (if (tree/satisfied-inode? right-node)
-        (let [new-right-node (tree/-set-left-id right-node (tree/-get-id new-node))
-              saved-new-node (tree/-save node txs-state)]
-          (tree/-save new-right-node txs-state)
-          [saved-new-node new-right-node])
-        (do
-          (tree/-save node txs-state)
-          [node])))))
-
-
-(defn- insert-node-aux
-  ([new-node target-node sibling? txs-state]
-   (insert-node-aux new-node target-node sibling? txs-state nil))
-  ([new-node target-node sibling? txs-state blocks-atom]
-   (let [result (if sibling?
-                  (insert-node-as-sibling txs-state new-node target-node)
-                  (insert-node-as-first-child txs-state new-node target-node))]
-     (when blocks-atom
-       (swap! blocks-atom concat result))
-     (first result))))
-
-;; TODO: refactor, move to insert-node
-(defn insert-node-as-last-child
-  [txs-state node target-node]
-  []
-  {:pre [(every? tree/satisfied-inode? [node target-node])]}
-  (let [children (tree/-get-children target-node)
-        [target-node sibling?] (if (seq children)
-                                 [(last children) true]
-                                 [target-node false])]
-    (insert-node-aux node target-node sibling? txs-state)))
-
-(defn insert-node
-  ([new-node target-node sibling?]
-   (insert-node new-node target-node sibling? nil))
-  ([new-node target-node sibling? {:keys [blocks-atom skip-transact? txs-state]
-                                   :or {skip-transact? false}}]
-   (if txs-state
-     (insert-node-aux new-node target-node sibling? txs-state blocks-atom)
-     (ds/auto-transact!
-      [txs-state (ds/new-outliner-txs-state)]
-      {:outliner-op :insert-node
-       :skip-transact? skip-transact?}
-      (insert-node-aux new-node target-node sibling? txs-state blocks-atom)))))
-
-(defn- walk-&-insert-nodes
-  [loc target-node sibling? transact]
-  (let [update-node-fn
-        (fn [_node new-node] new-node)]
-    (if (zip/end? loc)
-      loc
-      (if (vector? (zip/node loc))
-        (recur (zip/next loc) target-node sibling? transact)
-        (let [left1 (zip/left loc)
-              left2 (zip/left (zip/left loc))]
-          (if-let [left (or (and left1 (not (vector? (zip/node left1))) left1)
-                            (and left2 (not (vector? (zip/node left2))) left2))]
-            ;; found left sibling loc
-            (let [new-node
-                  (insert-node-aux (zip/node loc) (zip/node left) true transact)]
-              (recur (zip/next (zip/edit loc update-node-fn new-node)) target-node sibling? transact))
-            ;; else: need to find parent loc
-            (if-let [parent (-> loc zip/up zip/left)]
-              (let [new-node
-                    (insert-node-aux (zip/node loc) (zip/node parent) false transact)]
-                (recur (zip/next (zip/edit loc update-node-fn new-node)) target-node sibling? transact))
-              ;; else: not found parent, it should be the root node
-              (let [new-node
-                    (insert-node-aux (zip/node loc) target-node sibling? transact)]
-                (recur (zip/next (zip/edit loc update-node-fn new-node)) target-node sibling? transact)))))))))
-
-
-(defn- get-node-tree-topmost-last-loc
-  [loc]
-  (let [result-loc-or-vec (zip/rightmost (zip/down loc))]
-    (if (vector? (zip/node result-loc-or-vec))
-      (zip/left result-loc-or-vec)
-      result-loc-or-vec)))
-
-(defn insert-nodes
-  "Insert nodes as children(or siblings) of target-node.
-  new-nodes-tree is an vector of blocks, e.g [1 [2 3] 4 [5 [6 7]]]"
-  [new-nodes-tree target-node sibling?]
-  (ds/auto-transact!
-   [txs-state (ds/new-outliner-txs-state)] {:outliner-op :insert-nodes}
-   ;; TODO: validate new-nodes-tree structure
-   (let [loc (zip/vector-zip new-nodes-tree)
-         updated-nodes (walk-&-insert-nodes loc target-node sibling? txs-state)
-         loc (zip/vector-zip (zip/root updated-nodes))
-         ;; topmost-last-loc=4, new-nodes-tree=[1 [2 3] 4 [5 [6 7]]]
-         topmost-last-loc (get-node-tree-topmost-last-loc loc)
-         right-node (tree/-get-right target-node)
-         down-node (tree/-get-down target-node)]
-     ;; update node's left&parent after inserted nodes
-     (cond
-       (and (not sibling?) (some? right-node) (nil? down-node))
-       nil            ;ignore
-       (and sibling? (some? right-node) topmost-last-loc) ;; right-node.left=N
-       (let [topmost-last-node (zip/node topmost-last-loc)
-             updated-node (tree/-set-left-id right-node (tree/-get-id topmost-last-node))]
-         (tree/-save updated-node txs-state))
-       (and (not sibling?) (some? down-node) topmost-last-loc) ;; down-node.left=N
-       (let [topmost-last-node (zip/node topmost-last-loc)
-             updated-node (tree/-set-left-id down-node (tree/-get-id topmost-last-node))]
-         (tree/-save updated-node txs-state))
-       (and sibling? (some? down-node)) ;; unchanged
-       nil))))
-
-(defn move-nodes
-  "Move nodes up/down."
-  [nodes up?]
-  (ds/auto-transact!
-    [txs-state (ds/new-outliner-txs-state)] {:outliner-op :move-nodes}
-    (let [first-node (first nodes)
-          last-node (last nodes)
-          left (tree/-get-left first-node)
-          move-to-another-parent? (if up?
-                                    (= left (tree/-get-parent first-node))
-                                    (and (tree/-get-parent last-node)
-                                         (nil? (tree/-get-right last-node))))
-          [up-node down-node] (if up?
-                                [left last-node]
-                                (let [down-node (if move-to-another-parent?
-                                                  (tree/-get-right (tree/-get-parent last-node))
-                                                  (tree/-get-right last-node))]
-                                  [first-node down-node]))]
-      (when (and up-node down-node)
-        (cond
-          (and move-to-another-parent? up?)
-          (when-let [target (tree/-get-left up-node)]
-            (when (and (not (:block/name (:data target))) ; page root block
-                       (not (= target
-                               (when-let [parent (tree/-get-parent first-node)]
-                                 (tree/-get-parent parent)))))
-              (insert-node-as-last-child txs-state first-node target)
-              (let [parent-id (tree/-get-id target)]
-                (doseq [node (rest nodes)]
-                  (let [node (tree/-set-parent-id node parent-id)]
-                    (tree/-save node txs-state))))
-              (when-let [down-node-right (tree/-get-right down-node)]
-                (let [down-node-right (tree/-set-left-id down-node-right (tree/-get-id (tree/-get-parent first-node)))]
-                  (tree/-save down-node-right txs-state)))))
-
-          move-to-another-parent?       ; down?
-          (do
-            (insert-node-as-first-child txs-state first-node down-node)
-            (let [parent-id (tree/-get-id down-node)]
-              (doseq [node (rest nodes)]
-                (let [node (tree/-set-parent-id node parent-id)]
-                  (tree/-save node txs-state))))
-            (when-let [down-node-down (tree/-get-down down-node)]
-              (let [down-node-down (tree/-set-left-id down-node-down (tree/-get-id last-node))]
-                (tree/-save down-node-down txs-state))))
-
-          up?                           ; sibling
-          (let [first-node (tree/-set-left-id first-node (tree/-get-left-id left))
-                left (tree/-set-left-id left (tree/-get-id last-node))]
-            (tree/-save first-node txs-state)
-            (tree/-save left txs-state)
-            (when-let [down-node-right (tree/-get-right down-node)]
-              (let [down-node-right (tree/-set-left-id down-node-right (tree/-get-id left))]
-                (tree/-save down-node-right txs-state))))
-
-          :else                       ; down && sibling
-          (let [first-node (tree/-set-left-id first-node (tree/-get-id down-node))
-                down-node (tree/-set-left-id down-node (tree/-get-id left))]
-            (tree/-save first-node txs-state)
-            (tree/-save down-node txs-state)
-            (when-let [down-node-right (tree/-get-right down-node)]
-              (let [down-node-right (tree/-set-left-id down-node-right (tree/-get-id last-node))]
-                (tree/-save down-node-right txs-state)))))))))
-
-(defn delete-node
-  "Delete node from the tree."
-  [node children?]
+(defn get-right-node
+  [node]
   {:pre [(tree/satisfied-inode? node)]}
-  (ds/auto-transact!
-    [txs-state (ds/new-outliner-txs-state)] {:outliner-op :delete-node}
-    (let [right-node (tree/-get-right node)]
-      (tree/-del node txs-state children?)
-      (when (tree/satisfied-inode? right-node)
-        (let [left-node (tree/-get-left node)
-              new-right-node (tree/-set-left-id right-node (tree/-get-id left-node))]
-          (tree/-save new-right-node txs-state))))))
+  (tree/-get-right node))
+
+(defn get-right-sibling
+  [db-id]
+  (when db-id
+    (when-let [block (db/entity db-id)]
+      (db-model/get-by-parent-&-left (conn/get-db)
+                                     (:db/id (:block/parent block))
+                                     db-id))))
+
+
+
+(defn- assoc-level-aux
+  [tree-vec children-key init-level]
+  (map (fn [block]
+         (let [children (get block children-key)
+               children' (assoc-level-aux children children-key (inc init-level))]
+           (cond-> (assoc block :block/level init-level)
+             (seq children')
+             (assoc children-key children')))) tree-vec))
+
+(defn- assoc-level
+  [children-key tree-vec]
+  (assoc-level-aux tree-vec children-key 1))
+
+(defn- assign-temp-id
+  [blocks replace-empty-target? target-block]
+  (map-indexed (fn [idx block]
+                 (let [db-id (if (and replace-empty-target? (zero? idx))
+                               (:db/id target-block)
+                               (dec (- idx)))]
+                   (assoc block :db/id db-id))) blocks))
+
+(defn- find-outdented-block-prev-hop
+  [outdented-block blocks]
+  (let [blocks (reverse
+                (take-while #(not= (:db/id outdented-block)
+                                   (:db/id %)) blocks))
+        blocks (drop-while #(= (:db/id (:block/parent outdented-block)) (:db/id (:block/parent %))) blocks)]
+    (when (seq blocks)
+      (loop [blocks blocks
+             matched (first blocks)]
+        (if (= (:block/parent (first blocks)) (:block/parent matched))
+          (recur (rest blocks) (first blocks))
+          matched)))))
+
+(defn- compute-block-parent
+  [block parent target-block prev-hop top-level? sibling? get-new-id]
+  (cond
+    prev-hop
+    (:db/id (:block/parent prev-hop))
+
+    top-level?
+    (if sibling?
+      (:db/id (:block/parent target-block))
+      (:db/id target-block))
+
+    :else
+    (get-new-id block parent)))
+
+(defn- compute-block-left
+  [blocks block left target-block prev-hop idx replace-empty-target? left-exists-in-blocks? get-new-id]
+  (cond
+    (zero? idx)
+    (if replace-empty-target?
+      (:db/id (:block/left target-block))
+      (:db/id target-block))
+
+    (and prev-hop (not left-exists-in-blocks?))
+    (:db/id (:block/left prev-hop))
+
+    :else
+    (or (get-new-id block left)
+        (get-new-id block (nth blocks (dec idx))))))
 
 (defn- get-left-nodes
   [node limit]
@@ -430,68 +292,78 @@
            result)
          result)))))
 
-(defn delete-nodes
-  "Delete nodes from the tree.
-  Args:
-    start-node: the node at the top of the outliner document.
-    end-node: the node at the bottom of the outliner document
-    block-ids: block ids between the start node and end node, including all the
-  children.
-  "
-  [start-node end-node block-ids]
-  {:pre [(tree/satisfied-inode? start-node)
-         (tree/satisfied-inode? end-node)]}
-  (ds/auto-transact!
-   [txs-state (ds/new-outliner-txs-state)]
-   {:outliner-op :delete-nodes}
-   (let [end-node-parents (->>
-                           (db/get-block-parents
-                            (state/get-current-repo)
-                            (tree/-get-id end-node)
-                            1000)
-                           (map :block/uuid)
-                           (set))
-         self-block? (contains? end-node-parents (tree/-get-id start-node))]
-     (if (or (= start-node end-node)
-             self-block?)
-       (delete-node start-node true)
-       (let [sibling? (= (tree/-get-parent-id start-node)
-                         (tree/-get-parent-id end-node))
-             right-node (tree/-get-right end-node)]
-         (when (tree/satisfied-inode? right-node)
-           (let [left-node-id (if sibling?
-                                (tree/-get-id (tree/-get-left start-node))
-                                (let [end-node-left-nodes (get-left-nodes end-node (count block-ids))
-                                      parents (->>
-                                               (db/get-block-parents
-                                                (state/get-current-repo)
-                                                (tree/-get-id start-node)
-                                                1000)
-                                               (map :block/uuid)
-                                               (set))
-                                      result (first (set/intersection (set end-node-left-nodes) parents))]
-                                  (when-not result
-                                    (util/pprint {:parents parents
-                                                  :end-node-left-nodes end-node-left-nodes}))
-                                  result))]
-             (assert left-node-id "Can't find the left-node-id")
-             (let [new-right-node (tree/-set-left-id right-node left-node-id)]
-               (tree/-save new-right-node txs-state))))
-         (let [txs (db-outliner/del-blocks block-ids)]
-           (ds/add-txs txs-state txs)))))))
-
-(defn first-child?
-  [node]
-  (=
-   (tree/-get-left-id node)
-   (tree/-get-parent-id node)))
+(defn- page-first-child?
+  [block]
+  (= (:block/left block)
+     (:block/page block)))
 
-(defn- first-level?
-  "Can't be outdented."
-  [node]
-  (nil? (tree/-get-parent (tree/-get-parent node))))
+(defn- page-block?
+  [block]
+  (some? (:block/name block)))
+
+;;; ### public utils
+
+(defn tree-vec-flatten
+  "Converts a `tree-vec` to blocks with `:block/level`.
+  A `tree-vec` example:
+  [{:id 1, :children [{:id 2,
+                       :children [{:id 3}]}]}
+   {:id 4, :children [{:id 5}
+                      {:id 6}]}]"
+  ([tree-vec]
+   (tree-vec-flatten tree-vec :children))
+  ([tree-vec children-key]
+   (->> tree-vec
+        (assoc-level children-key)
+        (mapcat #(tree-seq map? children-key %))
+        (map #(dissoc % :block/children)))))
+
+(defn save-block
+  "Save the `block`."
+  [block']
+  {:pre [(map? block')]}
+  (let [txs-state (atom [])]
+    (tree/-save (block block') txs-state)
+    {:tx-data @txs-state}))
+
+(defn blocks-with-level
+  "Calculate `:block/level` for all the `blocks`. Blocks should be sorted already."
+  [blocks]
+  {:pre [(seq blocks)]}
+  (let [blocks (if (sequential? blocks) blocks [blocks])
+        root (assoc (first blocks) :block/level 1)]
+    (loop [m [root]
+           blocks (rest blocks)]
+      (if (empty? blocks)
+        m
+        (let [block (first blocks)
+              parent (:block/parent block)
+              parent-level (when parent
+                             (:block/level
+                              (first
+                               (filter (fn [x]
+                                         (or
+                                          (and (map? parent)
+                                               (= (:db/id x) (:db/id parent)))
+                                          ;; lookup
+                                          (and (vector? parent)
+                                               (= (:block/uuid x) (second parent))))) m))))
+              level (if parent-level
+                      (inc parent-level)
+                      1)
+              block (assoc block :block/level level)
+              m' (vec (conj m block))]
+          (recur m' (rest blocks)))))))
+
+(defn get-top-level-blocks
+  "Get only the top level blocks."
+  [blocks]
+  {:pre [(seq blocks)]}
+  (let [level-blocks (blocks-with-level blocks)]
+    (filter (fn [b] (= 1 (:block/level b))) level-blocks)))
 
 (defn get-right-siblings
+  "Get `node`'s right siblings."
   [node]
   {:pre [(tree/satisfied-inode? node)]}
   (when-let [parent (tree/-get-parent node)]
@@ -500,128 +372,364 @@
            last
            rest))))
 
-(defn- logical-outdenting
-  [txs-state parent nodes first-node last-node last-node-right parent-parent-id parent-right]
-  (some-> last-node-right
-          (tree/-set-left-id (tree/-get-left-id first-node))
-          (tree/-save txs-state))
-  (let [first-node (tree/-set-left-id first-node (tree/-get-id parent))]
-    (doseq [node (cons first-node (rest nodes))]
-      (-> (tree/-set-parent-id node parent-parent-id)
-          (tree/-save txs-state))))
-  (some-> parent-right
-          (tree/-set-left-id (tree/-get-id last-node))
-          (tree/-save txs-state)))
-
-(defn indent-outdent-nodes
-  [nodes indent?]
-  (ds/auto-transact!
-   [txs-state (ds/new-outliner-txs-state)] {:outliner-op :indent-outdent-nodes}
-   (let [first-node (first nodes)
-         last-node (last nodes)]
-     (if indent?
-       (when-not (first-child? first-node)
-         (let [first-node-left-id (tree/-get-left-id first-node)
-               last-node-right (tree/-get-right last-node)
-               parent-or-last-child-id (or (-> (db/get-block-immediate-children (state/get-current-repo)
-                                                                                first-node-left-id)
-                                               last
-                                               :block/uuid)
-                                           first-node-left-id)
-               first-node (tree/-set-left-id first-node parent-or-last-child-id)]
-           (doseq [node (cons first-node (rest nodes))]
-             (-> (tree/-set-parent-id node first-node-left-id)
-                 (tree/-save txs-state)))
-           (some-> last-node-right
-                   (tree/-set-left-id first-node-left-id)
-                   (tree/-save txs-state))
-           (when-let [parent (get-block-by-id first-node-left-id)]
-             (when (db-model/block-collapsed? first-node-left-id)
-               (set-block-collapsed! txs-state (:db/id (get-data parent)) false)))))
-       (when-not (first-level? first-node)
-         (let [parent (tree/-get-parent first-node)
-               parent-parent-id (tree/-get-parent-id parent)
-               parent-right (tree/-get-right parent)
-               last-node-right (tree/-get-right last-node)
-               last-node-id (tree/-get-id last-node)]
-           (logical-outdenting txs-state parent nodes first-node last-node last-node-right parent-parent-id parent-right)
-           (when-not (state/logical-outdenting?)
-             ;; direct outdenting (the old behavior)
-             (let [right-siblings (get-right-siblings last-node)
-                   right-siblings (doall
-                                   (map (fn [sibling]
-                                          (some->
-                                           (tree/-set-parent-id sibling last-node-id)
-                                           (tree/-save txs-state)))
-                                     right-siblings))]
-               (when-let [last-node-right (first right-siblings)]
-                 (let [last-node-children (tree/-get-children last-node)
-                       left-id (if (seq last-node-children)
-                                 (tree/-get-id (last last-node-children))
-                                 last-node-id)]
-                   (when left-id
-                     (some-> (tree/-set-left-id last-node-right left-id)
-                             (tree/-save txs-state)))))))))))))
-
-(defn- set-nodes-page-aux
-  [node page page-format txs-state]
-  (let [new-node (update node :data assoc
-                         :block/page page
-                         :block/format page-format)]
-    (tree/-save new-node txs-state)
-    (doseq [n (tree/-get-children new-node)]
-      (set-nodes-page-aux n page page-format txs-state))))
-
-(defn- set-nodes-page
-  [node target-node txs-state]
-  (let [page (or (get-in target-node [:data :block/page])
-                 {:db/id (get-in target-node [:data :db/id])}) ; or page block
-
-        page-format (:block/format (db/entity (or (:db/id page) page)))]
-    (set-nodes-page-aux node page page-format txs-state)))
-
-(defn move-subtree
-  "Move subtree to a destination position in the relation tree.
+;;; ### insert-blocks, delete-blocks, move-blocks
+
+(defn- insert-blocks-aux
+  [blocks target-block {:keys [sibling? replace-empty-target? keep-uuid? move? outliner-op]}]
+  (let [block-uuids (map :block/uuid blocks)
+        ids (set (map :db/id blocks))
+        uuids (zipmap block-uuids
+                      (if keep-uuid?
+                        block-uuids
+                        (repeatedly random-uuid)))
+        uuids (if replace-empty-target?
+                (assoc uuids (:block/uuid (first blocks)) (:block/uuid target-block))
+                uuids)
+        id->new-uuid (->> (map (fn [block] (when-let [id (:db/id block)]
+                                             [id (get uuids (:block/uuid block))])) blocks)
+                          (into {}))
+        target-page (or (:db/id (:block/page target-block))
+                        ;; target block is a page itself
+                        (:db/id target-block))
+        get-new-id (fn [block lookup]
+                     (cond
+                       (or (map? lookup) (vector? lookup))
+                       (when-let [uuid (if (and (vector? lookup) (= (first lookup) :block/uuid))
+                                         (get uuids (last lookup))
+                                         (get id->new-uuid (:db/id lookup)))]
+                         [:block/uuid uuid])
+
+                       (integer? lookup)
+                       lookup
+
+                       :else
+                       (throw (js/Error. (str "[insert-blocks] illegal lookup: " lookup ", block: " block)))))
+        indent-outdent? (= outliner-op :indent-outdent-blocks)]
+    (map-indexed (fn [idx {:block/keys [parent left] :as block}]
+                   (when-let [uuid (get uuids (:block/uuid block))]
+                     (let [top-level? (= (:block/level block) 1)
+                           outdented-block? (and indent-outdent?
+                                                 top-level?
+                                                 (not= (:block/parent block) (:block/parent target-block)))
+                           prev-hop (if outdented-block? (find-outdented-block-prev-hop block blocks) nil)
+                           left-exists-in-blocks? (contains? ids (:db/id (:block/left block)))
+                           parent (compute-block-parent block parent target-block prev-hop top-level? sibling? get-new-id)
+                           left (compute-block-left blocks block left target-block prev-hop idx replace-empty-target? left-exists-in-blocks? get-new-id)]
+                       (cond->
+                         (merge block {:block/uuid uuid
+                                       :block/page target-page
+                                       :block/parent parent
+                                       :block/left left})
+                         ;; We'll keep the original `:db/id` if it's a move operation,
+                         ;; e.g. drag and drop shouldn't change the ids.
+                         (not move?)
+                         (dissoc :db/id)))))
+                 blocks)))
+
+(defn insert-blocks
+  "Insert blocks as children (or siblings) of target-node.
   Args:
-    root: root of subtree
-    target-node: the destination
-    sibling?: as sibling of the target-node or child"
-  [root target-node sibling?]
-  {:pre [(every? tree/satisfied-inode? [root target-node])
-         (boolean? sibling?)]}
-  (if-let [target-node-id (tree/-get-id target-node)]
-    (when-not (and
-               (or (and sibling?
-                        (= (tree/-get-left-id root) target-node-id)
-                        (not= (tree/-get-parent-id root) target-node-id))
-                   (and (not sibling?)
-                        (= (tree/-get-left-id root) target-node-id)
-                        (= (tree/-get-parent-id root) target-node-id)))
-               (= target-node-id (tree/-get-id root)))
-      (let [root-page (:db/id (:block/page (:data root)))
-            target-page (:db/id (:block/page (:data target-node)))
-            not-same-page? (not= root-page target-page)
-            opts (cond-> {:outliner-op :move-subtree
-                          :move-blocks [(:db/id (get-data root))]
-                          :target (:db/id (get-data target-node))}
-                   not-same-page?
-                   (assoc :from-page root-page
-                          :target-page target-page))]
-        (ds/auto-transact!
-         [txs-state (ds/new-outliner-txs-state)] opts
-         (let [left-node-id (tree/-get-left-id root)
-               right-node (tree/-get-right root)]
-           (when (tree/satisfied-inode? right-node)
-             (let [new-right-node (tree/-set-left-id right-node left-node-id)]
-               (tree/-save new-right-node txs-state)))
-           (let [new-root (first (if sibling?
-                                   (insert-node-as-sibling txs-state root target-node)
-                                   (insert-node-as-first-child txs-state root target-node)))]
-             (when (not= root-page target-page)
-               (set-nodes-page new-root target-node txs-state)))))))
-    (js/console.trace)))
+    `blocks`: blocks should be sorted already.
+    `target-block`: where `blocks` will be inserted.
+    Options:
+      `sibling?`: as siblings (true) or children (false).
+      `keep-uuid?`: whether to replace `:block/uuid` from the parameter `blocks`.
+                    For example, if `blocks` are from internal copy, the uuids
+                    need to be changed, but there's no need for drag & drop.
+      `outliner-op`: what's the current outliner operation.
+      `replace-empty-target?`: If the `target-block` is an empty block, whether
+                               to replace it, it defaults to be `false`.
+    ``"
+  [blocks target-block {:keys [sibling? keep-uuid? outliner-op replace-empty-target?]}]
+  {:pre [(seq blocks)
+         (s/valid? ::block-map-or-entity target-block)]}
+  (let [target-block' (db/pull (:db/id target-block))
+        _ (assert (some? target-block') (str "Invalid target: " target-block))
+        sibling? (if (page-block? target-block') false sibling?)
+        move? (contains? #{:move-blocks :move-blocks-up-down :indent-outdent-blocks} outliner-op)
+        keep-uuid? (if move? true keep-uuid?)
+        replace-empty-target? (if (some? replace-empty-target?)
+                                replace-empty-target?
+                                (and sibling?
+                                     (string/blank? (:block/content target-block'))
+                                     (> (count blocks) 1)
+                                     (not move?)))
+        blocks' (blocks-with-level blocks)
+        insert-opts {:sibling? sibling?
+                     :replace-empty-target? replace-empty-target?
+                     :keep-uuid? keep-uuid?
+                     :move? move?
+                     :outliner-op outliner-op}
+        tx (insert-blocks-aux blocks' target-block' insert-opts)]
+    (if (some (fn [b] (or (nil? (:block/parent b)) (nil? (:block/left b)))) tx)
+      (do
+        (state/pub-event! [:instrument {:type :outliner/invalid-structure
+                                        :payload {:data (mapv #(dissoc % :block/content) tx)}}])
+        (throw (ex-info "Invalid outliner data"
+                        {:opts insert-opts
+                         :tx (vec tx)
+                         :blocks (vec blocks)
+                         :target-block target-block'})))
+      (let [uuids-tx (->> (map :block/uuid tx)
+                          (remove nil?)
+                          (map (fn [uuid] {:block/uuid uuid})))
+            tx (if move?
+                 tx
+                 (assign-temp-id tx replace-empty-target? target-block'))
+            target-node (block target-block')
+            next (if sibling?
+                   (tree/-get-right target-node)
+                   (tree/-get-down target-node))
+            next-tx (when (and next (not (contains? (set (map :db/id blocks)) (:db/id (:data next)))))
+                      (when-let [left (last (filter (fn [b] (= 1 (:block/level b))) tx))]
+                        [{:block/uuid (tree/-get-id next)
+                          :block/left (:db/id left)}]))
+            full-tx (util/concat-without-nil uuids-tx tx next-tx)]
+        (when (and replace-empty-target? (state/editing?))
+          (state/set-edit-content! (state/get-edit-input-id) (:block/content (first blocks))))
+        {:tx-data full-tx
+         :blocks tx}))))
+
+(defn- delete-block
+  "Delete block from the tree."
+  [txs-state block' children?]
+  (let [node (block block')
+        right-node (tree/-get-right node)]
+    (tree/-del node txs-state children?)
+    (when (tree/satisfied-inode? right-node)
+      (let [left-node (tree/-get-left node)
+            new-right-node (tree/-set-left-id right-node (tree/-get-id left-node))]
+        (tree/-save new-right-node txs-state)))
+    @txs-state))
+
+(defn delete-blocks
+  "Delete blocks from the tree.
+   Args:
+    `children?`: whether to replace `blocks'` children too. "
+  [blocks {:keys [children?]
+           :or {children? true}}]
+  [:pre [(seq blocks)]]
+  (let [txs-state (ds/new-outliner-txs-state)
+        block-ids (map (fn [b] [:block/uuid (:block/uuid b)]) blocks)
+        start-block (first blocks)
+        end-block (last (get-top-level-blocks blocks))
+        start-node (block start-block)
+        end-node (block end-block)
+        end-node-parents (->>
+                          (db/get-block-parents
+                           (state/get-current-repo)
+                           (tree/-get-id end-node)
+                           1000)
+                          (map :block/uuid)
+                          (set))
+        self-block? (contains? end-node-parents (tree/-get-id start-node))]
+    (if (or
+         (= 1 (count blocks))
+         (= start-node end-node)
+         self-block?)
+      (delete-block txs-state start-block children?)
+      (let [sibling? (= (tree/-get-parent-id start-node)
+                        (tree/-get-parent-id end-node))
+            right-node (tree/-get-right end-node)]
+        (when (tree/satisfied-inode? right-node)
+          (let [left-node-id (if sibling?
+                               (tree/-get-id (tree/-get-left start-node))
+                               (let [end-node-left-nodes (get-left-nodes end-node (count block-ids))
+                                     parents (->>
+                                              (db/get-block-parents
+                                               (state/get-current-repo)
+                                               (tree/-get-id start-node)
+                                               1000)
+                                              (map :block/uuid)
+                                              (set))
+                                     result (first (set/intersection (set end-node-left-nodes) parents))]
+                                 (when-not result
+                                   (util/pprint {:parents parents
+                                                 :end-node-left-nodes end-node-left-nodes}))
+                                 result))]
+            (assert left-node-id "Can't find the left-node-id")
+            (let [new-right-node (tree/-set-left-id right-node left-node-id)]
+              (tree/-save new-right-node txs-state))))
+        (doseq [id block-ids]
+          (let [node (block (db/pull id))]
+            (tree/-del node txs-state true)))))
+    {:tx-data @txs-state}))
+
+(defn- build-move-blocks-next-tx
+  [blocks]
+  (let [id->blocks (zipmap (map :db/id blocks) blocks)
+        top-level-blocks (get-top-level-blocks blocks)
+        top-level-blocks-ids (set (map :db/id top-level-blocks))
+        right-block (get-right-sibling (:db/id (last top-level-blocks)))]
+    (when (and right-block
+               (not (contains? top-level-blocks-ids (:db/id right-block))))
+      {:db/id (:db/id right-block)
+       :block/left (loop [block (:block/left right-block)]
+                     (if (contains? top-level-blocks-ids (:db/id block))
+                       (recur (:block/left (get id->blocks (:db/id block))))
+                       (:db/id block)))})))
+
+(defn move-blocks
+  "Move `blocks` to `target-block` as siblings or children."
+  [blocks target-block {:keys [sibling? outliner-op]}]
+  [:pre [(seq blocks)
+         (s/valid? ::block-map-or-entity target-block)]]
+  (when (not (contains? (set (map :db/id blocks)) (:db/id target-block)))
+    (let [parents (->> (db/get-block-parents (state/get-current-repo) (:block/uuid target-block))
+                       (map :db/id)
+                       (set))
+          move-parents-to-child? (some parents (map :db/id blocks))]
+      (when-not move-parents-to-child?
+        (let [blocks (get-top-level-blocks blocks)
+              first-block (first blocks)
+              {:keys [tx-data]} (insert-blocks blocks target-block {:sibling? sibling?
+                                                                    :outliner-op (or outliner-op :move-blocks)})]
+          (when (seq tx-data)
+            (let [first-block-page (:db/id (:block/page first-block))
+                  target-page (:db/id (:block/page target-block))
+                  not-same-page? (not= first-block-page target-page)
+                  move-blocks-next-tx [(build-move-blocks-next-tx blocks)]
+                  children-page-tx (when not-same-page?
+                                     (let [children-ids (mapcat #(db/get-block-children-ids (state/get-current-repo) (:block/uuid %)) blocks)]
+                                       (map (fn [uuid] {:block/uuid uuid
+                                                        :block/page target-page}) children-ids)))
+                  full-tx (util/concat-without-nil tx-data move-blocks-next-tx children-page-tx)
+                  tx-meta (cond-> {:move-blocks (mapv :db/id blocks)
+                                   :target (:db/id target-block)}
+                            not-same-page?
+                            (assoc :from-page first-block-page
+                                   :target-page target-page))]
+              {:tx-data full-tx
+               :tx-meta tx-meta})))))))
+
+(defn move-blocks-up-down
+  "Move blocks up/down."
+  [blocks up?]
+  {:pre [(seq blocks) (boolean? up?)]}
+  (let [first-block (db/entity (:db/id (first blocks)))
+        first-block-parent (:block/parent first-block)
+        left-left (:block/left (:block/left first-block))
+        top-level-blocks (get-top-level-blocks blocks)
+        last-top-block (last top-level-blocks)
+        last-top-block-parent (:block/parent last-top-block)
+        right (get-right-sibling (:db/id last-top-block))
+        opts {:outliner-op :move-blocks-up-down}]
+    (cond
+      (and up? left-left)
+      (cond
+        (= (:block/parent left-left) first-block-parent)
+        (move-blocks blocks left-left (merge opts {:sibling? true}))
+
+        (= (:db/id left-left) (:db/id first-block-parent))
+        (move-blocks blocks left-left (merge opts {:sibling? false}))
+
+        (= (:block/left first-block) first-block-parent)
+        (let [target-children (:block/_parent left-left)]
+          (if (seq target-children)
+            (when (= (:block/parent left-left) (:block/parent first-block-parent))
+              (let [target-block (last (db-model/sort-by-left target-children left-left))]
+                (move-blocks blocks target-block (merge opts {:sibling? true}))))
+            (move-blocks blocks left-left (merge opts {:sibling? false}))))
+
+        :else
+        nil)
+
+      (not up?)
+      (if right
+        (move-blocks blocks right (merge opts {:sibling? true}))
+        (when last-top-block-parent
+          (when-let [parent-right (get-right-sibling (:db/id last-top-block-parent))]
+            (move-blocks blocks parent-right (merge opts {:sibling? false})))))
+
+      :else
+      nil)))
+
+(defn indent-outdent-blocks
+  "Indent or outdent `blocks`."
+  [blocks indent?]
+  {:pre [(seq blocks) (boolean? indent?)]}
+  (let [first-block (db/entity (:db/id (first blocks)))
+        left (db/entity (:db/id (:block/left first-block)))
+        parent (:block/parent first-block)
+        db (db/get-db)
+        top-level-blocks (get-top-level-blocks blocks)
+        concat-tx-fn (fn [& results]
+                       {:tx-data (->> (map :tx-data results)
+                                      (apply util/concat-without-nil))
+                        :tx-meta (:tx-meta (first results))})
+        opts {:outliner-op :indent-outdent-blocks}]
+    (if indent?
+      (when (and left (not (page-first-child? first-block)))
+        (let [last-direct-child-id (db-model/get-block-last-direct-child db (:db/id left) false)
+              blocks' (drop-while (fn [b]
+                                    (= (:db/id (:block/parent b))
+                                       (:db/id left)))
+                                  top-level-blocks)]
+          (when (seq blocks')
+            (if last-direct-child-id
+              (let [last-direct-child (db/entity last-direct-child-id)
+                    result (move-blocks blocks' last-direct-child (merge opts {:sibling? true}))
+                    ;; expand `left` if it's collapsed
+                    collapsed-tx (when (:block/collapsed? left)
+                                   {:tx-data [{:db/id (:db/id left)
+                                               :block/collapsed? false}]})]
+                (concat-tx-fn result collapsed-tx))
+              (move-blocks blocks' left (merge opts {:sibling? false}))))))
+      (when (and parent (not (page-block? (db/entity (:db/id parent)))))
+        (let [blocks' (take-while (fn [b]
+                                    (not= (:db/id (:block/parent b))
+                                          (:db/id (:block/parent parent))))
+                                  top-level-blocks)
+              result (move-blocks blocks' parent (merge opts {:sibling? true}))]
+          (if (state/logical-outdenting?)
+            result
+            ;; direct outdenting (default behavior)
+            (let [last-top-block (db/pull (:db/id (last blocks')))
+                  right-siblings (->> (get-right-siblings (block last-top-block))
+                                      (map :data))]
+              (if (seq right-siblings)
+                (let [result2 (if-let [last-direct-child-id (db-model/get-block-last-direct-child db (:db/id last-top-block) false)]
+                                (move-blocks right-siblings (db/entity last-direct-child-id) (merge opts {:sibling? true}))
+                                (move-blocks right-siblings last-top-block (merge opts {:sibling? false})))]
+                  (concat-tx-fn result result2))
+                result))))))))
+
+;;; ### write-operations have side-effects (do transactions) ;;;;;;;;;;;;;;;;
+
+(def ^:private ^:dynamic *transaction-data*
+  "Stores transaction-data that are generated by one or more write-operations,
+  see also `frontend.modules.outliner.transaction/save-transactions`"
+  nil)
+
+(defn- op-transact!
+  [fn-var & args]
+  {:pre [(var? fn-var)]}
+  (when (nil? *transaction-data*)
+    (throw (js/Error. (str (:name (meta fn-var)) " is not used in (save-transactions ...)"))))
+  (let [result (apply @fn-var args)]
+    (conj! *transaction-data* (select-keys result [:tx-data :tx-meta]))
+    result))
+
+(defn save-block!
+  [block]
+  (op-transact! #'save-block block))
 
-(defn get-right-node
-  [node]
-  {:pre [(tree/satisfied-inode? node)]}
-  (tree/-get-right node))
+(defn insert-blocks!
+  [blocks target-block opts]
+  (op-transact! #'insert-blocks blocks target-block opts))
+
+(defn delete-blocks!
+  [blocks opts]
+  (op-transact! #'delete-blocks blocks opts))
+
+(defn move-blocks!
+  [blocks target-block sibling?]
+  (op-transact! #'move-blocks blocks target-block {:sibling? sibling?}))
+
+(defn move-blocks-up-down!
+  [blocks up?]
+  (op-transact! #'move-blocks-up-down blocks up?))
+
+(defn indent-outdent-blocks!
+  [blocks indent?]
+  (op-transact! #'indent-outdent-blocks blocks indent?))

+ 27 - 50
src/main/frontend/modules/outliner/datascript.cljc

@@ -21,13 +21,6 @@
        (instance? cljs.core/Atom state)
        (coll? @state))))
 
-#?(:cljs
-   (defn add-txs
-     [state txs]
-     (assert (outliner-txs-state? state)
-       "db should be satisfied outliner-tx-state?")
-     (swap! state into txs)))
-
 #?(:cljs
    (defn after-transact-pipelines
      [{:keys [_db-before _db-after _tx-data _tempids _tx-meta] :as tx-report}]
@@ -51,48 +44,32 @@
      (let [txs (remove-nil-from-transaction txs)
            txs (map (fn [m] (if (map? m)
                               (dissoc m
-                                      :block/children :block/meta :block/top? :block/bottom?
-                                      :block/title :block/body :block/level)
+                                      :block/children :block/meta :block/top? :block/bottom? :block/anchor
+                                      :block/title :block/body :block/level :block/container)
                               m)) txs)]
-       ;; (util/pprint txs)
+       (util/pprint txs)
        (when (and (seq txs)
-                 (not (:skip-transact? opts)))
-        (try
-          (let [conn (conn/get-conn false)
-                editor-cursor (state/get-current-edit-block-and-position)
-                meta (merge opts {:editor-cursor editor-cursor})
-                rs (d/transact! conn txs meta)]
-            (when true                 ; TODO: add debug flag
-              (let [eids (distinct (mapv first (:tx-data rs)))
-                    left&parent-list (->>
-                                      (d/q '[:find ?e ?l ?p
-                                             :in $ [?e ...]
-                                             :where
-                                             [?e :block/left ?l]
-                                             [?e :block/parent ?p]] @conn eids)
-                                      (vec)
-                                      (map next))]
-                (assert (= (count left&parent-list) (count (distinct left&parent-list))) eids)))
-            (when-not config/test?
-              (after-transact-pipelines rs))
-            rs)
-          (catch js/Error e
-            (log/error :exception e)
-            (throw e)))))))
-
-#?(:clj
-   (defmacro auto-transact!
-     "Copy from with-open.
-     Automatically transact! after executing the body."
-     [bindings opts & body]
-     (#'core/assert-args
-       (vector? bindings) "a vector for its binding"
-       (even? (count bindings)) "an even number of forms in binding vector")
-     (cond
-       (= (count bindings) 0) `(do ~@body)
-       (symbol? (bindings 0)) `(let ~(subvec bindings 0 2)
-                                 (try
-                                   (auto-transact! ~(subvec bindings 2) ~opts ~@body)
-                                   (transact! (deref ~(bindings 0)) ~opts)))
-       :else (throw (IllegalArgumentException.
-                      "with-db only allows Symbols in bindings")))))
+                  (not (:skip-transact? opts)))
+         (try
+           (let [repo (get opts :repo (state/get-current-repo))
+                 conn (conn/get-db repo false)
+                 editor-cursor (state/get-current-edit-block-and-position)
+                 meta (merge opts {:editor-cursor editor-cursor})
+                 rs (d/transact! conn txs meta)]
+             (when true                 ; TODO: add debug flag
+               (let [eids (distinct (mapv first (:tx-data rs)))
+                     left&parent-list (->>
+                                       (d/q '[:find ?e ?l ?p
+                                              :in $ [?e ...]
+                                              :where
+                                              [?e :block/left ?l]
+                                              [?e :block/parent ?p]] @conn eids)
+                                       (vec)
+                                       (map next))]
+                 (assert (= (count left&parent-list) (count (distinct left&parent-list))) eids)))
+             (when-not config/test?
+               (after-transact-pipelines rs))
+             rs)
+           (catch js/Error e
+             (log/error :exception e)
+             (throw e)))))))

+ 35 - 0
src/main/frontend/modules/outliner/transaction.cljc

@@ -0,0 +1,35 @@
+(ns frontend.modules.outliner.transaction
+  #?(:cljs (:require-macros [frontend.modules.outliner.transaction])))
+
+(defmacro transact!
+  "Batch all the transactions in `body` to a single transaction, Support nested transact! calls.
+  Currently there are no options, it'll execute body and collect all transaction data generated by body.
+  `Args`:
+    `opts`: Every key is optional, opts except `additional-tx` will be transacted as `tx-meta`.
+            {:graph \"Which graph will be transacted to\"
+             :outliner-op \"For example, :save-block, :insert-blocks, etc. \"
+             :additional-tx \"Additional tx data that can be bundled together
+                              with the body in this macro.\"}
+  `Example`:
+  (transact! {:graph \"test\"}
+    (insert-blocks! ...)
+    ;; do something
+    (move-blocks! ...)
+    (delete-blocks! ...))"
+  [opts & body]
+  (assert (map? opts))
+  `(if (some? frontend.modules.outliner.core/*transaction-data*)
+     (do ~@body)
+     (binding [frontend.modules.outliner.core/*transaction-data* (transient [])]
+       ~@body
+       (let [r# (persistent! frontend.modules.outliner.core/*transaction-data*)
+             tx# (mapcat :tx-data r#)
+             ;; FIXME: should we merge all the tx-meta?
+             tx-meta# (first (map :tx-meta r#))
+             all-tx# (concat tx# (:additional-tx ~opts))
+             opts# (merge (dissoc ~opts :additional-tx) tx-meta#)]
+         (when (seq all-tx#)
+           (let [result# (frontend.modules.outliner.datascript/transact! all-tx# opts#)]
+             {:tx-report result#
+              :tx-data all-tx#
+              :tx-meta tx-meta#}))))))

+ 2 - 2
src/main/frontend/modules/outliner/utils.cljs

@@ -30,12 +30,12 @@
     (and
       (vector? id)
       (= (first id) :block/name))
-    (let [conn (conn/get-conn false)]
+    (let [conn (conn/get-db false)]
       (-> (db-outliner/get-by-id conn id)
         (:block/uuid)))
 
     (or (e/entity? id) (map? id))
-    (let [conn (conn/get-conn false)]
+    (let [conn (conn/get-db false)]
       (-> (db-outliner/get-by-id conn (:db/id id))
         (:block/uuid)))
 

+ 26 - 13
src/main/frontend/state.cljs

@@ -126,13 +126,14 @@
 
      ;; for audio record
      :editor/record-status                  "NONE"
-     
+
      :db/last-transact-time                 {}
      ;; whether database is persisted
      :db/persisted?                         {}
      :cursor-range                          nil
 
      :selection/mode                        false
+     ;; Warning: blocks order is determined when setting this attribute
      :selection/blocks                      []
      :selection/start-block                 nil
      ;; either :up or :down, defaults to down
@@ -185,7 +186,7 @@
      :graph/syncing?                        false
 
      ;; copied blocks
-     :copy/blocks                           {:copy/content nil :copy/block-tree nil}
+     :copy/blocks                           {:copy/content nil :copy/block-ids nil}
 
      :copy/export-block-text-indent-style   (or (storage/get :copy/export-block-text-indent-style)
                                                 "dashes")
@@ -682,10 +683,11 @@
    (set-selection-blocks! blocks :down))
   ([blocks direction]
    (when (seq blocks)
-     (swap! state assoc
-            :selection/mode true
-            :selection/blocks blocks
-            :selection/direction direction))))
+     (let [blocks (util/sort-by-height blocks)]
+       (swap! state assoc
+             :selection/mode true
+             :selection/blocks blocks
+             :selection/direction direction)))))
 
 (defn into-selection-mode!
   []
@@ -700,7 +702,14 @@
 
 (defn get-selection-blocks
   []
-  (util/sort-by-height (:selection/blocks @state)))
+  (:selection/blocks @state))
+
+(defn get-selection-block-ids
+  []
+  (->> (sub :selection/blocks)
+       (keep #(when-let [id (dom/attr % "blockid")]
+                (uuid id)))
+       (distinct)))
 
 (defn in-selection-mode?
   []
@@ -716,7 +725,8 @@
   (dom/add-class! block "selected noselect")
   (swap! state assoc
          :selection/mode true
-         :selection/blocks (conj (vec (:selection/blocks @state)) block)
+         :selection/blocks (-> (conj (vec (:selection/blocks @state)) block)
+                               (util/sort-by-height))
          :selection/direction direction))
 
 (defn drop-last-selection-block!
@@ -1466,7 +1476,13 @@
 
 (defn set-copied-blocks
   [content ids]
-  (set-state! :copy/blocks {:copy/content content :copy/block-tree ids}))
+  (set-state! :copy/blocks {:copy/content content
+                            :copy/block-ids ids
+                            :copy/full-blocks nil}))
+
+(defn set-copied-full-blocks!
+  [blocks]
+  (set-state! [:copy/blocks :copy/full-blocks] blocks))
 
 (defn get-export-block-text-indent-style []
   (:copy/export-block-text-indent-style @state))
@@ -1532,11 +1548,8 @@
   ([blocks]
    (exit-editing-and-set-selected-blocks! blocks :down))
   ([blocks direction]
-   (util/select-unhighlight! (dom/by-class "selected"))
-   (clear-selection!)
    (clear-edit!)
-   (set-selection-blocks! blocks direction)
-   (util/select-highlight! blocks)))
+   (set-selection-blocks! blocks direction)))
 
 (defn remove-watch-state [key]
   (remove-watch state key))

+ 0 - 31
src/main/frontend/util.cljc

@@ -748,14 +748,6 @@
            :up
            :down)))))
 
-#?(:cljs
-   (defn rec-get-block-node
-     [node]
-     (if (and node (d/has-class? node "ls-block"))
-       node
-       (and node
-            (rec-get-block-node (gobj/get node "parentNode"))))))
-
 #?(:cljs
    (defn rec-get-blocks-container
      [node]
@@ -782,29 +774,6 @@
      (->> blocks
           (remove (fn [b] (= "true" (d/attr b "data-embed")))))))
 
-;; Take the idea from https://stackoverflow.com/questions/4220478/get-all-dom-block-elements-for-selected-texts.
-;; FIXME: Note that it might not works for IE.
-#?(:cljs
-   (defn get-selected-nodes
-     [class-name]
-     (try
-       (when (gobj/get js/window "getSelection")
-         (let [selection (js/window.getSelection)
-               range (.getRangeAt selection 0)
-               container (-> (gobj/get range "commonAncestorContainer")
-                             (rec-get-blocks-container))
-               start-node (gobj/get range "startContainer")
-               container-nodes (array-seq (selection/getSelectedNodes container start-node))]
-           (map
-            (fn [node]
-              (if (or (= 3 (gobj/get node "nodeType"))
-                      (not (d/has-class? node class-name))) ;textnode
-                (rec-get-block-node node)
-                node))
-            container-nodes)))
-       (catch js/Error _e
-         nil))))
-
 #?(:cljs
    (defn get-selected-text
      []

+ 7 - 5
src/main/frontend/util/page_property.cljs

@@ -3,6 +3,7 @@
             [frontend.db :as db]
             [frontend.modules.outliner.core :as outliner-core]
             [frontend.modules.outliner.file :as outliner-file]
+            [frontend.modules.outliner.transaction :as outliner-tx]
             [frontend.state :as state]
             [frontend.util :as util]))
 
@@ -77,9 +78,10 @@
                                       (str (name key) ":: " value))
                      :block/format format
                      :block/properties {key value}
-                     :block/pre-block? true}]
-          (outliner-core/insert-node (outliner-core/block block)
-                                     (outliner-core/block page)
-                                     false)
-          (db/transact! [(assoc page-id :block/properties {key value})])))
+                     :block/pre-block? true}
+              page-properties-tx [(assoc page-id :block/properties {key value})]]
+          (outliner-tx/transact!
+            {:outliner-op :insert-blocks
+             :additional-tx page-properties-tx}
+            (outliner-core/insert-blocks! block page {:sibling? false}))))
       (outliner-file/sync-to-file page-id))))

+ 9 - 9
src/main/logseq/api.cljs

@@ -359,7 +359,7 @@
 (def ^:export get_selected_blocks
   (fn []
     (when-let [blocks (and (state/in-selection-mode?)
-                           (seq (:selection/blocks @state/state)))]
+                           (seq (state/get-selection-blocks)))]
       (let [blocks (->> blocks
                         (map (fn [^js el] (some-> (.getAttribute el "blockid")
                                                   (db-model/query-block-by-uuid)))))]
@@ -441,7 +441,7 @@
       (when-let [bb (bean/->clj batch-blocks)]
         (let [bb (if-not (vector? bb) (vector bb) bb)
               {:keys [sibling]} (bean/->clj opts)
-              _ (editor-handler/paste-block-tree-after-target
+              _ (editor-handler/insert-block-tree-after-target
                   (:db/id block) sibling bb (:block/format block))]
           nil)))))
 
@@ -475,9 +475,9 @@
 
                     :else
                     nil)
-          src-block-uuid (db-model/query-block-by-uuid (medley/uuid src-block-uuid))
-          target-block-uuid (db-model/query-block-by-uuid (medley/uuid target-block-uuid))]
-      (editor-dnd-handler/move-block nil src-block-uuid target-block-uuid move-to) nil)))
+          src-block (db-model/query-block-by-uuid (medley/uuid src-block-uuid))
+          target-block (db-model/query-block-by-uuid (medley/uuid target-block-uuid))]
+      (editor-dnd-handler/move-blocks nil [src-block] target-block move-to) nil)))
 
 (def ^:export get_block
   (fn [id-or-uuid ^js opts]
@@ -501,7 +501,7 @@
 (def ^:export get_current_block
   (fn [^js opts]
     (let [block (state/get-edit-block)
-          block (or block (some-> (first (:selection/blocks @state/state))
+          block (or block (some-> (first (state/get-selection-blocks))
                             (.getAttribute "blockid")
                             (db-model/get-block-by-uuid)))
           block (or block (state/get-last-edit-block))]
@@ -583,9 +583,9 @@
 (defn ^:export datascript_query
   [query & inputs]
   (when-let [repo (state/get-current-repo)]
-    (when-let [conn (db/get-conn repo)]
+    (when-let [db (db/get-db repo)]
       (let [query (cljs.reader/read-string query)
-            result (apply d/q query conn inputs)]
+            result (apply d/q query db inputs)]
         (clj->js result)))))
 
 (def ^:export custom_query db/custom-query)
@@ -593,7 +593,7 @@
 (defn ^:export download_graph_db
   []
   (when-let [repo (state/get-current-repo)]
-    (when-let [db (db/get-conn repo)]
+    (when-let [db (db/get-db repo)]
       (let [db-str (if db (db/db->string db) "")
             data-str (str "data:text/edn;charset=utf-8," (js/encodeURIComponent db-str))]
         (when-let [anchor (gdom/getElement "download")]

+ 1 - 5
src/test/frontend/core_test.cljs

@@ -6,8 +6,4 @@
   []
   (->
     (state/get-current-repo)
-    (conn/get-conn false)))
-
-
-
-
+    (conn/get-db false)))

+ 2 - 2
src/test/frontend/db/query_dsl_test.cljs

@@ -553,7 +553,7 @@ last-modified-at:: 1609084800002"}]]
    '[:find (pull ?b [*])
      :where
      [?b :block/name]]
-   (frontend.db/get-conn test-db)))
+   (frontend.db/get-db test-db)))
 
  ;; (or (priority a) (not (priority a)))
  ;; FIXME: Error: Insufficient bindings: #{?priority} not bound in [(contains? #{"A"} ?priority)]
@@ -565,4 +565,4 @@ last-modified-at:: 1609084800002"}]]
      (or (and [?b :block/priority ?priority] [(contains? #{"A"} ?priority)])
          (not [?b :block/priority #{"A"}]
               [(contains? #{"A"} ?priority)]))]
-   (frontend.db/get-conn test-db))))
+   (frontend.db/get-db test-db))))

+ 406 - 215
src/test/frontend/modules/outliner/core_test.cljs

@@ -1,130 +1,101 @@
 (ns frontend.modules.outliner.core-test
   (:require [cljs.test :refer [deftest is use-fixtures testing] :as test]
+            [clojure.test.check.generators :as gen]
             [frontend.test.fixtures :as fixtures]
             [frontend.modules.outliner.core :as outliner-core]
-            [frontend.modules.outliner.datascript :as outliner-ds]
             [frontend.modules.outliner.tree :as tree]
-            [frontend.modules.outliner.utils :as outliner-u]))
+            [frontend.modules.outliner.transaction :as outliner-tx]
+            [frontend.db :as db]
+            [frontend.db.model :as db-model]
+            [clojure.walk :as walk]
+            [frontend.format.block :as block]
+            [datascript.core :as d]
+            [frontend.test.helper :as helper]))
+
+(def test-db helper/test-db)
 
 (use-fixtures :each
   fixtures/load-test-env
   fixtures/react-components
   fixtures/reset-db)
 
-(defn build-block
+(defn get-block
   ([id]
-   (build-block id nil nil))
-  ([id parent-id left-id & [m]]
-   (let [m (->> (merge m {:block/uuid id
-                          :block/parent
-                          (outliner-u/->block-lookup-ref parent-id)
-                          :block/left
-                          (outliner-u/->block-lookup-ref left-id)
-                          :block/content (str id)})
-                (remove #(nil? (val %)))
-                (into {}))]
-     (outliner-core/block m))))
-
-(defrecord TreeNode [id children])
+   (get-block id false))
+  ([id node?]
+   (cond-> (db/pull test-db '[*] [:block/uuid id])
+     node?
+     outliner-core/block)))
 
 (defn build-node-tree
-  [[id children :as _tree]]
-  (let [children (mapv build-node-tree children)]
-    (->TreeNode id children)))
-
-(defn build-db-records
-  "build RDS record from memory node struct."
-  [tree-record]
-  (outliner-ds/auto-transact!
-   [state (outliner-ds/new-outliner-txs-state)] nil
-   (letfn [(build [node queue]
-             (let [{:keys [id left parent]} node
-                   block (build-block id parent left)
-                   left (atom (:id node))
-                   children (map (fn [c]
-                                   (let [node (assoc c :left @left :parent (:id node))]
-                                     (swap! left (constantly (:id c)))
-                                     node))
-                                 (:children node))
-                   queue (concat queue children)]
-               (tree/-save block state)
-               (when (seq queue)
-                 (build (first queue) (rest queue)))))]
-     (let [root (assoc tree-record :left "1" :parent "1")]
-       (tree/-save (build-block "1") state)
-       (build root '())))))
-
-
-(def tree [1 [[2 [[3 [[4]
-                      [5]]]
-                  [6 [[7 [[8]]]]]
-                  [9 [[10]
-                      [11]]]]]
-              [12 [[13]
-                   [14]
-                   [15]]]
-              [16 [[17]]]]])
-
-(def node-tree (build-node-tree tree))
+  [col]
+  (let [blocks (->> col
+                    (walk/postwalk
+                     (fn [f]
+                       (if (and (vector? f)
+                                (= 2 (count f))
+                                (integer? (first f))
+                                (vector? (second f)))
+                         {:block/uuid (first f)
+                          :block/children (let [v (second f)]
+                                            (cond
+                                              (sequential? v)
+                                              (mapv
+                                               (fn [v]
+                                                 (if (integer? v)
+                                                   [v]
+                                                   v))
+                                               v)
 
-(comment
-  (build-db-records node-tree)
-  (dotimes [i 18]
-    (when-not (= i 0)
-      (prn (d/pull @(core-test/get-current-conn) '[*] [:block/uuid i])))))
+                                              :else
+                                              [[v]]))}
+                         f)))
 
-(deftest test-insert-node-as-first-child
-  (testing "
-  Insert a node between 6 and 9.
-  [1 [[2 [[18]         ;; add
-          [3 [[4]
-              [5]]]
-          [6 [[7 [[8]]]]]
+                    (walk/postwalk
+                     (fn [f]
+                       (if (and (vector? f)
+                                (= 1 (count f))
+                                (integer? (first f)))
+                         {:block/uuid (first f)}
+                         f))))
+        blocks (outliner-core/tree-vec-flatten blocks :block/children)]
+    (map (fn [block] (assoc block
+                            :block/page 1
+                            :block/content 1)) blocks)))
 
-          [9 [[10]
-              [11]]]]]
-      [12 [[13]
-           [14]
-           [15]]]
-      [16 [[17]]]]]
-   "
-    (build-db-records node-tree)
-    (let [new-node (build-block 18 nil nil)
-          parent-node (build-block 2 1 1)]
-      (outliner-ds/auto-transact!
-       [state (outliner-ds/new-outliner-txs-state)] nil
-       (outliner-core/insert-node-as-first-child state new-node parent-node))
-      (let [children-of-2 (->> (build-block 2 1 1)
-                               (tree/-get-children)
-                               (mapv #(-> % :data :block/uuid)))]
-        (is (= [18 3 6 9] children-of-2))))))
-
-(deftest test-insert-node-as-sibling
-  (testing "
-  Insert a node between 6 and 9.
-  [1 [[2 [[3 [[4]
-              [5]]]
-          [6 [[7 [[8]]]]]
-          [18]         ;; add
-          [9 [[10]
-              [11]]]]]
-      [12 [[13]
-           [14]
-           [15]]]
-      [16 [[17]]]]]
-   "
-    (build-db-records node-tree)
-    (let [new-node (build-block 18 nil nil)
-          left-node (build-block 6 2 3)]
-      (outliner-ds/auto-transact!
-       [state (outliner-ds/new-outliner-txs-state)] nil
-       (outliner-core/insert-node-as-sibling state new-node left-node))
-      (let [children-of-2 (->> (build-block 2 1 1)
-                               (tree/-get-children)
-                               (mapv #(-> % :data :block/uuid)))]
-        (is (= [3 6 18 9] children-of-2))))))
-
-(deftest test-delete-node
+(defn- build-blocks
+  [tree]
+  (block/with-parent-and-left 1 (build-node-tree tree)))
+
+(defn transact-tree!
+  [tree]
+  (db/transact! test-db (concat [{:db/id 1
+                                  :block/uuid 1
+                                  :block/name "Test page"}]
+                                (build-blocks tree))))
+
+(def tree
+  [[22 [[2 [[3 [[4]
+                [5]]]
+            [6 [[7 [[8]]]]]
+            [9 [[10]
+                [11]]]]]
+        [12 [[13]
+             [14]
+             [15]]]
+        [16 [[17]]]]]])
+
+(defn get-blocks-count
+  []
+  (count (d/datoms (db/get-db test-db) :avet :block/uuid)))
+
+(defn get-children
+  [id]
+  (->> (get-block id true)
+       (tree/-get-children)
+       (mapv #(-> % :data :block/uuid))))
+
+(deftest test-delete-block
   (testing "
   Insert a node between 6 and 9.
   [1 [[2 [[3 [[4]
@@ -137,16 +108,13 @@
            [15]]]
       [16 [[17]]]]]
    "
-    (build-db-records node-tree)
-    (let [node (build-block 6 2 3)]
-      (outliner-core/delete-node node true)
-      (let [children-of-2 (->> (build-block 2 1 1)
-                               (tree/-get-children)
-                               (mapv #(-> % :data :block/uuid)))]
-        (is (= [3 9] children-of-2))))))
+    (transact-tree! tree)
+    (let [block (get-block 6)]
+      (outliner-tx/transact! {:graph test-db}
+        (outliner-core/delete-blocks! [block] true))
+      (is (= [3 9] (get-children 2))))))
 
-
-(deftest test-move-subtree-as-sibling
+(deftest test-move-block-as-sibling
   (testing "
   Move 3 between 14 and 15.
   [1 [[2 [[6 [[7 [[8]]]]]
@@ -159,20 +127,14 @@
            [15]]]
       [16 [[17]]]]]
    "
-    (build-db-records node-tree)
-    (let [node (build-block 3 2 2)
-          target-node (build-block 14 12 13)]
-      (outliner-core/move-subtree node target-node true)
-      (let [old-parent's-children (->> (build-block 2 1 1)
-                                       (tree/-get-children)
-                                       (mapv #(-> % :data :block/uuid)))
-            new-parent's-children (->> (build-block 12 1 2)
-                                       (tree/-get-children)
-                                       (mapv #(-> % :data :block/uuid)))]
-        (is (= [6 9] old-parent's-children))
-        (is (= [13 14 3 15] new-parent's-children)))))
-
-  (deftest test-move-subtree-as-first-child
+    (transact-tree! tree)
+    (outliner-tx/transact!
+      {:graph test-db}
+      (outliner-core/move-blocks! [(get-block 3)] (get-block 14) true))
+    (is (= [6 9] (get-children 2)))
+    (is (= [13 14 3 15] (get-children 12))))
+
+  (deftest test-move-block-as-first-child
     (testing "
   Move 3 as first child of 12.
 
@@ -186,21 +148,14 @@
            [15]]]
       [16 [[17]]]]]
    "
-      (build-db-records node-tree)
-      (let [node (build-block 3 2 2)
-            target-node (build-block 12 1 2)]
-        (outliner-core/move-subtree node target-node false)
-        (let [old-parent's-children (->> (build-block 2 1 1)
-                                         (tree/-get-children)
-                                         (mapv #(-> % :data :block/uuid)))
-              new-parent's-children (->> (build-block 12 1 2)
-                                         (tree/-get-children)
-                                         (mapv #(-> % :data :block/uuid)))]
-          (is (= [6 9] old-parent's-children))
-          (is (= [3 13 14 15] new-parent's-children)))))))
-
-
-(deftest test-indent-nodes
+      (transact-tree! tree)
+      (outliner-tx/transact!
+        {:graph test-db}
+        (outliner-core/move-blocks! [(get-block 3)] (get-block 12) false))
+      (is (= [6 9] (get-children 2)))
+      (is (= [3 13 14 15] (get-children 12))))))
+
+(deftest test-indent-blocks
   (testing "
   [1 [[2 [[3
            [[4]
@@ -213,19 +168,16 @@
            [15]]]
       [16 [[17]]]]]
   "
-    (build-db-records node-tree)
-    (let [nodes [(build-block 6 2 3)
-                 (build-block 9 2 6)]]
-      (outliner-core/indent-outdent-nodes nodes true)
-      (let [children-of-3 (->> (build-block 3)
-                               (tree/-get-children)
-                               (mapv #(-> % :data :block/uuid)))]
-        (is (= [4 5 6 9] children-of-3))))))
-
-(deftest test-outdent-nodes
+    (transact-tree! tree)
+    (outliner-tx/transact!
+      {:graph test-db}
+      (outliner-core/indent-outdent-blocks! [(get-block 6) (get-block 9)] true))
+    (is (= [4 5 6 9] (get-children 3)))))
+
+(deftest test-outdent-blocks
   (testing "
   [1 [[2 [[3]
-          [4] ;; outdent 6, 9
+          [4] ;; outdent 4, 5
           [5]
           [6 [[7 [[8]]]]]
           [9 [[10]
@@ -235,19 +187,13 @@
            [15]]]
       [16 [[17]]]]]
   "
-    (build-db-records node-tree)
-    (let [nodes [(build-block 4 3 3)
-                 (build-block 5 3 4)]]
-      (outliner-core/indent-outdent-nodes nodes false)
-      (let [children-of-2 (->> (build-block 2)
-                               (tree/-get-children)
-                               (mapv #(-> % :data :block/uuid)))]
-        (is (= [3 4 5 6 9] children-of-2))))))
-
-(comment
-  (run-test test-outdent-nodes))
+    (transact-tree! tree)
+    (outliner-tx/transact!
+      {:graph test-db}
+      (outliner-core/indent-outdent-blocks! [(get-block 4) (get-block 5)] false))
+    (is (= [3 4 5 6 9] (get-children 2)))))
 
-(deftest test-delete-nodes
+(deftest test-delete-blocks
   (testing "
   [1 [[2 [[3 [[4]
               [5]]]
@@ -260,20 +206,13 @@
            [15]]]
       [16 [[17]]]]]
 "
-    (build-db-records node-tree)
-    (let [start-node (build-block 6 2 3)
-          end-node (build-block 11 9 10)
-          block-ids [7 8 9 10]]
-      (outliner-core/delete-nodes start-node end-node block-ids)
-      (let [children-of-2 (->> (build-block 2)
-                               (tree/-get-children)
-                               (mapv #(-> % :data :block/uuid)))]
-        (is (= [3] children-of-2))))))
+    (transact-tree! tree)
+    (outliner-tx/transact!
+      {:graph test-db}
+      (outliner-core/delete-blocks! [(get-block 6) (get-block 9)] {}))
+    (is (= [3] (get-children 2)))))
 
-(comment
-  (run-test test-delete-nodes))
-
-(deftest test-move-node
+(deftest test-move-blocks-up-down
   (testing "
   [1 [[2 [[3 [[4]
               [5]]]
@@ -285,18 +224,13 @@
            [15]]]
       [16 [[17]]]]]
   "
-    (build-db-records node-tree)
-    (let [node (build-block 9 2 6)]
-      (outliner-core/move-nodes [node] true)
-      (let [children-of-2 (->> (build-block 2)
-                               (tree/-get-children)
-                               (mapv #(-> % :data :block/uuid)))]
-        (is (= [3 9 6] children-of-2))))))
+    (transact-tree! tree)
+    (outliner-tx/transact!
+      {:graph test-db}
+      (outliner-core/move-blocks-up-down! [(get-block 9)] true))
+    (is (= [3 9 6] (get-children 2)))))
 
-(comment
-  (run-test test-move-node))
-
-(deftest test-insert-nodes
+(deftest test-insert-blocks
   (testing "
   add [18 [19 20] 21] after 6
 
@@ -310,26 +244,283 @@
            [15]]]
       [16 [[17]]]]]
  "
-    (build-db-records node-tree)
-    (let [new-nodes-tree [(build-block 18)
-                          [(build-block 19)
-                           (build-block 20)]
-                          (build-block 21)]
-          target-node (build-block 6 2 3)]
-      (outliner-core/insert-nodes
-       new-nodes-tree target-node true)
-      (let [children-of-2 (->> (build-block 2)
-                               (tree/-get-children)
-                               (mapv #(-> % :data :block/uuid)))]
-        (is (= [3 6 18 21 9] children-of-2)))
-
-      (let [children-of-18 (->> (build-block 18)
-                                (tree/-get-children)
-                                (mapv #(-> % :data :block/uuid)))]
-        (is (= [19 20] children-of-18))))))
+    (transact-tree! tree)
+    (let [new-blocks (build-blocks [[18 [[19] [20]]]
+                                    [21]])
+          target-block (get-block 6)]
+      (outliner-tx/transact!
+        {:graph test-db}
+        (outliner-core/insert-blocks! new-blocks target-block {:sibling? true
+                                                               :keep-uuid? true
+                                                               :replace-empty-target? false}))
+      (is (= [3 6 18 21 9] (get-children 2)))
 
-(comment
-  (cljs.test/run-tests test-insert-nodes))
+      (is (= [19 20] (get-children 18))))))
+
+(deftest test-batch-transact
+  (testing "add 4, 5 after 2 and delete 3"
+    (let [tree [[1 [[2] [3]]]]]
+      (transact-tree! tree)
+      (let [new-blocks (build-blocks [[4 [5]]])
+            target-block (get-block 2)]
+        (outliner-tx/transact!
+          {:graph test-db}
+          (outliner-core/insert-blocks! new-blocks target-block {:sibling? false
+                                                                 :keep-uuid? true
+                                                                 :replace-empty-target? false})
+          (outliner-core/delete-blocks! [(get-block 3)] {}))
+
+        (is (= [4] (get-children 2)))
+
+        (is (= [5] (get-children 4)))
+
+        (is (nil? (get-block 3)))))))
+
+(deftest test-bocks-with-level
+  (testing "blocks with level"
+    (is (= (outliner-core/blocks-with-level
+            [{:db/id 6,
+              :block/left #:db{:id 3},
+              :block/level 3,
+              :block/parent #:db{:id 2},
+              :block/uuid 6}
+             {:db/id 9,
+              :block/left #:db{:id 6},
+              :block/level 3,
+              :block/parent #:db{:id 2},
+              :block/uuid 9}])
+           [{:db/id 6,
+             :block/left #:db{:id 3},
+             :block/level 1,
+             :block/parent #:db{:id 2},
+             :block/uuid 6}
+            {:db/id 9,
+             :block/left #:db{:id 6},
+             :block/level 1,
+             :block/parent #:db{:id 2},
+             :block/uuid 9}]))
+    (is (= (outliner-core/blocks-with-level
+            [{:db/id 6,
+              :block/left #:db{:id 3},
+              :block/level 3,
+              :block/parent #:db{:id 2},
+              :block/uuid 6}
+             {:db/id 9,
+              :block/left #:db{:id 6},
+              :block/level 4,
+              :block/parent #:db{:id 6},
+              :block/uuid 9}])
+           [{:db/id 6,
+             :block/left #:db{:id 3},
+             :block/level 1,
+             :block/parent #:db{:id 2},
+             :block/uuid 6}
+            {:db/id 9,
+             :block/left #:db{:id 6},
+             :block/level 2,
+             :block/parent #:db{:id 6},
+             :block/uuid 9}]))))
+
+;;; Fuzzy tests
+
+(def init-id (atom 100))
+
+(def unique-id (gen/fmap (fn [_] (swap! init-id inc)) gen/nat))
+(def compound (fn [inner-gen]
+                (gen/tuple unique-id (gen/vector inner-gen 1 2))))
+
+(def gen-node (gen/recursive-gen compound unique-id))
+
+(def gen-tree (gen/vector gen-node))
+
+(defn- gen-safe-tree
+  []
+  (->> (gen/generate gen-tree)
+       (remove integer?)))
+
+(defn gen-blocks
+  []
+  (let [tree (gen-safe-tree)]
+    (if (seq tree)
+      (let [result (build-blocks tree)]
+        (if (seq result)
+          result
+          (gen-blocks)))
+      (gen-blocks))))
+
+(defn insert-blocks!
+  [blocks target]
+  (outliner-tx/transact! {:graph test-db}
+    (outliner-core/insert-blocks! blocks
+                                  target
+                                  {:sibling? (gen/generate gen/boolean)
+                                   :keep-uuid? true
+                                   :replace-empty-target? false})))
+
+(defn transact-random-tree!
+  []
+  (let [tree (gen-safe-tree)]
+    (transact-tree! tree)))
+
+(defn get-datoms
+  []
+  (d/datoms (db/get-db test-db) :avet :block/uuid))
+
+(defn get-random-block
+  []
+  (let [datoms (->> (get-datoms)
+                    (remove (fn [datom] (= 1 (:e datom)))))]
+    (if (seq datoms)
+      (let [id (:e (gen/generate (gen/elements datoms)))]
+        (db/pull test-db '[*] id))
+      (do
+        (transact-random-tree!)
+        (get-random-block)))))
+
+(defn get-random-successive-blocks
+  []
+  (let [limit (inc (rand-int 20))]
+    (when-let [block (get-random-block)]
+      (loop [result [block]
+             node block]
+        (if-let [next (outliner-core/get-right-sibling (:db/id node))]
+          (let [next (db/pull test-db '[*] (:db/id next))]
+            (if (>= (count result) limit)
+              result
+              (recur (conj result next) next)))
+          result)))))
+
+(deftest random-inserts
+  (testing "Random inserts"
+    (transact-random-tree!)
+    (let [c1 (get-blocks-count)
+          *random-count (atom 0)]
+      (dotimes [_i 100]
+        (let [blocks (gen-blocks)]
+          (swap! *random-count + (count blocks))
+          (insert-blocks! blocks (get-random-block))))
+      (let [total (get-blocks-count)]
+        (is (= total (+ c1 @*random-count)))))))
+
+(deftest random-deletes
+  (testing "Random deletes"
+    (transact-random-tree!)
+    (dotimes [_i 100]
+      (insert-blocks! (gen-blocks) (get-random-block))
+      (let [blocks (get-random-successive-blocks)]
+        (when (seq blocks)
+          (outliner-tx/transact! {:graph test-db}
+            (outliner-core/delete-blocks! blocks {})))))))
+
+(deftest random-moves
+  (testing "Random moves"
+    (transact-random-tree!)
+    (let [c1 (get-blocks-count)
+          *random-count (atom 0)]
+      (dotimes [_i 100]
+        (let [blocks (gen-blocks)]
+          (swap! *random-count + (count blocks))
+          (insert-blocks! blocks (get-random-block)))
+        (let [blocks (get-random-successive-blocks)]
+          (when (seq blocks)
+            (let [target (get-random-block)]
+              (outliner-tx/transact! {:graph test-db}
+                (outliner-core/move-blocks! blocks target (gen/generate gen/boolean)))
+              (let [total (get-blocks-count)]
+                (is (= total (+ c1 @*random-count)))))))))))
+
+(deftest random-move-up-down
+  (testing "Random move up down"
+    (transact-random-tree!)
+    (let [c1 (get-blocks-count)
+          *random-count (atom 0)]
+      (dotimes [_i 100]
+        (let [blocks (gen-blocks)]
+          (swap! *random-count + (count blocks))
+          (insert-blocks! blocks (get-random-block)))
+        (let [blocks (get-random-successive-blocks)]
+          (when (seq blocks)
+            (outliner-tx/transact! {:graph test-db}
+              (outliner-core/move-blocks-up-down! blocks (gen/generate gen/boolean)))
+            (let [total (get-blocks-count)]
+              (is (= total (+ c1 @*random-count))))))))))
+
+(deftest random-indent-outdent
+  (testing "Random indent and outdent"
+    (transact-random-tree!)
+    (let [c1 (get-blocks-count)
+          *random-count (atom 0)]
+      (dotimes [_i 100]
+        (let [blocks (gen-blocks)]
+          (swap! *random-count + (count blocks))
+          (insert-blocks! blocks (get-random-block)))
+        (let [blocks (get-random-successive-blocks)]
+          (when (seq blocks)
+            (outliner-tx/transact! {:graph test-db}
+              (outliner-core/indent-outdent-blocks! blocks (gen/generate gen/boolean)))
+            (let [total (get-blocks-count)]
+              (is (= total (+ c1 @*random-count))))))))))
+
+(deftest random-mixed-ops
+  (testing "Random mixed operations"
+    (transact-random-tree!)
+    (let [c1 (get-blocks-count)
+          *random-count (atom 0)
+          ops [
+               ;; insert
+               (fn []
+                 (let [blocks (gen-blocks)]
+                   (swap! *random-count + (count blocks))
+                   (insert-blocks! blocks (get-random-block))))
+
+               ;; delete
+               (fn []
+                 (let [blocks (get-random-successive-blocks)]
+                   (when (seq blocks)
+                     (swap! *random-count - (count blocks))
+                     (outliner-tx/transact! {:graph test-db}
+                       (outliner-core/delete-blocks! blocks {})))))
+
+               ;; move
+               (fn []
+                 (let [blocks (get-random-successive-blocks)]
+                   (when (seq blocks)
+                     (outliner-tx/transact! {:graph test-db}
+                       (outliner-core/move-blocks! blocks (get-random-block) (gen/generate gen/boolean))))))
+
+               ;; move up down
+               (fn []
+                 (let [blocks (get-random-successive-blocks)]
+                   (when (seq blocks)
+                     (outliner-tx/transact! {:graph test-db}
+                      (outliner-core/move-blocks-up-down! blocks (gen/generate gen/boolean))))))
+
+               ;; indent outdent
+               (fn []
+                 (let [blocks (get-random-successive-blocks)]
+                   (when (seq blocks)
+                     (outliner-tx/transact! {:graph test-db}
+                       (outliner-core/indent-outdent-blocks! blocks (gen/generate gen/boolean))))))]]
+      (dotimes [_i 500]
+        ((rand-nth ops)))
+      (let [total (get-blocks-count)
+            page-id 1]
+
+        ;; Invariants:
+
+        ;; 1. created blocks length >= existing blocks + deleted top-level blocks
+        (is (<= total (+ c1 @*random-count)))
+
+        ;; 2. verify page's length + page itself = total blocks
+        (is (= (inc (db-model/get-page-blocks-count test-db page-id))
+               total))
+
+        ;; 3. verify the outliner parent/left structure
+        (is (= (inc (count (db-model/get-paginated-blocks test-db page-id {:limit total
+                                                                           :use-cache? false})))
+               total))))))
 
 (comment
-  (cljs.test/run-tests))
+  (dotimes [i 5]
+    (cljs.test/run-tests))
+  )

+ 0 - 33
src/test/frontend/modules/outliner/ds_test.cljs

@@ -1,33 +0,0 @@
-(ns frontend.modules.outliner.ds-test
-  (:require [cljs.test :refer [deftest is use-fixtures] :as test]
-            [frontend.test.fixtures :as fixtures]
-            [frontend.modules.outliner.datascript :as ds]))
-
-(use-fixtures :each
-  fixtures/load-test-env
-  fixtures/reset-db)
-
-(deftest test-with-db-macro
-  (let [db-report (ds/auto-transact! [txs-state (ds/new-outliner-txs-state)]
-                    nil
-                    (let [datom [{:block/uuid #uuid"606c1962-ad7f-424e-b120-0dc7fcb25415",
-                                  :block/refs (),
-                                  :block/repo "logseq_local_test_navtive_fs",
-                                  :block/meta {:timestamps [], :properties [], :start-pos 0, :end-pos 15},
-                                  :block/format :markdown,
-                                  :block/level 1,
-                                  :block/tags [],
-                                  :block/refs-with-children (),
-                                  :block/content "level test",
-                                  :db/id 72,
-                                  :block/path-refs (),}]]
-                      (ds/add-txs txs-state datom)))
-        rt [[72 :block/uuid #uuid "606c1962-ad7f-424e-b120-0dc7fcb25415" 536870913 true]
-            [72 :block/repo "logseq_local_test_navtive_fs" 536870913 true]
-            [72 :block/format :markdown 536870913 true]
-            [72 :block/refs-with-children () 536870913 true]
-            [72 :block/content "level test" 536870913 true]]]
-    (is (= rt (mapv vec (:tx-data db-report))))))
-
-(comment
-  (test/run-tests))

+ 4 - 3
src/test/frontend/test/fixtures.cljs

@@ -4,7 +4,8 @@
             [frontend.db-schema :as db-schema]
             [frontend.db.conn :as conn]
             [frontend.db.react :as react]
-            [frontend.state :as state]))
+            [frontend.state :as state]
+            [frontend.test.helper :as helper]))
 
 (defn load-test-env
   [f]
@@ -21,12 +22,12 @@
   [repo]
   (let [db-name (conn/datascript-db repo)
         db-conn (d/create-conn db-schema/schema)]
-    (conn/reset-conn! conn/conns {})
+    (state/set-current-repo! repo)
     (swap! conn/conns assoc db-name db-conn)))
 
 (defn reset-db
   [f]
-  (let [repo (state/get-current-repo)]
+  (let [repo helper/test-db]
     (reset-datascript repo)
     (let [r (f)]
       (reset-datascript repo) r)))

Some files were not shown because too many files changed in this diff