Selaa lähdekoodia

enhance(ux): set/unset #Page to convert between page and block (#11970)

* enhance(ux): add/remove #Page to toggle page/block

* enhance(ux): "p t" to set tags for selected blocks

Updated "p a" to toggle displaying all properties including hidden ones.

* feat: cmd+k move blocks

* fix: block could be converted to page and create invalid nodes

Pages with block namespace parents is nonsensical and pages in
property values led to multiple validation errors. Also guard
against class and property pages as namespace parents

* fix: warn on failed cut+paste or indent/outdent of page blocks. Better to warn than silent failure which leaves user confused on
what happened. 

---------

Co-authored-by: Gabriel Horner <[email protected]>
Tienson Qin 2 kuukautta sitten
vanhempi
sitoutus
d84d3f9652
39 muutettua tiedostoa jossa 951 lisäystä ja 436 poistoa
  1. 8 1
      clj-e2e/dev/user.clj
  2. 4 3
      clj-e2e/src/logseq/e2e/assert.clj
  3. 17 0
      clj-e2e/src/logseq/e2e/block.clj
  4. 8 4
      clj-e2e/src/logseq/e2e/graph.clj
  5. 28 17
      clj-e2e/src/logseq/e2e/util.clj
  6. 96 0
      clj-e2e/test/logseq/e2e/editor_basic_test.clj
  7. 1 0
      clj-e2e/test/logseq/e2e/outliner_basic_test.clj
  8. 18 0
      deps/db/src/logseq/db.cljs
  9. 51 36
      deps/outliner/src/logseq/outliner/core.cljs
  10. 10 3
      deps/outliner/src/logseq/outliner/op.cljs
  11. 22 13
      deps/outliner/src/logseq/outliner/property.cljs
  12. 77 6
      deps/outliner/src/logseq/outliner/validate.cljs
  13. 67 7
      deps/outliner/test/logseq/outliner/validate_test.cljs
  14. 56 21
      src/main/frontend/components/cmdk/core.cljs
  15. 2 1
      src/main/frontend/components/container.cljs
  16. 2 1
      src/main/frontend/components/header.cljs
  17. 2 7
      src/main/frontend/components/library.cljs
  18. 2 0
      src/main/frontend/components/property/dialog.cljs
  19. 20 14
      src/main/frontend/components/property/value.cljs
  20. 1 0
      src/main/frontend/components/select.cljs
  21. 68 66
      src/main/frontend/components/selection.cljs
  22. 6 0
      src/main/frontend/handler/db_based/property.cljs
  23. 5 4
      src/main/frontend/handler/dnd.cljs
  24. 46 18
      src/main/frontend/handler/editor.cljs
  25. 9 9
      src/main/frontend/handler/events/ui.cljs
  26. 3 3
      src/main/frontend/log.cljs
  27. 7 2
      src/main/frontend/modules/outliner/op.cljs
  28. 12 1
      src/main/frontend/modules/shortcut/config.cljs
  29. 1 0
      src/main/frontend/state.cljs
  30. 2 1
      src/main/frontend/ui.cljs
  31. 2 1
      src/main/frontend/worker/handler/page.cljs
  32. 86 7
      src/main/frontend/worker/pipeline.cljs
  33. 16 11
      src/main/frontend/worker/rtc/remote_update.cljs
  34. 7 3
      src/main/frontend/worker/search.cljs
  35. 108 108
      src/main/logseq/api.cljs
  36. 58 58
      src/main/logseq/api/block.cljs
  37. 3 3
      src/main/mobile/components/settings.cljs
  38. 2 0
      src/resources/dicts/en.edn
  39. 18 7
      src/test/frontend/modules/outliner/core_test.cljs

+ 8 - 1
clj-e2e/dev/user.clj

@@ -4,6 +4,7 @@
             [logseq.e2e.block :as b]
             [logseq.e2e.commands-basic-test]
             [logseq.e2e.config :as config]
+            [logseq.e2e.editor-basic-test]
             [logseq.e2e.fixtures :as fixtures]
             [logseq.e2e.graph :as graph]
             [logseq.e2e.keyboard :as k]
@@ -73,6 +74,11 @@
   (->> (future (run-tests 'logseq.e2e.rtc-extra-test))
        (swap! *futures assoc :rtc-extra-test)))
 
+(defn run-editor-basic-test
+  []
+  (->> (future (run-tests 'logseq.e2e.editor-basic-test))
+       (swap! *futures assoc :editor-basic-test)))
+
 (defn run-tag-basic-test
   []
   (->> (future (run-tests 'logseq.e2e.tag-basic-test))
@@ -80,7 +86,8 @@
 
 (defn run-all-basic-test
   []
-  (run-tests 'logseq.e2e.commands-basic-test
+  (run-tests 'logseq.e2e.editor-basic-test
+             'logseq.e2e.commands-basic-test
              'logseq.e2e.multi-tabs-basic-test
              'logseq.e2e.outliner-basic-test
              'logseq.e2e.rtc-basic-test

+ 4 - 3
clj-e2e/src/logseq/e2e/assert.clj

@@ -18,9 +18,10 @@
 
 (defn assert-non-editor-mode
   []
-  (assert-is-hidden (loc/or "[data-testid='block editor']"
-                            ;; TODO: remove this when this prop-name fixed on dom
-                            "[datatestid='block editor']")))
+  (w/wait-for-not-visible
+   (loc/or "[data-testid='block editor']"
+           ;; TODO: remove this when this prop-name fixed on dom
+           "[datatestid='block editor']")))
 
 (defn assert-in-normal-mode?
   "- not editing mode

+ 17 - 0
clj-e2e/src/logseq/e2e/block.clj

@@ -97,3 +97,20 @@
 (defn outdent
   []
   (indent-outdent false))
+
+(defn toggle-property
+  [property-title property-value]
+  (k/press (if util/mac? "ControlOrMeta+p" "Control+Alt+p"))
+  (w/fill ".ls-property-dialog .ls-property-input input" property-title)
+  (w/wait-for (format "#ac-0.menu-link:has-text('%s')" property-title))
+  (k/enter)
+  (util/wait-timeout 100)
+  (w/click (w/-query ".ls-property-dialog .ls-property-input input"))
+  (util/wait-timeout 100)
+  (util/input property-value)
+  (w/wait-for (format "#ac-0.menu-link:has-text('%s')" property-value))
+  (k/enter))
+
+(defn select-blocks
+  [n]
+  (util/repeat-keyboard n "Shift+ArrowUp"))

+ 8 - 4
clj-e2e/src/logseq/e2e/graph.clj

@@ -2,9 +2,11 @@
   (:require [clojure.edn :as edn]
             [clojure.string :as string]
             [logseq.e2e.assert :as assert]
+            [logseq.e2e.keyboard :as k]
             [logseq.e2e.locator :as loc]
             [logseq.e2e.util :as util]
-            [wally.main :as w]))
+            [wally.main :as w]
+            [wally.repl :as repl]))
 
 (defn- refresh-all-remote-graphs
   []
@@ -55,9 +57,11 @@
 
 (defn validate-graph
   []
+  (k/esc)
+  (k/esc)
   (util/search-and-click "(Dev) Validate current graph")
-  (assert/assert-is-visible (loc/and ".notifications div" (w/get-by-text "Your graph is valid")))
-  (let [content (.textContent (loc/and ".notifications div" (w/get-by-text "Your graph is valid")))
+  (assert/assert-is-visible (loc/and ".notifications div.notification-success div" (w/get-by-text "Your graph is valid")))
+  (let [content (.textContent (loc/and ".notifications div.notification-success div" (w/get-by-text "Your graph is valid")))
         summary (edn/read-string (subs content (string/index-of content "{")))]
-    (w/click ".notifications .ls-icon-x")
+    (w/click ".notifications div.notification-success .ls-icon-x")
     summary))

+ 28 - 17
clj-e2e/src/logseq/e2e/util.clj

@@ -63,19 +63,31 @@
     (.pressSequentially input-node text
                         (.setDelay (Locator$PressSequentiallyOptions.) delay))))
 
+(defn exit-edit
+  []
+  (when (get-editor)
+    (k/esc))
+  (assert/assert-non-editor-mode))
+
 (defn double-esc
   "Exits editing mode and ensure there's no action bar"
   []
-  (k/esc)
-  (k/esc))
+  (when (w/visible? "div[data-radix-popper-content-wrapper]")
+    (k/esc))
+  (exit-edit)
+  (when (w/visible? "div[data-radix-popper-content-wrapper]")
+    (k/esc)))
 
 (defn search
   [text]
-  (double-esc)
-  (assert/assert-in-normal-mode?)
-  (w/click :#search-button)
-  (w/wait-for ".cp__cmdk-search-input")
-  (w/fill ".cp__cmdk-search-input" text))
+  (if (w/visible? ".cp__cmdk-search-input")
+    (w/fill ".cp__cmdk-search-input" text)
+    (do
+      (double-esc)
+      (assert/assert-in-normal-mode?)
+      (w/click :#search-button)
+      (w/wait-for ".cp__cmdk-search-input")
+      (w/fill ".cp__cmdk-search-input" text))))
 
 (defn search-and-click
   [search-text]
@@ -105,11 +117,6 @@
   []
   (count-elements ".ls-page-blocks .page-blocks-inner .ls-block"))
 
-(defn exit-edit
-  []
-  (k/esc)
-  (assert/assert-non-editor-mode))
-
 (defn get-text
   [locator]
   (if (string? locator)
@@ -129,7 +136,7 @@
 (defn repeat-keyboard
   [n shortcut]
   (dotimes [_i n]
-    (k/press shortcut)))
+    (k/press shortcut {:delay 20})))
 
 (defn get-page-blocks-contents
   []
@@ -161,11 +168,13 @@
 
 (defn move-cursor-to-end
   []
-  (k/press "ControlOrMeta+a" "ArrowRight"))
+  (k/press ["ControlOrMeta+a" "ArrowRight"]
+           {:delay 20}))
 
 (defn move-cursor-to-start
   []
-  (k/press "ControlOrMeta+a" "ArrowLeft"))
+  (k/press ["ControlOrMeta+a" "ArrowLeft"]
+           {:delay 20}))
 
 (defn input-command
   [command]
@@ -179,11 +188,13 @@
   (w/click "a.menu-link.chosen"))
 
 (defn set-tag
-  [tag]
+  "`hidden?`: some tags may be hidden from the UI, e.g. Page"
+  [tag & {:keys [hidden?]
+          :or {hidden? false}}]
   (press-seq " #" {:delay 20})
   (press-seq tag)
   (w/click (first (w/query (format "a.menu-link:has-text(\"%s\")" tag))))
-  (when (not= (string/lower-case tag) "task")
+  (when (and (not= (string/lower-case tag) "task") (not hidden?))
     ;; wait tag added on ui
     (assert/assert-is-visible
      (-> ".ls-block:not(.block-add-button)"

+ 96 - 0
clj-e2e/test/logseq/e2e/editor_basic_test.clj

@@ -0,0 +1,96 @@
+(ns logseq.e2e.editor-basic-test
+  (:require
+   [clojure.set :as set]
+   [clojure.test :refer [deftest testing is use-fixtures]]
+   [logseq.e2e.assert :as assert]
+   [logseq.e2e.block :as b]
+   [logseq.e2e.fixtures :as fixtures]
+   [logseq.e2e.keyboard :as k]
+   [logseq.e2e.page :as p]
+   [logseq.e2e.util :as util]
+   [wally.main :as w]
+   [wally.repl :as repl]))
+
+(use-fixtures :once fixtures/open-page)
+
+(use-fixtures :each
+  fixtures/new-logseq-page
+  fixtures/validate-graph)
+
+(deftest toggle-between-page-and-block
+  (testing "Convert block to page and back"
+    (b/new-block "b1")
+    (util/set-tag "Page" {:hidden? true})
+    (assert/assert-is-visible ".ls-page-blocks .ls-block .ls-icon-file")
+    (b/toggle-property "Tags" "Page")
+    (assert/assert-is-hidden ".ls-page-blocks .ls-block .ls-icon-file")))
+
+(deftest toggle-between-page-and-block-for-selected-blocks
+  (testing "Convert selected blocks to pages and back"
+    (b/new-blocks ["b1" "b2" "b3"])
+    (b/select-blocks 3)
+    (b/toggle-property "Tags" "Page")
+    (assert/assert-is-visible ".ls-page-blocks .ls-block .ls-icon-file")
+    (w/wait-for (format "#ac-0.menu-link:has-text('%s')" "Page"))
+    (k/enter)
+    (w/wait-for-not-visible ".ls-page-blocks .ls-block .ls-icon-file")))
+
+(deftest disallow-adding-page-tag-to-normal-pages
+  (testing "Disallow adding #Page to normal pages"
+    (k/arrow-up)
+    (util/move-cursor-to-end)
+    (util/press-seq " #" {:delay 20})
+    (util/press-seq "Page")
+    (assert/assert-is-hidden (format "#ac-0.menu-link:has-text('%s')" "Page"))))
+
+(deftest move-blocks-mod+shift+m
+  (testing "move blocks using `mod+shift+m`"
+    (p/new-page "Target page")
+    (p/new-page "Source page")
+    (b/new-blocks ["b1" "b2" "b3"])
+    (b/select-blocks 3)
+    (k/press "ControlOrMeta+Shift+m")
+    (w/fill "input[placeholder=\"Move blocks to\"]" "Target page")
+    (w/wait-for (w/get-by-test-id "Target page"))
+    (.focus (w/-query ".cp__cmdk-search-input"))
+    (k/enter)
+    (assert/assert-have-count ".ls-page-blocks .page-blocks-inner .ls-block" 0)))
+
+(deftest move-blocks-cmdk
+  (testing "move blocks using cmdk"
+    (p/new-page "Target page 2")
+    (p/new-page "Source page 2")
+    (b/new-blocks ["b1" "b2" "b3"])
+    (b/select-blocks 3)
+    (util/search-and-click "Move blocks to")
+    (w/fill "input[placeholder=\"Move blocks to\"]" "Target page 2")
+    (w/wait-for (w/get-by-test-id "Target page 2"))
+    (.focus (w/-query ".cp__cmdk-search-input"))
+    (k/enter)
+    (assert/assert-have-count ".ls-page-blocks .page-blocks-inner .ls-block" 0)))
+
+(deftest move-pages-to-library
+  (testing "move pages using `mod+shift+m`"
+    (p/goto-page "Library")
+    (p/new-page "test page")
+    (b/new-blocks ["block1" "block2" "block3"])
+    (b/select-blocks 3)
+    (k/press "ControlOrMeta+Shift+m")
+    (w/fill "input[placeholder=\"Move blocks to\"]" "Library")
+    (w/wait-for (w/get-by-test-id "Library"))
+    (.focus (w/-query ".cp__cmdk-search-input"))
+    (k/enter)
+    (p/goto-page "Library")
+    (let [contents (set (util/get-page-blocks-contents))]
+      (is (set/subset? (set ["block1" "block2" "block3"]) contents)))
+    (p/goto-page "test page")
+    (b/new-blocks ["block4" "block5"])
+    (b/select-blocks 2)
+    (k/press "ControlOrMeta+Shift+m")
+    (w/fill "input[placeholder=\"Move blocks to\"]" "Library")
+    (w/wait-for (w/get-by-test-id "Library"))
+    (.focus (w/-query ".cp__cmdk-search-input"))
+    (k/enter)
+    (p/goto-page "Library")
+    (let [contents (set (util/get-page-blocks-contents))]
+      (is (set/subset? (set ["block1" "block2" "block3" "block4" "block5"]) contents)))))

+ 1 - 0
clj-e2e/test/logseq/e2e/outliner_basic_test.clj

@@ -100,6 +100,7 @@
     (b/indent)
     (k/arrow-up)
     (b/delete-blocks)
+    (util/wait-editor-visible)
     (is (= "b1" (util/get-edit-content)))
     (is (= 1 (util/page-blocks-count)))))
 

+ 18 - 0
deps/db/src/logseq/db.cljs

@@ -7,6 +7,7 @@
             [clojure.walk :as walk]
             [datascript.core :as d]
             [datascript.impl.entity :as de]
+            [logseq.common.config :as common-config]
             [logseq.common.util :as common-util]
             [logseq.common.uuid :as common-uuid]
             [logseq.db.common.delete-blocks :as delete-blocks] ;; Load entity extensions
@@ -272,6 +273,9 @@
 (def get-built-in-page db-db/get-built-in-page)
 
 (def library? db-db/library?)
+(defn get-library-page
+  [db]
+  (get-built-in-page db common-config/library-page-name))
 
 (defn get-case-page
   "Case sensitive version of get-page. For use with DB graphs"
@@ -575,3 +579,17 @@
   (if (sqlite-util/db-based-graph? repo)
     db-schema/schema
     file-schema/schema))
+
+(defn page-in-library?
+  "Check whether a `page` exists on the Library page"
+  [db page]
+  (when (page? page)
+    (when-let [library-page (get-built-in-page db common-config/library-page-name)]
+      (loop [parent (:block/parent page)]
+        (cond
+          (nil? parent)
+          false
+          (= (:db/id parent) (:db/id library-page))
+          true
+          :else
+          (recur (:block/parent parent)))))))

+ 51 - 36
deps/outliner/src/logseq/outliner/core.cljs

@@ -522,22 +522,22 @@
      block)))
 
 (defn- get-target-block-page
-  [target-block]
+  [target-block sibling?]
   (or
-   (when (ldb/page? target-block)
-     (:db/id target-block))
    (:db/id (:block/page target-block))
    ;; target parent is a page
-   (when-let [parent (:block/parent target-block)]
-     (when (ldb/page? parent)
-       (:db/id parent)))
+   (when sibling?
+     (when-let [parent (:block/parent target-block)]
+       (when (ldb/page? parent)
+         (:db/id parent))))
+
    ;; target-block is a page itself
    (:db/id target-block)))
 
 (defn- build-insert-blocks-tx
   [db target-block blocks uuids get-new-id {:keys [sibling? outliner-op replace-empty-target? insert-template? keep-block-order?]}]
   (let [block-ids (set (map :block/uuid blocks))
-        target-page (get-target-block-page target-block)
+        target-page (get-target-block-page target-block sibling?)
         orders (get-block-orders blocks target-block sibling? keep-block-order?)]
     (map-indexed (fn [idx {:block/keys [parent] :as block}]
                    (when-let [uuid' (get uuids (:block/uuid block))]
@@ -551,28 +551,31 @@
                                                       (let [ref-ids (set (map :block/uuid (:block/refs block)))]
                                                         (->> (set/intersection block-ids ref-ids)
                                                              (remove #{(:block/uuid block)})))))
-                           m (cond->
-                              {:db/id (:db/id block)
-                               :block/uuid uuid'
-                               :block/parent parent
-                               :block/order order}
-                               (not (ldb/page? block))
-                               (assoc :block/page target-page))
-                           result (->
-                                   (if (de/entity? block)
-                                     (assoc m :block/level (:block/level block))
-                                     (merge block m))
-                                   (update :block/title (fn [value]
-                                                          (if (seq template-ref-block-ids)
-                                                            (reduce
-                                                             (fn [value id]
-                                                               (string/replace value
-                                                                               (page-ref/->page-ref id)
-                                                                               (page-ref/->page-ref (uuids id))))
-                                                             value
-                                                             template-ref-block-ids)
-                                                            value)))
-                                   (dissoc :db/id))]
+                           m {:db/id (:db/id block)
+                              :block/uuid uuid'
+                              :block/parent parent
+                              :block/order order}
+                           result* (->
+                                    (if (de/entity? block)
+                                      (assoc m :block/level (:block/level block))
+                                      (merge block m))
+                                    (update :block/title (fn [value]
+                                                           (if (seq template-ref-block-ids)
+                                                             (reduce
+                                                              (fn [value id]
+                                                                (string/replace value
+                                                                                (page-ref/->page-ref id)
+                                                                                (page-ref/->page-ref (uuids id))))
+                                                              value
+                                                              template-ref-block-ids)
+                                                             value)))
+                                    (dissoc :db/id))
+                           page? (or (ldb/page? block) (:block/name block))
+                           result (cond-> result*
+                                    (not page?)
+                                    (assoc :block/page target-page)
+                                    page?
+                                    (dissoc :block/page))]
                        (update-property-ref-when-paste result uuids))))
                  blocks)))
 
@@ -608,7 +611,7 @@
      :id->new-uuid id->new-uuid}))
 
 (defn- get-target-block
-  [db blocks target-block {:keys [outliner-op indent? sibling? up?]}]
+  [db blocks target-block {:keys [outliner-op bottom? top? indent? sibling? up?]}]
   (when-let [block (if (:db/id target-block)
                      (d/entity db (:db/id target-block))
                      (when (:block/uuid target-block)
@@ -636,7 +639,16 @@
                              [block sibling?]
 
                              (contains? #{:insert-blocks :move-blocks} outliner-op)
-                             [block sibling?]
+                             (cond
+                               top?
+                               [block false]
+
+                               bottom?
+                               (if-let [last-child (last (ldb/sort-by-order (:block/_parent block)))]
+                                 [last-child true]
+                                 [block false])
+                               :else
+                               [block sibling?])
 
                              linked
                              (get-last-child-or-self db linked)
@@ -683,6 +695,8 @@
     `target-block`: where `blocks` will be inserted.
     Options:
       `sibling?`: as siblings (true) or children (false).
+      `bottom?`: inserts block to the bottom.
+      `top?`: inserts block to the top.
       `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 internal cut or drag & drop.
@@ -854,7 +868,7 @@
   (let [target-block (d/entity db (:db/id target-block))
         block (d/entity db (:db/id block))
         first-block-page (:db/id (:block/page block))
-        target-page (get-target-block-page target-block)
+        target-page (get-target-block-page target-block sibling?)
         not-same-page? (not= first-block-page target-page)
         block-order (if sibling?
                       (db-order/gen-key (:block/order target-block)
@@ -890,7 +904,7 @@
 
 (defn- move-blocks
   "Move `blocks` to `target-block` as siblings or children."
-  [_repo conn blocks target-block {:keys [_sibling? _up? outliner-op _indent?]
+  [_repo conn blocks target-block {:keys [_sibling? _top? _bottom? _up? outliner-op _indent?]
                                    :as opts}]
   {:pre [(seq blocks)
          (m/validate block-map-or-entity target-block)]}
@@ -1065,9 +1079,10 @@
     (op-transact! f repo conn blocks opts)))
 
 (defn move-blocks!
-  [repo conn blocks target-block sibling?]
-  (op-transact! move-blocks repo conn blocks target-block {:sibling? sibling?
-                                                           :outliner-op :move-blocks}))
+  [repo conn blocks target-block opts]
+  (op-transact! move-blocks repo conn blocks target-block
+                (assoc opts :outliner-op :move-blocks)))
+
 (defn move-blocks-up-down!
   [repo conn blocks up?]
   (op-transact! move-blocks-up-down repo conn blocks up?))

+ 10 - 3
deps/outliner/src/logseq/outliner/op.cljs

@@ -26,7 +26,7 @@
    [:move-blocks
     [:catn
      [:op :keyword]
-     [:args [:tuple ::ids ::id :boolean]]]]
+     [:args [:tuple ::ids ::id ::option]]]]
    [:move-blocks-up-down
     [:catn
      [:op :keyword]
@@ -53,6 +53,10 @@
     [:catn
      [:op :keyword]
      [:args [:tuple ::block-id ::property-id ::value]]]]
+   [:batch-delete-property-value
+    [:catn
+     [:op :keyword]
+     [:args [:tuple ::block-ids ::property-id ::value]]]]
    [:create-property-text-block
     [:catn
      [:op :keyword]
@@ -171,11 +175,11 @@
            (outliner-core/delete-blocks! repo conn date-formatter blocks (merge opts opts')))
 
          :move-blocks
-         (let [[block-ids target-block-id sibling?] args
+         (let [[block-ids target-block-id opts] args
                blocks (keep #(d/entity @conn %) block-ids)
                target-block (d/entity @conn target-block-id)]
            (when (and target-block (seq blocks))
-             (outliner-core/move-blocks! repo conn blocks target-block sibling?)))
+             (outliner-core/move-blocks! repo conn blocks target-block opts)))
 
          :move-blocks-up-down
          (let [[block-ids up?] args
@@ -211,6 +215,9 @@
          :batch-remove-property
          (apply outliner-property/batch-remove-property! conn args)
 
+         :batch-delete-property-value
+         (apply outliner-property/batch-delete-property-value! conn args)
+
          :class-add-property
          (apply outliner-property/class-add-property! conn args)
 

+ 22 - 13
deps/outliner/src/logseq/outliner/property.cljs

@@ -520,22 +520,31 @@
                          {:outliner-op :upsert-property}))
         (d/entity @conn db-ident')))))
 
+(defn batch-delete-property-value!
+  "batch delete value when a property has multiple values"
+  [conn block-eids property-id property-value]
+  (when-let [property (d/entity @conn property-id)]
+    (when (and (db-property/many? property)
+               (not (some #(= property-id (:db/ident (d/entity @conn %))) block-eids)))
+      (when (= property-id :block/tags)
+        (outliner-validate/validate-tags-property-deletion @conn block-eids property-value))
+      (if (= property-id :block/tags)
+        (let [tx-data (map (fn [id] [:db/retract id property-id property-value]) block-eids)]
+          (ldb/transact! conn tx-data {:outliner-op :save-block}))
+        (doseq [block-eid block-eids]
+          (when-let [block (d/entity @conn block-eid)]
+            (let [current-val (get block property-id)
+                  fv (first current-val)]
+              (if (and (= 1 (count current-val)) (or (= property-value fv) (= property-value (:db/id fv))))
+                (remove-block-property! conn (:db/id block) property-id)
+                (ldb/transact! conn
+                               [[:db/retract (:db/id block) property-id property-value]]
+                               {:outliner-op :save-block})))))))))
+
 (defn delete-property-value!
   "Delete value if a property has multiple values"
   [conn block-eid property-id property-value]
-  (when-let [property (d/entity @conn property-id)]
-    (let [block (d/entity @conn block-eid)]
-      (when (and block (not= property-id (:db/ident block)) (db-property/many? property))
-        (when (= property-id :block/tags)
-          (outliner-validate/validate-tags-property-deletion @conn [block-eid] property-value))
-
-        (let [current-val (get block property-id)
-              fv (first current-val)]
-          (if (and (= 1 (count current-val)) (or (= property-value fv) (= property-value (:db/id fv))))
-            (remove-block-property! conn (:db/id block) property-id)
-            (ldb/transact! conn
-                           [[:db/retract (:db/id block) property-id property-value]]
-                           {:outliner-op :save-block})))))))
+  (batch-delete-property-value! conn [block-eid] property-id property-value))
 
 (defn ^:api get-classes-parents
   [tags]

+ 77 - 6
deps/outliner/src/logseq/outliner/validate.cljs

@@ -204,11 +204,13 @@
 
 (defn- disallow-node-cant-tag-with-private-tags
   [db block-eids v & {:keys [delete?]}]
-  (when (and (ldb/private-tags (:db/ident (d/entity db v)))
-             ;; Allow assets to be tagged
-             (not (and
-                   (every? (fn [id] (ldb/asset? (d/entity db id))) block-eids)
-                   (= :logseq.class/Asset (:db/ident (d/entity db v))))))
+  ;; Skip #Page as it is validated by later fns
+  (when (and (contains? (disj ldb/private-tags :logseq.class/Page) (:db/ident (d/entity db v)))
+             (not
+               ;; Allow assets to be tagged
+              (and
+               (every? (fn [id] (ldb/asset? (d/entity db id))) block-eids)
+               (= :logseq.class/Asset (:db/ident (d/entity db v))))))
     (throw (ex-info (str (if delete? "Can't remove tag" "Can't set tag")
                          " with built-in #" (:block/title (d/entity db v)))
                     {:type :notification
@@ -229,15 +231,84 @@
                                              " on built-in " (pr-str (:block/title built-in-ent)))
                                :type :error}}))))
 
+(defn- disallow-removing-page-tag
+  "Disallow page->block when
+  1. this page doesn't have :block/parent
+  2. its parent is Library
+  3. it has page child"
+  [db eids v]
+  (when (= (:db/ident (d/entity db v)) :logseq.class/Page)
+    (let [library-page (ldb/get-library-page db)]
+      (doseq [eid eids]
+        (let [entity (d/entity db eid)]
+          (when (ldb/internal-page? entity)
+            (cond
+              (not (:block/parent entity))
+              (throw (ex-info "This page cannot be converted to a block"
+                              {:type :notification
+                               :payload
+                               {:message (str "Page " (pr-str (:block/title entity)) " cannot be converted to a block")
+                                :type :error
+                                :entity (into {} entity)
+                                :property :block/tags}}))
+              (= (:db/id library-page) (:db/id (:block/parent entity)))
+              (throw (ex-info "This page cannot be converted to a block"
+                              {:type :notification
+                               :payload
+                               {:message (str "Page " (pr-str (:block/title entity)) " cannot be converted to a block, please move it to another page first")
+                                :type :error
+                                :entity (into {} entity)
+                                :property :block/tags}}))
+              (some entity-util/page? (:block/_parent entity))
+              (throw (ex-info "This page cannot be converted to a block"
+                              {:type :notification
+                               :payload
+                               {:message (str "Page " (pr-str (:block/title entity)) " cannot be converted to a block because it has page children")
+                                :type :error
+                                :entity (into {} entity)
+                                :property :block/tags}})))))))))
+
+(defn- validate-block-can-tag-with-page-tag
+  "Validates block can convert to page by adding #Page for allowed scenarios"
+  [db eids v]
+  (when (= (:db/ident (d/entity db v)) :logseq.class/Page)
+    (doseq [eid eids]
+      (let [block (d/entity db eid)]
+        (when (:block/parent block)
+          (validate-page-title (:block/title block) {:node block})
+          (validate-page-title-characters (:block/title block) {:node block})
+
+          ;; Only allow block to be page when its parent is a page to guard against invalid pages
+          ;; in property values or pages being created with blocks as namespace parents
+          (when (or (not (entity-util/page? (:block/parent block)))
+                    (:logseq.property/created-from-property block))
+            (let [message (if (:logseq.property/created-from-property block)
+                            "Can't convert property value to page."
+                            "Can't convert this block to page since its parent is not a page.")]
+              (throw (ex-info message
+                              {:type :notification
+                               :payload {:message message
+                                         :type :error
+                                         :block (into {} block)}}))))
+          ;; Guard against classes and properties becoming namespace parents
+          (when (or (entity-util/class? (:block/page block)) (entity-util/property? (:block/page block)))
+            (throw (ex-info "Can't convert this block to page when block is in a property or tag."
+                            {:type :notification
+                             :payload {:message "Can't convert this block to page when block is in a property or tag."
+                                       :type :error
+                                       :block (into {} block)}}))))))))
+
 (defn validate-tags-property
   "Validates adding a property value to :block/tags for given blocks"
   [db block-eids v]
   (disallow-tagging-a-built-in-entity db block-eids)
   (disallow-node-cant-tag-with-private-tags db block-eids v)
+  (validate-block-can-tag-with-page-tag db block-eids v)
   (disallow-node-cant-tag-with-built-in-non-tags db block-eids v))
 
 (defn validate-tags-property-deletion
   "Validates deleting a property value from :block/tags for given blocks"
   [db block-eids v]
   (disallow-tagging-a-built-in-entity db block-eids {:delete? true})
-  (disallow-node-cant-tag-with-private-tags db block-eids v {:delete? true}))
+  (disallow-node-cant-tag-with-private-tags db block-eids v {:delete? true})
+  (disallow-removing-page-tag db block-eids v))

+ 67 - 7
deps/outliner/test/logseq/outliner/validate_test.cljs

@@ -158,12 +158,21 @@
                                                         [(entity-plus/entity-memoized @conn :logseq.class/Cards)]))))))
 
 (deftest validate-tags-property
-  (let [conn (db-test/create-conn-with-blocks
-              {:classes {:SomeTag {}}
+  (let [class-uuid (random-uuid)
+        conn (db-test/create-conn-with-blocks
+              {:classes {:SomeTag {:block/uuid class-uuid :build/keep-uuid? true}}
                :pages-and-blocks
                [{:page {:block/title "page1"}
-                 :blocks [{:block/title "block"}]}]})
-        block (db-test/find-block-by-content @conn "block")]
+                 :blocks [{:block/title "block"
+                           :build/children [{:block/title "block - invalid location"}]}
+                          {:block/title "block / invalid title"}]}
+                {:page {:block/uuid class-uuid}
+                 :blocks [{:block/title "class block"}]}]
+               :build-existing-tx? true})
+        block (db-test/find-block-by-content @conn "block")
+        block-invalid-title (db-test/find-block-by-content @conn #"invalid title")
+        block-invalid-location (db-test/find-block-by-content @conn #"invalid location")
+        block-invalid-in-class (db-test/find-block-by-content @conn "class block")]
 
     (is (thrown-with-msg?
          js/Error
@@ -185,15 +194,66 @@
 
     (is (thrown-with-msg?
          js/Error
-         #"Can't set tag.*Page"
-         (outliner-validate/validate-tags-property @conn [(:db/id block)] :logseq.class/Page))
+         #"Can't set tag.*Tag"
+         (outliner-validate/validate-tags-property @conn [(:db/id block)] :logseq.class/Tag))
         "Nodes can't be tagged with built-in private tags")
 
     (is (thrown-with-msg?
          js/Error
          #"Can't set tag.*Priority"
          (outliner-validate/validate-tags-property @conn [(:db/id block)] :logseq.property/priority))
-        "Nodes can't be tagged with built-in non tags")))
+        "Nodes can't be tagged with built-in non tags")
+
+    (is (nil? (outliner-validate/validate-tags-property @conn [(:db/id block)] :logseq.class/Page))
+        "Blocks can be tagged with #Page")
+
+    (is (thrown-with-msg?
+         js/Error
+         #"Page name can't.*/"
+         (outliner-validate/validate-tags-property @conn [(:db/id block-invalid-title)] :logseq.class/Page))
+        "Block with invalid title can't be tagged with #Page")
+
+    (is (thrown-with-msg?
+         js/Error
+         #"Can't convert this block to page"
+         (outliner-validate/validate-tags-property @conn [(:db/id block-invalid-location)] :logseq.class/Page))
+        "Block with invalid location can't be tagged with #Page")
+
+    (is (thrown-with-msg?
+         js/Error
+         #"Can't convert this block to page"
+         (outliner-validate/validate-tags-property @conn [(:db/id block-invalid-in-class)] :logseq.class/Page))
+        "Block in class or property can't be tagged with #Page")))
+
+(deftest validate-tags-property-deletion
+  (let [conn (db-test/create-conn-with-blocks
+              {:classes {:SomeTag {}}
+               :pages-and-blocks
+               [{:page {:block/title "page1"}
+                 :blocks [{:block/title "block" :build/tags [:logseq.class/Page]}]}]})
+        page (db-test/find-page-by-title @conn "page1")
+        page-with-parent (db-test/find-block-by-content @conn "block")]
+
+    (is (thrown-with-msg?
+         js/Error
+         #"Can't remove tag.*Task"
+         (outliner-validate/validate-tags-property-deletion @conn [(:db/id (d/entity @conn :logseq.class/Task))] :logseq.class/Tag))
+        "built-in class must not have tag deleted by the user")
+
+    (is (thrown-with-msg?
+         js/Error
+         #"Can't remove tag.*Tag"
+         (outliner-validate/validate-tags-property-deletion @conn [(:db/id (d/entity @conn :user.class/SomeTag))] :logseq.class/Tag))
+        "Node can't have private tag deleted by user")
+
+    (is (nil? (outliner-validate/validate-tags-property-deletion @conn [(:db/id page-with-parent)] :logseq.class/Page))
+        "Page with parent can remove #Page")
+
+    (is (thrown-with-msg?
+         js/Error
+         #"This page cannot be converted"
+         (outliner-validate/validate-tags-property-deletion @conn [(:db/id page)] :logseq.class/Page))
+        "Page without parent can't remove #Page")))
 
 ;; Try as many of the validations against a new graph to confirm
 ;; that validations make sense and are valid for a new graph

+ 56 - 21
src/main/frontend/components/cmdk/core.cljs

@@ -40,6 +40,10 @@
             [promesa.core :as p]
             [rum.core :as rum]))
 
+(defn- get-action
+  []
+  (:action (:search/args @state/state)))
+
 (defn translate [t {:keys [id desc]}]
   (when id
     (let [desc-i18n (t (shortcut-utils/decorate-namespace id))]
@@ -197,8 +201,10 @@
                (first))))
 
 (defn state->action [state]
-  (let [highlighted-item (state->highlighted-item state)]
-    (cond (:source-page highlighted-item) :open
+  (let [highlighted-item (state->highlighted-item state)
+        action (get-action)]
+    (cond (and (:source-page highlighted-item) (= action :move-blocks)) :trigger
+          (:source-page highlighted-item) :open
           (:source-block highlighted-item) :open
           (:file-path highlighted-item) :open
           (:source-search highlighted-item) :search
@@ -327,7 +333,9 @@
         repo (state/get-current-repo)
         current-page (when-let [id (page-util/get-current-page-id)]
                        (db/entity id))
-        opts {:limit 100 :dev? config/dev? :built-in? true}]
+        opts (cond-> {:limit 100 :dev? config/dev? :built-in? true}
+               (contains? #{:move-blocks} (get-action))
+               (assoc :page-only? true))]
     (swap! !results assoc-in [group :status] :loading)
     (swap! !results assoc-in [:current-page :status] :loading)
     (p/let [blocks (search/block-search repo @!input opts)
@@ -548,12 +556,21 @@
       (reset! (::input state) search-query))))
 
 (defmethod handle-action :trigger [_ state _event]
-  (let [command (some-> state state->highlighted-item :source-command)
-        dont-close-commands #{:graph/open :graph/remove :dev/replace-graph-with-db-file :misc/import-edn-data}]
-    (when-let [action (:action command)]
+  (let [highlighted-item (some-> state state->highlighted-item)
+        command (:source-command highlighted-item)
+        dont-close-commands #{:graph/open :graph/remove :dev/replace-graph-with-db-file :misc/import-edn-data :editor/move-blocks}
+        search-args (:search/args @state/state)
+        action (or (:action command)
+                   (when-let [trigger (:trigger search-args)]
+                     #(trigger highlighted-item)))
+        input-ref @(::input-ref state)]
+    (when action
+      (when input-ref
+        (set! (.-value input-ref) "")
+        (.focus input-ref))
+      (action)
       (when-not (contains? dont-close-commands (:id command))
-        (shui/dialog-close! :ls-dialog-cmdk))
-      (util/schedule #(action) 32))))
+        (shui/dialog-close! :ls-dialog-cmdk)))))
 
 (defmethod handle-action :create [_ state _event]
   (let [item (state->highlighted-item state)
@@ -692,11 +709,13 @@
                              ;; for some reason, the highlight effect does not always trigger on a
                              ;; boolean value change so manually pass in the dep
                             :on-highlight-dep highlighted-item
-                            :on-click (fn [e]
-                                        (reset! (::highlighted-item state) item)
-                                        (handle-action :default state item)
-                                        (when-let [on-click (:on-click item)]
-                                          (on-click e)))
+                            :on-click
+                            (fn [e]
+                              (util/stop-propagation e)
+                              (reset! (::highlighted-item state) item)
+                              (handle-action :default state item)
+                              (when-let [on-click (:on-click item)]
+                                (on-click e)))
                              ;; :on-mouse-enter (fn [e]
                              ;;                   (when (not highlighted?)
                              ;;                     (reset! (::highlighted-item state) (assoc item :mouse-enter-triggered-highlight true))))
@@ -805,12 +824,20 @@
       (and enter? (not composing?)) (do
                                       (handle-action :default state e)
                                       (util/stop-propagation e))
-      esc? (let [filter' @(::filter state)]
-             (if filter'
+      esc? (let [filter' @(::filter state)
+                 action (get-action)
+                 move-blocks? (= :move-blocks action)]
+             (cond
+               (and move-blocks? (string/blank? input))
+               (state/close-modal!)
+
+               (and filter' (not move-blocks?))
                (do
                  (util/stop e)
                  (reset! (::filter state) nil)
                  (load-results :default state))
+
+               :else
                (when-not (string/blank? input)
                  (util/stop e)
                  (handle-input-change state nil ""))))
@@ -831,12 +858,15 @@
 (defn- input-placeholder
   [sidebar?]
   (let [search-mode (:search/mode @state/state)
-        search-args (:search/args @state/state)]
+        action (get-action)]
     (cond
+      (= action :move-blocks)
+      "Move blocks to"
+
       (and (= search-mode :graph) (not sidebar?))
       "Add graph filter"
 
-      (= search-args :new-page)
+      (= action :new-page)
       "Type a page name to create"
 
       :else
@@ -1000,8 +1030,10 @@
        (shortcut/listen-all!))
      state)}
   {:init (fn [state]
-           (let [search-mode (:search/mode @state/state)
+           (let [search-mode (or (:search/mode @state/state) :global)
                  opts (last (:rum/args state))]
+             (when (nil? search-mode)
+               (state/set-state! :search/mode :global))
              (assoc state
                     ::ref (atom nil)
                     ::filter (if (and search-mode
@@ -1032,8 +1064,10 @@
   (rum/local false ::input-changed?)
   [state {:keys [sidebar?] :as opts}]
   (let [*input (::input state)
-        search-mode (:search/mode @state/state)
-        group-filter (:group (rum/react (::filter state)))
+        search-mode (state/sub :search/mode)
+        group-filter (or (when (and (not (contains? #{:global :graph} search-mode)) (not (:sidebar? opts)))
+                           search-mode)
+                         (:group (rum/react (::filter state))))
         results-ordered (state->results-ordered state search-mode)
         all-items (mapcat last results-ordered)
         first-item (first all-items)]
@@ -1072,7 +1106,8 @@
      (when-not sidebar? (hints state))]))
 
 (rum/defc cmdk-modal [props]
-  [:div {:class "cp__cmdk__modal rounded-lg w-[90dvw] max-w-4xl relative"}
+  [:div {:class "cp__cmdk__modal rounded-lg w-[90dvw] max-w-4xl relative"
+         :data-keep-selection true}
    (cmdk props)])
 
 (rum/defc cmdk-block [props]

+ 2 - 1
src/main/frontend/components/container.cljs

@@ -1019,7 +1019,8 @@
        :on-pointer-up (fn []
                         (when-let [container (gdom/getElement "app-container-wrapper")]
                           (d/remove-class! container "blocks-selection-mode")
-                          (when (> (count (state/get-selection-blocks)) 1)
+                          (when (and (> (count (state/get-selection-blocks)) 1)
+                                     (not (util/input? js/document.activeElement)))
                             (util/clear-selection!))))}
 
       [:button#skip-to-main

+ 2 - 1
src/main/frontend/components/header.cljs

@@ -403,7 +403,8 @@
          (when current-repo
            (ui/with-shortcut :go/search "right"
              [:button.button.icon#search-button
-              {:title (t :header/search)
+              {:data-keep-selection true
+               :title (t :header/search)
                :on-click #(do (when (or (mobile-util/native-android?)
                                         (mobile-util/native-iphone?))
                                 (state/set-left-sidebar-open! false))

+ 2 - 7
src/main/frontend/components/library.cljs

@@ -7,7 +7,6 @@
             [frontend.search :as search]
             [frontend.state :as state]
             [frontend.ui :as ui]
-            [logseq.db :as ldb]
             [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
@@ -39,12 +38,8 @@
       :selected-choices selected-choices
       :on-chosen (fn [chosen selected?]
                    (if selected?
-                     (let [last-child (->> (:block/_parent (db/entity (:db/id library-page)))
-                                           ldb/sort-by-order
-                                           last)
-                           target (or last-child library-page)
-                           chosen-block (db/entity chosen)]
-                       (editor-handler/move-blocks! [chosen-block] target (if last-child true false))
+                     (let [chosen-block (db/entity chosen)]
+                       (editor-handler/move-blocks! [chosen-block] library-page {:bottom? true})
                        (set-selected-choices! (conj selected-choices chosen)))
                      (do
                        (db/transact! (state/get-current-repo)

+ 2 - 0
src/main/frontend/components/property/dialog.cljs

@@ -14,6 +14,7 @@
                  k (:property-key opts)]
              (when-let [view-selected-blocks (:selected-blocks opts)]
                (state/set-state! :view/selected-blocks view-selected-blocks))
+             (state/set-state! :ui/show-property-dialog? true)
              (assoc state
                     ::property-key (atom k)
                     ::property (atom (when k (db/get-case-page k))))))
@@ -21,6 +22,7 @@
                    (when-let [close-fn (:on-dialog-close (last (:rum/args state)))]
                      (close-fn))
                    (state/set-state! :view/selected-blocks nil)
+                   (state/set-state! :ui/show-property-dialog? false)
                    state)}
   [state blocks opts]
   (when (seq blocks)

+ 20 - 14
src/main/frontend/components/property/value.cljs

@@ -249,8 +249,7 @@
        (p/do!
         (ui-outliner-tx/transact!
          {:outliner-op :save-block}
-         (doseq [block blocks]
-           (db-property-handler/delete-property-value! (:db/id block) (:db/ident property) value)))
+         (db-property-handler/batch-delete-property-value! (map :db/id blocks) (:db/ident property) value))
         (when (or (not many?)
                   ;; values will be cleared
                   (and many? (<= (count (get block (:db/ident property))) 1)))
@@ -698,10 +697,13 @@
                   excluded-options)
 
                 (contains? #{:class :property} property-type)
-                (let [classes (model/get-all-classes
-                               repo
-                               {:except-root-class? true
-                                :except-private-tags? (not (contains? #{:logseq.property/template-applied-to} (:db/ident property)))})]
+                (let [classes (cond->
+                               (model/get-all-classes
+                                repo
+                                {:except-root-class? true
+                                 :except-private-tags? (not (contains? #{:logseq.property/template-applied-to} (:db/ident property)))})
+                                (not (or (and (entity-util/page? block) (not (ldb/internal-page? block))) (:logseq.property/created-from-property block)))
+                                (conj (db/entity :logseq.class/Page)))]
                   (if (= property-type :class)
                     classes
                     (property-handler/get-class-property-choices)))
@@ -771,7 +773,8 @@
                                 :label label
                                 :value id
                                 :disabled? (and tags? (contains?
-                                                       (set/union #{:logseq.class/Journal :logseq.class/Whiteboard} ldb/internal-tags)
+                                                       (set/union #{:logseq.class/Journal :logseq.class/Whiteboard}
+                                                                  (set/difference ldb/internal-tags #{:logseq.class/Page}))
                                                        (:db/ident node)))))) nodes)
         classes' (remove (fn [class] (= :logseq.class/Root (:db/ident class))) classes)
         opts' (cond->
@@ -788,13 +791,12 @@
                                               "Set alias"
                                               :else
                                               (str "Set " (:block/title property)))
-                 :show-new-when-not-exact-match? (if (or extends-property?
-                                                         ;; Don't allow creating private tags
-                                                         (and (= :block/tags (:db/ident property))
-                                                              (seq (set/intersection (set (map :db/ident classes'))
-                                                                                     ldb/private-tags))))
-                                                   false
-                                                   true)
+                 :show-new-when-not-exact-match? (not
+                                                  (or (and extends-property? (contains? (set children-pages) (:db/id block)))
+                                                      ;; Don't allow creating private tags
+                                                      (and (= :block/tags (:db/ident property))
+                                                           (seq (set/intersection (set (map :db/ident classes'))
+                                                                                  ldb/private-tags)))))
                  :extract-chosen-fn :value
                  :extract-fn (fn [x] (or (:label-value x) (:label x)))
                  :input-opts input-opts
@@ -825,6 +827,10 @@
                                   (when-not add-tag-property?
                                     (log/error :msg "No :db/id found or created for chosen" :chosen chosen)))))})
 
+                (= :block/tags (:db/ident property))
+                (assoc :exact-match-exclude-items
+                       (set (map (fn [ident] (:block/title (db/entity ident))) ldb/private-tags)))
+
                 (and (seq classes') (not tags-or-alias?))
                 (assoc
                   ;; Provides additional completion for inline classes on new pages or objects

+ 1 - 0
src/main/frontend/components/select.cljs

@@ -172,6 +172,7 @@
                                                                      (render-item result chosen? multiple-choices? *selected-choices)))
                                     :class             "cp__select-results"
                                     :on-chosen         (fn [raw-chosen e]
+                                                         (util/stop-propagation e)
                                                          (when clear-input-on-chosen?
                                                            (reset! *input ""))
                                                          (let [chosen (extract-chosen-fn raw-chosen)]

+ 68 - 66
src/main/frontend/components/selection.cljs

@@ -9,79 +9,81 @@
             [logseq.shui.ui :as shui]
             [rum.core :as rum]))
 
-(rum/defc action-bar
+(rum/defc action-bar < rum/reactive
   [& {:keys [on-cut on-copy selected-blocks hide-dots? button-border?]
       :or {on-cut #(editor-handler/cut-selection-blocks true)}}]
-  (let [selected-blocks (map (fn [block] (if (number? block) (db/entity block) block)) selected-blocks)
-        on-copy (if (and selected-blocks (nil? on-copy))
-                  #(editor-handler/copy-selection-blocks true {:selected-blocks selected-blocks})
-                  (or on-copy #(editor-handler/copy-selection-blocks true)))
-        button-opts {:variant :outline
-                     :size :sm
-                     :class (str "p-2 text-xs h-8"
-                                 (when-not button-border?
-                                   " !border-b-0"))}
-        db-graph? (config/db-based-graph?)]
-    [:div.selection-action-bar
-     (shui/button-group
-      ;; set tag
-      (when db-graph?
+  (when-not (or (state/sub :search/mode)
+                (state/sub :ui/show-property-dialog?))
+    (let [selected-blocks (map (fn [block] (if (number? block) (db/entity block) block)) selected-blocks)
+          on-copy (if (and selected-blocks (nil? on-copy))
+                    #(editor-handler/copy-selection-blocks true {:selected-blocks selected-blocks})
+                    (or on-copy #(editor-handler/copy-selection-blocks true)))
+          button-opts {:variant :outline
+                       :size :sm
+                       :class (str "p-2 text-xs h-8"
+                                   (when-not button-border?
+                                     " !border-b-0"))}
+          db-graph? (config/db-based-graph?)]
+      [:div.selection-action-bar
+       (shui/button-group
+        ;; set tag
+        (when db-graph?
+          (shui/button
+           (assoc button-opts
+                  :on-pointer-down (fn [e]
+                                     (util/stop e)
+                                     (state/pub-event! [:editor/new-property {:target (.-target e)
+                                                                              :selected-blocks selected-blocks
+                                                                              :property-key "Tags"
+                                                                              :on-dialog-close #(state/pub-event! [:editor/hide-action-bar])}])))
+           (ui/tooltip (ui/icon "hash" {:size 13}) "Set tag"
+                       {:trigger-props {:class "flex"}})))
         (shui/button
          (assoc button-opts
                 :on-pointer-down (fn [e]
                                    (util/stop e)
-                                   (state/pub-event! [:editor/new-property {:target (.-target e)
-                                                                            :selected-blocks selected-blocks
-                                                                            :property-key "Tags"
-                                                                            :on-dialog-close #(state/pub-event! [:editor/hide-action-bar])}])))
-         (ui/tooltip (ui/icon "hash" {:size 13}) "Set tag"
-                     {:trigger-props {:class "flex"}})))
-      (shui/button
-       (assoc button-opts
-              :on-pointer-down (fn [e]
-                                 (util/stop e)
-                                 (on-copy)
-                                 (state/clear-selection!)
-                                 (state/pub-event! [:editor/hide-action-bar])))
-       "Copy")
-      (when db-graph?
+                                   (on-copy)
+                                   (state/clear-selection!)
+                                   (state/pub-event! [:editor/hide-action-bar])))
+         "Copy")
+        (when db-graph?
+          (shui/button
+           (assoc button-opts
+                  :on-pointer-down (fn [e]
+                                     (util/stop e)
+                                     (state/pub-event! [:editor/new-property {:target (.-target e)
+                                                                              :selected-blocks selected-blocks
+                                                                              :on-dialog-close #(state/pub-event! [:editor/hide-action-bar])}])))
+           "Set property"))
+        (when db-graph?
+          (shui/button
+           (assoc button-opts
+                  :on-pointer-down (fn [e]
+                                     (util/stop e)
+                                     (state/pub-event! [:editor/new-property {:target (.-target e)
+                                                                              :selected-blocks selected-blocks
+                                                                              :remove-property? true
+                                                                              :select-opts {:show-new-when-not-exact-match? false}
+                                                                              :on-dialog-close #(state/pub-event! [:editor/hide-action-bar])}])))
+           "Unset property"))
         (shui/button
          (assoc button-opts
                 :on-pointer-down (fn [e]
                                    (util/stop e)
-                                   (state/pub-event! [:editor/new-property {:target (.-target e)
-                                                                            :selected-blocks selected-blocks
-                                                                            :on-dialog-close #(state/pub-event! [:editor/hide-action-bar])}])))
-         "Set property"))
-      (when db-graph?
-        (shui/button
-         (assoc button-opts
-                :on-pointer-down (fn [e]
-                                   (util/stop e)
-                                   (state/pub-event! [:editor/new-property {:target (.-target e)
-                                                                            :selected-blocks selected-blocks
-                                                                            :remove-property? true
-                                                                            :select-opts {:show-new-when-not-exact-match? false}
-                                                                            :on-dialog-close #(state/pub-event! [:editor/hide-action-bar])}])))
-         "Unset property"))
-      (shui/button
-       (assoc button-opts
-              :on-pointer-down (fn [e]
-                                 (util/stop e)
-                                 (on-cut)
-                                 (state/pub-event! [:editor/hide-action-bar])))
-       (ui/icon "trash" {:size 13}))
-      (when-not hide-dots?
-        (shui/button
-         (assoc button-opts
-                :on-pointer-down (fn [e]
-                                   (util/stop e)
-                                   (shui/popup-hide!)
-                                   (shui/popup-show! e
-                                                     (fn [{:keys [id]}]
-                                                       [:div {:on-click #(shui/popup-hide! id)
-                                                              :data-keep-selection true}
-                                                        ((state/get-component :selection/context-menu))])
-                                                     {:content-props {:class "w-[280px] ls-context-menu-content"}
-                                                      :as-dropdown? true})))
-         (ui/icon "dots" {:size 13}))))]))
+                                   (on-cut)
+                                   (state/pub-event! [:editor/hide-action-bar])))
+         (ui/icon "trash" {:size 13}))
+        (when-not hide-dots?
+          (shui/button
+           (assoc button-opts
+                  :on-pointer-down (fn [e]
+                                     (util/stop e)
+                                     (shui/popup-hide!)
+                                     (shui/popup-show! e
+                                                       (fn [{:keys [id]}]
+                                                         [:div {:on-click #(shui/popup-hide! id)
+                                                                :data-keep-selection true}
+                                                          ((state/get-component :selection/context-menu))])
+                                                       {:content-props {:class "w-[280px] ls-context-menu-content"}
+                                                        :as-dropdown? true})))
+           (ui/icon "dots" {:size 13}))))])))

+ 6 - 0
src/main/frontend/handler/db_based/property.cljs

@@ -37,6 +37,12 @@
    {:outliner-op :delete-property-value}
    (outliner-op/delete-property-value! block-id property-id property-value)))
 
+(defn batch-delete-property-value!
+  [block-ids property-id property-value]
+  (ui-outliner-tx/transact!
+   {:outliner-op :batch-delete-property-value}
+   (outliner-op/batch-delete-property-value! block-ids property-id property-value)))
+
 (defn create-property-text-block!
   [block-id property-id value opts]
   (ui-outliner-tx/transact!

+ 5 - 4
src/main/frontend/handler/dnd.cljs

@@ -59,12 +59,13 @@
                     (:block/uuid (ldb/get-left-sibling target-block)))]
              (if first-child?
                (when-let [parent (:block/parent target-block)]
-                 (outliner-op/move-blocks! blocks' parent false))
+                 (outliner-op/move-blocks! blocks' parent {:sibling? false}))
                (if-let [before-node (ldb/get-left-sibling target-block)]
-                 (outliner-op/move-blocks! blocks' before-node true)
+                 (outliner-op/move-blocks! blocks' before-node {:sibling? true})
                  (when-let [parent (:block/parent target-block)]
-                   (outliner-op/move-blocks! blocks' parent false)))))
-           (outliner-op/move-blocks! blocks' target-block (not nested?)))))
+                   (outliner-op/move-blocks! blocks' parent {:sibling? false})))))
+           (outliner-op/move-blocks! blocks' target-block
+                                     {:sibling? (not nested?)}))))
 
       :else
       nil)))

+ 46 - 18
src/main/frontend/handler/editor.cljs

@@ -860,7 +860,7 @@
                        (ui-outliner-tx/transact!
                         transact-opts
                         (when (seq children)
-                          (outliner-op/move-blocks! children prev-block false))
+                          (outliner-op/move-blocks! children prev-block {:sibling? false}))
                         (delete-block-aux! block)
                         (save-block! repo prev-block new-content {})))))
 
@@ -870,11 +870,29 @@
                    (delete-block-aux! block)))))))))))
 
 (defn move-blocks!
-  [blocks target sibling?]
+  [blocks target opts]
   (when (seq blocks)
     (ui-outliner-tx/transact!
      {:outliner-op :move-blocks}
-     (outliner-op/move-blocks! blocks target sibling?))))
+     (outliner-op/move-blocks! blocks target opts))))
+
+(defn move-selected-blocks
+  [e]
+  (util/stop e)
+  (let [block-ids (or (seq (state/get-selection-block-ids))
+                      (when-let [id (:block/uuid (state/get-edit-block))]
+                        [id]))]
+    (if (seq block-ids)
+      (let [blocks (->> (map (fn [id] (db/entity [:block/uuid id])) block-ids)
+                        block-handler/get-top-level-blocks)]
+        (route-handler/go-to-search! :nodes
+                                     {:action :move-blocks
+                                      :blocks blocks
+                                      :trigger (fn [chosen]
+                                                 (state/pub-event! [:editor/hide-action-bar])
+                                                 (state/clear-selection!)
+                                                 (move-blocks! blocks (:source-page chosen) {:bottom? true}))}))
+      (notification/show! "There's no block selected, please select blocks first." :warning))))
 
 (defn delete-block!
   [repo]
@@ -1284,6 +1302,10 @@
   (some->> (shui-popup/get-popups)
            (some #(some-> % (:id) (str) (string/includes? (str id))))))
 
+(defn dialog-exists?
+  [id]
+  (shui-dialog/get-modal id))
+
 (defn show-action-bar!
   [& {:keys [delay]
       :or {delay 200}}]
@@ -1644,7 +1666,12 @@
 (defn get-matched-classes
   "Return matched classes except the root tag"
   [q]
-  (let [classes (->> (db-model/get-all-classes (state/get-current-repo) {:except-root-class? true})
+  (let [editing-block (some-> (state/get-edit-block) :db/id db/entity)
+        non-page-block? (and editing-block (not (ldb/page? editing-block)))
+        all-classes (cond-> (db-model/get-all-classes (state/get-current-repo) {:except-root-class? true})
+                      non-page-block?
+                      (conj (db/entity :logseq.class/Page)))
+        classes (->> all-classes
                      (mapcat (fn [class]
                                (conj (:block/alias class) class)))
                      (common-util/distinct-by :db/id)
@@ -3340,18 +3367,19 @@
 
 (defn open-selected-block!
   [direction e]
-  (let [selected-blocks (state/get-selection-blocks)
-        f (case direction :left first :right last)
-        node (some-> selected-blocks f)]
-    (if (some-> node (dom/has-class? "block-add-button"))
-      (.click node)
-      (when-let [block-id (some-> node (dom/attr "blockid") uuid)]
-        (util/stop e)
-        (let [block {:block/uuid block-id}
-              left? (= direction :left)
-              opts {:container-id (some-> node (dom/attr "containerid") (parse-long))
-                    :event e}]
-          (edit-block! block (if left? 0 :max) opts))))))
+  (when-not (auto-complete?)
+    (let [selected-blocks (state/get-selection-blocks)
+          f (case direction :left first :right last)
+          node (some-> selected-blocks f)]
+      (if (some-> node (dom/has-class? "block-add-button"))
+        (.click node)
+        (when-let [block-id (some-> node (dom/attr "blockid") uuid)]
+          (util/stop e)
+          (let [block {:block/uuid block-id}
+                left? (= direction :left)
+                opts {:container-id (some-> node (dom/attr "containerid") (parse-long))
+                      :event e}]
+            (edit-block! block (if left? 0 :max) opts)))))))
 
 (defn shortcut-left-right [direction]
   (fn [e]
@@ -3960,8 +3988,8 @@
         (p/do!
          (when (seq children)
            (if-let [today-last-child (last (ldb/sort-by-order (:block/_parent today)))]
-             (move-blocks! children today-last-child true)
-             (move-blocks! children today false)))
+             (move-blocks! children today-last-child {:sibling? true})
+             (move-blocks! children today {:sibling? false})))
          (state/close-modal!)
          (mobile-state/set-popup! nil)
          (when (seq children)

+ 9 - 9
src/main/frontend/handler/events/ui.cljs

@@ -44,13 +44,14 @@
             [promesa.core :as p]))
 
 (defmethod events/handle :go/search [_]
-  (shui/dialog-open!
-   cmdk/cmdk-modal
-   {:id :ls-dialog-cmdk
-    :align :top
-    :content-props {:class "ls-dialog-cmdk"}
-    :close-btn? false
-    :onEscapeKeyDown (fn [e] (.preventDefault e))}))
+  (when-not (editor-handler/dialog-exists? :ls-dialog-cmdk)
+    (shui/dialog-open!
+     cmdk/cmdk-modal
+     {:id :ls-dialog-cmdk
+      :align :top
+      :content-props {:class "ls-dialog-cmdk"}
+      :close-btn? false
+      :onEscapeKeyDown (fn [e] (.preventDefault e))})))
 
 (defmethod events/handle :command/run [_]
   (when (util/electron?)
@@ -233,8 +234,7 @@
         (if target'
           (shui/popup-show! target'
                             #(property-dialog/dialog blocks opts')
-                            {:align "start"
-                             :auto-focus? true})
+                            {:align "start"})
           (shui/dialog-open! #(property-dialog/dialog blocks opts')
                              {:id :property-dialog
                               :align "start"}))))))

+ 3 - 3
src/main/frontend/log.cljs

@@ -1,8 +1,8 @@
 (ns frontend.log
   "System-component-like ns that encapsulates logging functionality"
-  (:require [lambdaisland.glogi :as log]
-            [lambdaisland.glogi.console :as glogi-console]
-            [frontend.config :as config]))
+  (:require [frontend.config :as config]
+            [lambdaisland.glogi :as log]
+            [lambdaisland.glogi.console :as glogi-console]))
 
 ;; TODO: Move code below into a fn to behave like a system component
 ;; instead of having no control over its behavior at require time

+ 7 - 2
src/main/frontend/modules/outliner/op.cljs

@@ -35,11 +35,11 @@
        [:delete-blocks [ids opts]]))))
 
 (defn move-blocks!
-  [blocks target-block sibling?]
+  [blocks target-block opts]
   (op-transact!
    (let [ids (map :db/id blocks)
          target-id (:db/id target-block)]
-     [:move-blocks [ids target-id sibling?]])))
+     [:move-blocks [ids target-id opts]])))
 
 (defn move-blocks-up-down!
   [blocks up?]
@@ -73,6 +73,11 @@
   (op-transact!
    [:delete-property-value [block-eid property-id property-value]]))
 
+(defn batch-delete-property-value!
+  [block-eids property-id property-value]
+  (op-transact!
+   [:batch-delete-property-value [block-eids property-id property-value]]))
+
 (defn create-property-text-block!
   [block-id property-id value opts]
   (op-transact!

+ 12 - 1
src/main/frontend/modules/shortcut/config.cljs

@@ -290,6 +290,8 @@
 
    :editor/move-block-down                  {:binding (if mac? "mod+shift+down" "alt+shift+down")
                                              :fn      (editor-handler/move-up-down false)}
+   :editor/move-blocks                      {:binding "mod+shift+m"
+                                             :fn      editor-handler/move-selected-blocks}
 
    :editor/open-edit                        {:binding "enter"
                                              :fn      (fn [e]
@@ -365,6 +367,11 @@
                                              :fn      (fn [e]
                                                         (when e (util/stop e))
                                                         (state/pub-event! [:editor/new-property {}]))}
+   :editor/set-tags                         {:binding "p t"
+                                             :db-graph? true
+                                             :selection? true
+                                             :fn      (fn []
+                                                        (state/pub-event! [:editor/new-property {:property-key "Tags"}]))}
 
    :editor/add-property-deadline            {:binding "p d"
                                              :db-graph? true
@@ -390,7 +397,7 @@
                                              :fn      (fn []
                                                         (state/pub-event! [:editor/new-property {:property-key "Icon"}]))}
 
-   :editor/toggle-display-all-properties    {:binding "p t"
+   :editor/toggle-display-all-properties    {:binding "p a"
                                              :db-graph? true
                                              :fn      ui-handler/toggle-show-empty-hidden-properties!}
 
@@ -792,6 +799,7 @@
           :editor/select-down
           :editor/move-block-up
           :editor/move-block-down
+          :editor/move-blocks
           :editor/open-edit
           :editor/open-selected-blocks-in-sidebar
           :editor/select-block-up
@@ -862,6 +870,7 @@
           :editor/copy-current-file
           :editor/copy-page-url
           :editor/new-whiteboard
+          :editor/set-tags
           :editor/add-property-deadline
           :editor/add-property-status
           :editor/add-property-priority
@@ -968,6 +977,7 @@
      :editor/open-link-in-sidebar
      :editor/move-block-up
      :editor/move-block-down
+     :editor/move-blocks
      :editor/escape-editing]
 
     :shortcut.category/block-command-editing
@@ -996,6 +1006,7 @@
      :editor/select-block-down
      :editor/delete-selection
      :editor/add-property
+     :editor/set-tags
      :editor/add-property-deadline
      :editor/add-property-status
      :editor/add-property-priority

+ 1 - 0
src/main/frontend/state.cljs

@@ -103,6 +103,7 @@
 
       ;; ui
       :ui/viewport                           {}
+      :ui/show-property-dialog?              (atom false)
 
       ;; left sidebar
       :ui/navigation-item-collapsed?         {}

+ 2 - 1
src/main/frontend/ui.cljs

@@ -295,7 +295,8 @@
               (icon "info-circle" {:class "text-indigo-500" :size "20"}))
             status)]
       [:div.ui__notifications-content
-       {:style
+       {:class (str "notification-" (name (or status :info)))
+        :style
         (when (or (= state "exiting")
                   (= state "exited"))
           {:z-index -1})}

+ 2 - 1
src/main/frontend/worker/handler/page.cljs

@@ -21,7 +21,8 @@
                                                    {:page-uuid uuid
                                                     :skip-existing-page-check? true})
         result            (ldb/transact! conn [page] {:persist-op? false
-                                                      :outliner-op :create-page})]
+                                                      :outliner-op :create-page
+                                                      :rtc-op? true})]
     [result page-name (:block/uuid page)]))
 
 (defn create!

+ 86 - 7
src/main/frontend/worker/pipeline.cljs

@@ -12,6 +12,7 @@
             [logseq.common.util :as common-util]
             [logseq.common.uuid :as common-uuid]
             [logseq.db :as ldb]
+            [logseq.db.common.order :as db-order]
             [logseq.db.common.sqlite :as common-sqlite]
             [logseq.db.frontend.validate :as db-validate]
             [logseq.db.sqlite.export :as sqlite-export]
@@ -126,6 +127,62 @@
               (assert (= (count (distinct (map :block/order children))) (count children))
                       (str ":block/order is not unique for children blocks, parent id: " (:db/id parent))))))))))
 
+(defn- toggle-page-and-block
+  [conn {:keys [db-before db-after tx-data tx-meta]}]
+  (when-not (:rtc-op? tx-meta)
+    (let [page-tag (d/entity @conn :logseq.class/Page)
+          library-page (ldb/get-library-page db-after)]
+      (mapcat
+       (fn [datom]
+         (let [id (:e datom)
+               page-tag-update? (and (= :block/tags (:a datom))
+                                     (= (:db/id page-tag) (:v datom)))
+               move-to-library? (and (= :block/parent (:a datom))
+                                     (= (:db/id library-page) (:v datom))
+                                     (:added datom))]
+           (when (or page-tag-update? move-to-library?)
+             (let [block-before (d/entity db-before id)
+                   block-after (d/entity db-after id)]
+               (when block-after
+                 (cond
+                   ;; move non-page block to Library
+                   (and move-to-library? (not (ldb/page? block-after)))
+                   [{:db/id id
+                     :block/name (common-util/page-name-sanity-lc (:block/title block-after))
+                     :block/tags :logseq.class/Page}
+                    [:db/retract id :block/page]]
+
+                   ;; block->page
+                   (and (:added datom) block-before (not (ldb/page? block-before))) ; block->page
+                   (let [->page-tx [{:db/id id
+                                     :block/name (common-util/page-name-sanity-lc (:block/title block-after))}
+                                    [:db/retract id :block/page]]
+                         move-parent-to-library-tx (let [block (d/entity db-after (:e datom))
+                                                         block-parent (:block/parent block)]
+                                                     (assert (ldb/page? block-parent))
+                                                     (when (and (nil? (:block/parent block-parent))
+                                                                block-parent
+                                                                (not= (:db/id block-parent) (:db/id library-page))
+                                                                (not (:db/ident block-parent))
+                                                                (not (ldb/built-in? block-parent)))
+                                                       [{:db/id (:db/id block-parent)
+                                                         :block/parent (:db/id (ldb/get-library-page db-after))
+                                                         :block/order (db-order/gen-key)}]))]
+                     (concat ->page-tx move-parent-to-library-tx))
+
+                   ;; page->block
+                   (and block-before (not (:added datom)) (ldb/internal-page? block-before))
+                   (let [parent (:block/parent block-before)
+                         parent-page (when parent
+                                       (loop [parent parent]
+                                         (if (ldb/page? parent)
+                                           parent
+                                           (recur (:block/parent parent)))))]
+                     (when parent-page
+                       [[:db/retract id :block/name]
+                        [:db/add id :block/page (:db/id parent-page)]]))))))))
+       tx-data))))
+
 (defn- add-missing-properties-to-typed-display-blocks
   "Add missing properties for these cases:
   1. Add corresponding tag when invoking commands like /code block.
@@ -220,22 +277,44 @@
 (defn- compute-extra-tx-data
   [repo conn tx-report]
   (let [{:keys [db-before db-after tx-data tx-meta]} tx-report
+        toggle-page-and-block-tx-data (toggle-page-and-block conn tx-report)
         display-blocks-tx-data (add-missing-properties-to-typed-display-blocks db-after tx-data)
         commands-tx (when-not (or (:undo? tx-meta) (:redo? tx-meta) (:rtc-tx? tx-meta))
                       (commands/run-commands conn tx-report))
         insert-templates-tx (insert-tag-templates repo tx-report)
         created-by-tx (add-created-by-ref-hook db-before db-after tx-data tx-meta)]
-    (concat display-blocks-tx-data commands-tx insert-templates-tx created-by-tx)))
+    (concat toggle-page-and-block-tx-data display-blocks-tx-data commands-tx insert-templates-tx created-by-tx)))
+
+(defn- undo-tx-data-if-disallowed!
+  [conn {:keys [tx-data]}]
+  (let [db @conn
+        page-has-block-parent? (some (fn [d] (and (:added d)
+                                                  (= :block/parent (:a d))
+                                                  (ldb/page? (d/entity db (:e d)))
+                                                  (not (ldb/page? (d/entity db (:v d)))))) tx-data)]
+    ;; TODO: add other cases that need to be undo
+    (when page-has-block-parent?
+      (let [reversed-tx-data (map (fn [[e a v _tx add?]]
+                                    (let [op (if add? :db/retract :db/add)]
+                                      [op e a v])) tx-data)]
+        (d/transact! conn reversed-tx-data {:op :undo-tx-data}))
+      (throw (ex-info "Page can't have block as parent"
+                      {:type :notification
+                       :payload {:message "Page can't have block as parent"
+                                 :type :warning}
+                       :tx-data tx-data})))))
 
 (defn- invoke-hooks-default
   [repo conn {:keys [tx-meta] :as tx-report} context]
+  ;; Notice: don't catch `undo-tx-data-if-disallowed!` since we want it failed immediately
+  (undo-tx-data-if-disallowed! conn tx-report)
   (try
-    (let [tx-before-refs (when (sqlite-util/db-based-graph? repo)
-                           (compute-extra-tx-data repo conn tx-report))
-          tx-report* (if (seq tx-before-refs)
-                       (let [result (ldb/transact! conn tx-before-refs {:pipeline-replace? true
-                                                                        :outliner-op :pre-hook-invoke
-                                                                        :skip-store? true})]
+    (let [extra-tx-data (when (sqlite-util/db-based-graph? repo)
+                          (compute-extra-tx-data repo conn tx-report))
+          tx-report* (if (seq extra-tx-data)
+                       (let [result (ldb/transact! conn extra-tx-data {:pipeline-replace? true
+                                                                       :outliner-op :pre-hook-invoke
+                                                                       :skip-store? true})]
                          (assoc tx-report
                                 :tx-data (concat (:tx-data tx-report) (:tx-data result))
                                 :db-after (:db-after result)))

+ 16 - 11
src/main/frontend/worker/rtc/remote_update.cljs

@@ -82,7 +82,8 @@ so need to pull earlier remote-data from websocket."})
                   nil sorted-order+block-uuid-coll)]
           (index/generate-key-between start-order end-order)
           block-order)]
-    (ldb/transact! conn [{:block/uuid block-uuid :block/order block-order*}])
+    (ldb/transact! conn [{:block/uuid block-uuid :block/order block-order*}]
+                   {:rtc-op? true})
     ;; TODO: add ops when block-order* != block-order
     ))
 
@@ -113,7 +114,8 @@ so need to pull earlier remote-data from websocket."})
                            block-parent (assoc :block/parent [:block/uuid block-parent])))
                        block-uuid+parent-coll)
                  {:persist-op? false
-                  :gen-undo-ops? false}))
+                  :gen-undo-ops? false
+                  :rtc-op? true}))
 
 (defmethod transact-db! :save-block [_ & args]
   (outliner-tx/transact!
@@ -128,11 +130,13 @@ so need to pull earlier remote-data from websocket."})
   (ldb/transact! conn
                  (mapv (fn [block-uuid] [:db/retractEntity [:block/uuid block-uuid]]) block-uuids)
                  {:persist-op? false
-                  :gen-undo-ops? false}))
+                  :gen-undo-ops? false
+                  :rtc-op? true}))
 
 (defmethod transact-db! :upsert-whiteboard-block [_ conn blocks]
   (ldb/transact! conn blocks {:persist-op? false
-                              :gen-undo-ops? false}))
+                              :gen-undo-ops? false
+                              :rtc-op? true}))
 
 (defn- group-remote-remove-ops-by-whiteboard-block
   "return {true [<whiteboard-block-ops>], false [<other-ops>]}"
@@ -174,7 +178,7 @@ so need to pull earlier remote-data from websocket."})
         (when-let [b (d/entity @conn [:block/uuid block-uuid])]
           (when-let [target-b
                      (d/entity @conn (:db/id (:block/page (d/entity @conn [:block/uuid block-uuid]))))]
-            (transact-db! :move-blocks&persist-op repo conn [b] target-b false))))
+            (transact-db! :move-blocks&persist-op repo conn [b] target-b {:sibling? false}))))
       (doseq [block-uuid block-uuids-to-remove]
         (when-let [b (d/entity @conn [:block/uuid block-uuid])]
           (transact-db! :delete-blocks repo conn date-formatter [b] {}))))))
@@ -189,7 +193,7 @@ so need to pull earlier remote-data from websocket."})
       (case [whiteboard-page-block? (some? local-parent) (some? remote-block-order)]
         [false true true]
         (do (if move?
-              (transact-db! :move-blocks repo conn [b] local-parent false)
+              (transact-db! :move-blocks repo conn [b] local-parent {:sibling? false})
               (transact-db! :insert-blocks repo conn
                             [{:block/uuid block-uuid
                               :block/title ""}]
@@ -198,7 +202,8 @@ so need to pull earlier remote-data from websocket."})
 
         [false true false]
         (if move?
-          (transact-db! :move-blocks repo conn [b] local-parent false)
+          (transact-db! :move-blocks repo conn [b] local-parent
+                        {:sibling? false})
           (transact-db! :insert-no-order-blocks conn [[block-uuid first-remote-parent]]))
 
         ([true false false] [true false true] [true true false] [true true true])
@@ -494,15 +499,15 @@ so need to pull earlier remote-data from websocket."})
       (let [{update-block-order-tx-data :tx-data op-value :op-value} (update-block-order (:db/id ent) op-value)
             first-remote-parent (first parents)
             local-parent (d/entity @conn [:block/uuid first-remote-parent])
-            whiteboard-page-block? (ldb/whiteboard? local-parent)]
+            whiteboard-page-block? (ldb/whiteboard? local-parent)
+            tx-meta {:persist-op? false :gen-undo-ops? false :rtc-op? true}]
         (if whiteboard-page-block?
           (upsert-whiteboard-block repo conn op-value)
           (do (when-let [schema-tx-data (remote-op-value->schema-tx-data block-uuid op-value)]
-                (ldb/transact! conn schema-tx-data {:persist-op? false :gen-undo-ops? false}))
+                (ldb/transact! conn schema-tx-data tx-meta))
               (when-let [tx-data (seq (remote-op-value->tx-data @conn ent (dissoc op-value :client/schema)
                                                                 rtc-const/ignore-attrs-when-syncing))]
-                (ldb/transact! conn (concat tx-data update-block-order-tx-data)
-                               {:persist-op? false :gen-undo-ops? false}))))))))
+                (ldb/transact! conn (concat tx-data update-block-order-tx-data) tx-meta))))))))
 
 (defn- apply-remote-update-ops
   [repo conn update-ops]

+ 7 - 3
src/main/frontend/worker/search.cljs

@@ -403,9 +403,13 @@ DROP TRIGGER IF EXISTS blocks_au;
                                 (let [{:keys [id page title snippet]} result
                                       block-id (uuid id)]
                                   (when-let [block (d/entity @conn [:block/uuid block-id])]
-                                    (when-not (and library-page-search?
-                                                   (or (:block/parent block)
-                                                       (not (ldb/internal-page? block)))) ; remove pages that already have parents
+                                    (when-not (or
+                                               ;; remove pages that already have parents
+                                               (and library-page-search?
+                                                    (or (ldb/page-in-library? @conn block)
+                                                        (not (ldb/internal-page? block))))
+                                               ;; remove non-page blocks when asking for pages only
+                                               (and page-only? (not (ldb/page? block))))
                                       (when (if dev?
                                               true
                                               (if built-in?

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

@@ -718,57 +718,57 @@
 (defn ^:export insert_block
   [block-uuid-or-page-name content ^js opts]
   (this-as this
-    (when (string/blank? block-uuid-or-page-name)
-      (throw (js/Error. "Page title or block UUID shouldn't be empty.")))
-
-    (p/let [block? (util/uuid-string? (str block-uuid-or-page-name))
-            block (<pull-block (str block-uuid-or-page-name))]
-      (if (and block? (not block))
-        (throw (js/Error. "Block not exists"))
-        (p/let [{:keys [before sibling focus customUUID properties autoOrderedList]} (bean/->clj opts)
-                [page-name block-uuid] (if (util/uuid-string? block-uuid-or-page-name)
-                                         [nil (uuid block-uuid-or-page-name)]
-                                         [block-uuid-or-page-name nil])
-                page-name (when page-name (util/page-name-sanity-lc page-name))
-                _ (when (and page-name
-                          (nil? (ldb/get-page (db/get-db) page-name)))
-                    (page-handler/<create! block-uuid-or-page-name {}))
-                custom-uuid (or customUUID (:id properties))
-                custom-uuid (when custom-uuid (sdk-utils/uuid-or-throw-error custom-uuid))
-                edit-block? (if (nil? focus) true focus)
-                _ (when (and custom-uuid (db-model/query-block-by-uuid custom-uuid))
-                    (throw (js/Error.
-                             (util/format "Custom block UUID already exists (%s)." custom-uuid))))
-                block-uuid' (if (and (not sibling) before block-uuid)
-                              (let [block (db/entity [:block/uuid block-uuid])
-                                    first-child (ldb/get-first-child (db/get-db) (:db/id block))]
-                                (if first-child
-                                  (:block/uuid first-child)
-                                  block-uuid))
-                              block-uuid)
-                insert-at-first-child? (not= block-uuid' block-uuid)
-                [sibling? before?] (if insert-at-first-child?
-                                     [true true]
-                                     [sibling before])
-                db-base? (db-graph?)
-                before? (if (and (false? sibling?) before? (not insert-at-first-child?))
-                          false
-                          before?)
-                new-block (editor-handler/api-insert-new-block!
-                            content
-                            {:block-uuid block-uuid'
-                             :sibling? sibling?
-                             :before? before?
-                             :edit-block? edit-block?
-                             :page page-name
-                             :custom-uuid custom-uuid
-                             :ordered-list? (if (boolean? autoOrderedList) autoOrderedList false)
-                             :properties (when (not db-base?)
-                                           (merge properties
-                                             (when custom-uuid {:id custom-uuid})))})
-                _ (when (and db-base? (some? properties))
-                    (api-block/save-db-based-block-properties! new-block properties this))]
-          (bean/->js (sdk-utils/normalize-keyword-for-json new-block)))))))
+           (when (string/blank? block-uuid-or-page-name)
+             (throw (js/Error. "Page title or block UUID shouldn't be empty.")))
+
+           (p/let [block? (util/uuid-string? (str block-uuid-or-page-name))
+                   block (<pull-block (str block-uuid-or-page-name))]
+             (if (and block? (not block))
+               (throw (js/Error. "Block not exists"))
+               (p/let [{:keys [before sibling focus customUUID properties autoOrderedList]} (bean/->clj opts)
+                       [page-name block-uuid] (if (util/uuid-string? block-uuid-or-page-name)
+                                                [nil (uuid block-uuid-or-page-name)]
+                                                [block-uuid-or-page-name nil])
+                       page-name (when page-name (util/page-name-sanity-lc page-name))
+                       _ (when (and page-name
+                                    (nil? (ldb/get-page (db/get-db) page-name)))
+                           (page-handler/<create! block-uuid-or-page-name {}))
+                       custom-uuid (or customUUID (:id properties))
+                       custom-uuid (when custom-uuid (sdk-utils/uuid-or-throw-error custom-uuid))
+                       edit-block? (if (nil? focus) true focus)
+                       _ (when (and custom-uuid (db-model/query-block-by-uuid custom-uuid))
+                           (throw (js/Error.
+                                   (util/format "Custom block UUID already exists (%s)." custom-uuid))))
+                       block-uuid' (if (and (not sibling) before block-uuid)
+                                     (let [block (db/entity [:block/uuid block-uuid])
+                                           first-child (ldb/get-first-child (db/get-db) (:db/id block))]
+                                       (if first-child
+                                         (:block/uuid first-child)
+                                         block-uuid))
+                                     block-uuid)
+                       insert-at-first-child? (not= block-uuid' block-uuid)
+                       [sibling? before?] (if insert-at-first-child?
+                                            [true true]
+                                            [sibling before])
+                       db-base? (db-graph?)
+                       before? (if (and (false? sibling?) before? (not insert-at-first-child?))
+                                 false
+                                 before?)
+                       new-block (editor-handler/api-insert-new-block!
+                                  content
+                                  {:block-uuid block-uuid'
+                                   :sibling? sibling?
+                                   :before? before?
+                                   :edit-block? edit-block?
+                                   :page page-name
+                                   :custom-uuid custom-uuid
+                                   :ordered-list? (if (boolean? autoOrderedList) autoOrderedList false)
+                                   :properties (when (not db-base?)
+                                                 (merge properties
+                                                        (when custom-uuid {:id custom-uuid})))})
+                       _ (when (and db-base? (some? properties))
+                           (api-block/save-db-based-block-properties! new-block properties this))]
+                 (bean/->js (sdk-utils/normalize-keyword-for-json new-block)))))))
 
 (def ^:export insert_batch_block
   (fn [block-uuid ^js batch-blocks ^js opts]
@@ -892,11 +892,11 @@
 (defn ^:export get_property
   [k]
   (this-as this
-    (p/let [prop (-get-property this k)]
-      (some-> prop
-        (assoc :type (:logseq.property/type prop))
-        (sdk-utils/normalize-keyword-for-json)
-        (bean/->js)))))
+           (p/let [prop (-get-property this k)]
+             (some-> prop
+                     (assoc :type (:logseq.property/type prop))
+                     (sdk-utils/normalize-keyword-for-json)
+                     (bean/->js)))))
 
 (defn ^:export upsert_property
   "schema:
@@ -920,7 +920,7 @@
                        schema (cond-> schema
                                 (string? (:cardinality schema))
                                 (-> (assoc :db/cardinality (keyword (:cardinality schema)))
-                                  (dissoc :cardinality))
+                                    (dissoc :cardinality))
 
                                 (string? (:type schema))
                                 (-> (assoc :logseq.property/type (keyword (:type schema)))
@@ -935,70 +935,70 @@
 (defn ^:export remove_property
   [k]
   (this-as this
-    (p/let [prop (-get-property this k)]
-      (when-let [uuid (and (api-block/plugin-property-key? (:db/ident prop))
-                        (:block/uuid prop))]
-        (page-common-handler/<delete! uuid nil nil)))))
+           (p/let [prop (-get-property this k)]
+             (when-let [uuid (and (api-block/plugin-property-key? (:db/ident prop))
+                                  (:block/uuid prop))]
+               (page-common-handler/<delete! uuid nil nil)))))
 
 ;; block properties
 (defn ^:export upsert_block_property
   [block-uuid key ^js value]
   (this-as this
-    (p/let [keyname (api-block/sanitize-user-property-name key)
-            block-uuid (sdk-utils/uuid-or-throw-error block-uuid)
-            repo (state/get-current-repo)
-            block (db-async/<get-block repo block-uuid :children? false)
-            db-base? (db-graph?)
-            key' (-> (if (keyword? keyname) (name keyname) keyname) (util/trim-safe))
-            value (bean/->clj value)]
-      (when block
-        (if db-base?
-          (p/do!
-            (api-block/save-db-based-block-properties! block {key' value} this))
-          (property-handler/set-block-property! repo block-uuid key' value))))))
+           (p/let [keyname (api-block/sanitize-user-property-name key)
+                   block-uuid (sdk-utils/uuid-or-throw-error block-uuid)
+                   repo (state/get-current-repo)
+                   block (db-async/<get-block repo block-uuid :children? false)
+                   db-base? (db-graph?)
+                   key' (-> (if (keyword? keyname) (name keyname) keyname) (util/trim-safe))
+                   value (bean/->clj value)]
+             (when block
+               (if db-base?
+                 (p/do!
+                  (api-block/save-db-based-block-properties! block {key' value} this))
+                 (property-handler/set-block-property! repo block-uuid key' value))))))
 
 (defn ^:export remove_block_property
   [block-uuid key]
   (this-as this
-    (p/let [key (api-block/sanitize-user-property-name key)
-            block-uuid (sdk-utils/uuid-or-throw-error block-uuid)
-            _ (db-async/<get-block (state/get-current-repo) block-uuid :children? false)
-            db? (config/db-based-graph? (state/get-current-repo))
-            key-ns? (and (keyword? key) (namespace key))
-            key (if key-ns? key (if (keyword? key) (name key) key))
-            key (if (and db? (not key-ns?))
-                  (api-block/get-db-ident-for-user-property-name
-                    key (api-block/resolve-property-prefix-for-db this))
-                  key)]
-      (property-handler/remove-block-property!
-        (state/get-current-repo)
-        block-uuid key))))
+           (p/let [key (api-block/sanitize-user-property-name key)
+                   block-uuid (sdk-utils/uuid-or-throw-error block-uuid)
+                   _ (db-async/<get-block (state/get-current-repo) block-uuid :children? false)
+                   db? (config/db-based-graph? (state/get-current-repo))
+                   key-ns? (and (keyword? key) (namespace key))
+                   key (if key-ns? key (if (keyword? key) (name key) key))
+                   key (if (and db? (not key-ns?))
+                         (api-block/get-db-ident-for-user-property-name
+                          key (api-block/resolve-property-prefix-for-db this))
+                         key)]
+             (property-handler/remove-block-property!
+              (state/get-current-repo)
+              block-uuid key))))
 
 (defn ^:export get_block_property
   [block-uuid key]
   (this-as this
-    (p/let [block-uuid (sdk-utils/uuid-or-throw-error block-uuid)
-            _ (db-async/<get-block (state/get-current-repo) block-uuid :children? false)]
-      (when-let [properties (some-> block-uuid (db-model/get-block-by-uuid) (:block/properties))]
-        (when (seq properties)
-          (let [key (api-block/sanitize-user-property-name key)
-                property-name (if (keyword? key) (name key) key)
-                ident (api-block/get-db-ident-for-user-property-name
-                        property-name (api-block/resolve-property-prefix-for-db this))
-                property-value (or (get properties key)
-                                 (get properties (keyword property-name))
-                                 (get properties ident))
-                property-value (if-let [property-id (:db/id property-value)]
-                                 (db/pull property-id) property-value)
-                property-value (cond-> property-value
-                                 (map? property-value)
-                                 (assoc
-                                   :value (or (:logseq.property/value property-value)
-                                            (:block/title property-value))
-                                   :ident ident))
-                parsed-value (api-block/parse-property-json-value-if-need ident property-value)]
-            (or parsed-value
-              (bean/->js (sdk-utils/normalize-keyword-for-json property-value)))))))))
+           (p/let [block-uuid (sdk-utils/uuid-or-throw-error block-uuid)
+                   _ (db-async/<get-block (state/get-current-repo) block-uuid :children? false)]
+             (when-let [properties (some-> block-uuid (db-model/get-block-by-uuid) (:block/properties))]
+               (when (seq properties)
+                 (let [key (api-block/sanitize-user-property-name key)
+                       property-name (if (keyword? key) (name key) key)
+                       ident (api-block/get-db-ident-for-user-property-name
+                              property-name (api-block/resolve-property-prefix-for-db this))
+                       property-value (or (get properties key)
+                                          (get properties (keyword property-name))
+                                          (get properties ident))
+                       property-value (if-let [property-id (:db/id property-value)]
+                                        (db/pull property-id) property-value)
+                       property-value (cond-> property-value
+                                        (map? property-value)
+                                        (assoc
+                                         :value (or (:logseq.property/value property-value)
+                                                    (:block/title property-value))
+                                         :ident ident))
+                       parsed-value (api-block/parse-property-json-value-if-need ident property-value)]
+                   (or parsed-value
+                       (bean/->js (sdk-utils/normalize-keyword-for-json property-value)))))))))
 
 (def ^:export get_block_properties
   (fn [block-uuid]

+ 58 - 58
src/main/logseq/api/block.cljs

@@ -24,7 +24,7 @@
 (defn convert?to-built-in-property-name
   [property-name]
   (if (and (not (qualified-keyword? property-name))
-        (contains? #{:background-color} property-name))
+           (contains? #{:background-color} property-name))
     (keyword :logseq.property property-name)
     property-name))
 
@@ -32,19 +32,19 @@
   [k]
   (if (string? k)
     (-> k (string/trim)
-      (string/replace " " "")
-      (string/replace #"^[:_\s]+" "")
-      (#(cond-> %
-          (not (string/includes? % "/"))
-          (string/lower-case))))
+        (string/replace " " "")
+        (string/replace #"^[:_\s]+" "")
+        (#(cond-> %
+            (not (string/includes? % "/"))
+            (string/lower-case))))
     k))
 
 (defn resolve-property-prefix-for-db
   [^js plugin]
   (->> (when (some-> js/window.LSPlugin (.-PluginLocal))
          (or (some->> plugin (.-id) (sanitize-user-property-name) (str "."))
-           "._api"))
-    (str "plugin.property")))
+             "._api"))
+       (str "plugin.property")))
 
 ;; FIXME: This ns should not be creating idents. This allows for ident conflicts
 ;; and assumes that names directly map to idents which is incorrect and breaks for multiple
@@ -58,27 +58,27 @@
                           (keyword property-name) property-name)
          property-name' (convert?to-built-in-property-name property-name')]
      (if (qualified-keyword? property-name') property-name'
-       (db-ident/create-db-ident-from-name prefix (name property-name) false)))))
+         (db-ident/create-db-ident-from-name prefix (name property-name) false)))))
 
 (defn plugin-property-key?
   [ident]
   (some-> ident (str)
-    (string/starts-with? ":plugin.property.")))
+          (string/starts-with? ":plugin.property.")))
 
 (defn into-readable-db-properties
   [properties]
   (some-> properties
-    (db-pu/readable-properties
-      {:original-key? true :key-fn str})))
+          (db-pu/readable-properties
+           {:original-key? true :key-fn str})))
 
 (defn into-properties
   ([block] (into-properties (state/get-current-repo) block))
   ([repo block]
    (if (some-> repo (config/db-based-graph?))
      (let [props (some->> block
-                   (filter (fn [[k _]] (db-property/property? k)))
-                   (into {})
-                   (into-readable-db-properties))
+                          (filter (fn [[k _]] (db-property/property? k)))
+                          (into {})
+                          (into-readable-db-properties))
            block (update block :block/properties merge props)
            block (apply dissoc (concat [block] (keys props)))]
        block)
@@ -87,8 +87,8 @@
 (defn parse-property-json-value-if-need
   [ident property-value]
   (when-let [prop (and (string? property-value)
-                    (plugin-property-key? ident)
-                    (some-> ident (db-utils/entity)))]
+                       (plugin-property-key? ident)
+                       (some-> ident (db-utils/entity)))]
     (if (= (:logseq.property/type prop) :string)
       (try
         (js/JSON.parse property-value)
@@ -108,10 +108,10 @@
                       (let [id (:db/id (ldb/get-case-page (conn/get-db) page))]
                         (if (nil? id)
                           (-> (page-handler/<create! page {:redirect? false})
-                            (p/then #(:db/id %)))
+                              (p/then #(:db/id %)))
                           id))))
-                (p/all)
-                (p/then (fn [vs] [ident :logseq.property/empty-placeholder vs true])))
+                  (p/all)
+                  (p/then (fn [vs] [ident :logseq.property/empty-placeholder vs true])))
               (let [value (if as-json? (js/JSON.stringify (bean/->js value)) value)]
                 [ident value nil false]))))
         ent (db-utils/entity ident)]
@@ -124,8 +124,8 @@
             schema {:logseq.property/type type
                     :db/cardinality :one}]
         (p/chain
-          (db-property-handler/upsert-property! ident schema {})
-          (fn [] (value-handle type false))))
+         (db-property-handler/upsert-property! ident schema {})
+         (fn [] (value-handle type false))))
       (let [value-multi? (vector? value)
             ident (:db/ident ent)
             ent-type (:logseq.property/type ent)
@@ -135,54 +135,54 @@
         (when cardinality-want-illegal-changed?
           (throw (js/Error. "Multiple property type can not be changed.")))
         (p/chain
-          (db-property-handler/upsert-property! ident
-            {:logseq.property/type ent-type
-             :db/cardinality (if (and (not ent-type-str?) value-multi?) :many :one)}
-            {})
-          #(value-handle ent-type ent-multi?))))))
+         (db-property-handler/upsert-property! ident
+                                               {:logseq.property/type ent-type
+                                                :db/cardinality (if (and (not ent-type-str?) value-multi?) :many :one)}
+                                               {})
+         #(value-handle ent-type ent-multi?))))))
 
 (defn save-db-based-block-properties!
   ([block properties] (save-db-based-block-properties! block properties nil))
   ([block properties ^js plugin]
    (when-let [block-id (and (seq properties) (:db/id block))]
      (let [properties (update-keys properties
-                        (fn [k]
-                          (let [prefix (resolve-property-prefix-for-db plugin)]
-                            (get-db-ident-for-user-property-name k prefix))))
+                                   (fn [k]
+                                     (let [prefix (resolve-property-prefix-for-db plugin)]
+                                       (get-db-ident-for-user-property-name k prefix))))
            *properties-page-refs (volatile! {})]
        (-> (for [ident (keys properties)]
              (p/let [ret (infer-property-value-type-to-save! ident (get properties ident))]
                ret))
-         (p/all)
-         (p/chain
-           (fn [props]
-             (->> props
-               (reduce (fn [a [k v vs multi?]]
-                         (if multi?
-                           (do (vswap! *properties-page-refs assoc k vs) a)
-                           (assoc a k v))) {})
-               (db-property-handler/set-block-properties! block-id)))
+           (p/all)
+           (p/chain
+            (fn [props]
+              (->> props
+                   (reduce (fn [a [k v vs multi?]]
+                             (if multi?
+                               (do (vswap! *properties-page-refs assoc k vs) a)
+                               (assoc a k v))) {})
+                   (db-property-handler/set-block-properties! block-id)))
            ;; handle page refs
-           (fn []
-             (when (seq @*properties-page-refs)
-               (doseq [[ident refs] @*properties-page-refs]
-                 (-> (property-handler/remove-block-property! (state/get-current-repo) block-id ident)
-                   (p/then
-                     (fn []
-                       (if (seq refs)
-                         (ui-outliner-tx/transact!
-                           {:outliner-op :set-block-properties}
-                           (doseq [eid refs]
-                             (when (number? eid)
-                               (property-handler/set-block-property!
+            (fn []
+              (when (seq @*properties-page-refs)
+                (doseq [[ident refs] @*properties-page-refs]
+                  (-> (property-handler/remove-block-property! (state/get-current-repo) block-id ident)
+                      (p/then
+                       (fn []
+                         (if (seq refs)
+                           (ui-outliner-tx/transact!
+                            {:outliner-op :set-block-properties}
+                            (doseq [eid refs]
+                              (when (number? eid)
+                                (property-handler/set-block-property!
                                  (state/get-current-repo) block-id ident eid))))
-                         (db-property-handler/set-block-property! block-id ident :logseq.property/empty-placeholder))))))))))))))
+                           (db-property-handler/set-block-property! block-id ident :logseq.property/empty-placeholder))))))))))))))
 
 (defn <sync-children-blocks!
   [block]
   (when block
     (db-async/<get-block (state/get-current-repo)
-      (:block/uuid (:block/parent block)) {:children? true})))
+                         (:block/uuid (:block/parent block)) {:children? true})))
 
 (defn get_block
   [id-or-uuid ^js opts]
@@ -190,19 +190,19 @@
                      (db-utils/pull id-or-uuid)
                      (and id-or-uuid (db-model/query-block-by-uuid (sdk-utils/uuid-or-throw-error id-or-uuid))))]
     (when (or (true? (some-> opts (.-includePage)))
-            (not (contains? block :block/name)))
+              (not (contains? block :block/name)))
       (when-let [uuid (:block/uuid block)]
         (let [{:keys [includeChildren]} (bean/->clj opts)
               repo (state/get-current-repo)
               block (if includeChildren
                       ;; nested children results
                       (let [blocks (->> (db-model/get-block-and-children repo uuid)
-                                     (map (fn [b]
-                                            (dissoc (db-utils/pull (:db/id b)) :block.temp/load-status))))]
+                                        (map (fn [b]
+                                               (dissoc (db-utils/pull (:db/id b)) :block.temp/load-status))))]
                         (first (outliner-tree/blocks->vec-tree blocks uuid)))
                       ;; attached shallow children
                       (assoc block :block/children
-                        (map #(list :uuid (:block/uuid %))
-                          (db/get-block-immediate-children repo uuid))))
+                             (map #(list :uuid (:block/uuid %))
+                                  (db/get-block-immediate-children repo uuid))))
               block (into-properties repo block)]
           (bean/->js (sdk-utils/normalize-keyword-for-json block)))))))

+ 3 - 3
src/main/mobile/components/settings.cljs

@@ -57,6 +57,6 @@
                        [:span.text-muted-foreground {:slot "icon-only"}
                         (ion/tabler-icon "dots" {:size 20})])))))
      (ion/content {:class "ion-padding"}
-      (user-profile login?)
-      [:div.mt-8
-       (repo/repos-cp)]))))
+                  (user-profile login?)
+                  [:div.mt-8
+                   (repo/repos-cp)]))))

+ 2 - 0
src/resources/dicts/en.edn

@@ -667,6 +667,7 @@
   :editor/select-down             "Select content below"
   :editor/move-block-up           "Move block up"
   :editor/move-block-down         "Move block down"
+  :editor/move-blocks             "Move blocks to"
   :editor/open-edit               "Edit selected block"
   :editor/open-selected-blocks-in-sidebar "Open selected block(s) in sidebar"
   :editor/select-block-up         "Select block above"
@@ -690,6 +691,7 @@
   :editor/zoom-out                "Zoom out editing block / Backwards otherwise"
   :editor/toggle-number-list      "Toggle number list"
   :editor/add-property            "Add property"
+  :editor/set-tags                "Set tags for selected block(s)"
   :editor/add-property-deadline   "Add task deadline to selected block"
   :editor/add-property-status     "Add task status to selected block"
   :editor/add-property-priority   "Add task priority to selected block"

+ 18 - 7
src/test/frontend/modules/outliner/core_test.cljs

@@ -169,7 +169,8 @@
      (transact-opts)
      (outliner-core/move-blocks! test-db
                                  (db/get-db test-db false)
-                                 [(get-block 3)] (get-block 14) true))
+                                 [(get-block 3)] (get-block 14)
+                                 {:sibling? true}))
     (is (= [6 9] (get-children 2)))
     (is (= [13 14 3 15] (get-children 12))))
 
@@ -192,7 +193,8 @@
        (transact-opts)
        (outliner-core/move-blocks! test-db
                                    (db/get-db test-db false)
-                                   [(get-block 3)] (get-block 12) false))
+                                   [(get-block 3)] (get-block 12)
+                                   {:sibling? false}))
       (is (= [6 9] (get-children 2)))
       (is (= [3 13 14 15] (get-children 12))))))
 
@@ -205,7 +207,8 @@
      (transact-opts)
      (outliner-core/move-blocks! test-db
                                  (db/get-db test-db false)
-                                 [(get-block 3)] (get-block 2) true))
+                                 [(get-block 3)] (get-block 2)
+                                 {:sibling? true}))
     (is (= [4] (get-children 2)))
     (is (= [2 3 5] (get-children 22)))))
 
@@ -220,7 +223,8 @@
      (transact-opts)
      (outliner-core/move-blocks! test-db
                                  (db/get-db test-db false)
-                                 [(get-block 3) (get-block 6)] (get-block 2) true))
+                                 [(get-block 3) (get-block 6)] (get-block 2)
+                                 {:sibling? true}))
     (is (= [4] (get-children 2)))
     (is (= [2 3 6 5 7] (get-children 22)))))
 
@@ -236,7 +240,8 @@
      (transact-opts)
      (outliner-core/move-blocks! test-db
                                  (db/get-db test-db false)
-                                 [(get-block 3) (get-block 5)] (get-block 2) false))
+                                 [(get-block 3) (get-block 5)] (get-block 2)
+                                 {:sibling? false}))
     (is (= [3 5 4] (get-children 2)))
     (is (= [2 6 7 8] (get-children 22)))))
 
@@ -706,7 +711,11 @@ tags:: tag1, tag2
           (when (seq blocks)
             (let [target (get-random-block)]
               (outliner-tx/transact! (transact-opts)
-                                     (outliner-core/move-blocks! test-db (db/get-db test-db false) blocks target (gen/generate gen/boolean)))
+                                     (outliner-core/move-blocks! test-db
+                                                                 (db/get-db test-db false)
+                                                                 blocks
+                                                                 target
+                                                                 {:sibling? (gen/generate gen/boolean)}))
               (let [total (get-blocks-count)]
                 (is (= total (count @*random-blocks)))))))))))
 
@@ -774,7 +783,9 @@ tags:: tag1, tag2
                    (outliner-tx/transact! (transact-opts)
                                           (outliner-core/move-blocks! test-db
                                                                       (db/get-db test-db false)
-                                                                      blocks (get-random-block) (gen/generate gen/boolean))))))
+                                                                      blocks
+                                                                      (get-random-block)
+                                                                      {:sibling? (gen/generate gen/boolean)})))))
 
              ;; move up down
              (fn []