Browse Source

Merge branch 'feat/db' into refactor/page-parent

Gabriel Horner 7 months ago
parent
commit
df9833040c
52 changed files with 730 additions and 505 deletions
  1. 2 2
      .github/workflows/build-desktop-release.yml
  2. 2 2
      .github/workflows/clj-e2e.yml
  3. 1 1
      android/app/build.gradle
  4. 7 3
      clj-e2e/bb.edn
  5. 27 21
      clj-e2e/dev/user.clj
  6. 1 0
      clj-e2e/src/logseq/e2e/block.clj
  7. 1 0
      clj-e2e/src/logseq/e2e/rtc.clj
  8. 2 2
      clj-e2e/test/logseq/e2e/commands_basic_test.clj
  9. 1 1
      clj-e2e/test/logseq/e2e/multi_tabs_basic_test.clj
  10. 1 1
      clj-e2e/test/logseq/e2e/outliner_basic_test.clj
  11. 1 1
      clj-e2e/test/logseq/e2e/plugins_basic_test.clj
  12. 4 4
      clj-e2e/test/logseq/e2e/reference_basic_test.clj
  13. 75 0
      clj-e2e/test/logseq/e2e/rtc_extra_test.clj
  14. 18 20
      deps/db/src/logseq/db/common/entity_plus.cljc
  15. 42 14
      deps/db/src/logseq/db/frontend/content.cljs
  16. 3 2
      deps/db/src/logseq/db/frontend/malli_schema.cljs
  17. 1 1
      deps/db/src/logseq/db/frontend/schema.cljs
  18. 2 1
      deps/db/src/logseq/db/sqlite/create_graph.cljs
  19. 17 12
      deps/db/src/logseq/db/sqlite/export.cljs
  20. 1 1
      resources/forge.config.js
  21. 1 1
      scripts/bump-version.sh
  22. 6 7
      src/main/frontend/commands.cljs
  23. 1 22
      src/main/frontend/common/file/core.cljs
  24. 91 86
      src/main/frontend/components/block.cljs
  25. 8 2
      src/main/frontend/components/block.css
  26. 1 1
      src/main/frontend/components/class.cljs
  27. 7 2
      src/main/frontend/components/container.cljs
  28. 26 23
      src/main/frontend/components/editor.cljs
  29. 49 46
      src/main/frontend/components/export.cljs
  30. 1 4
      src/main/frontend/components/file_based/hierarchy.cljs
  31. 4 2
      src/main/frontend/components/header.cljs
  32. 4 3
      src/main/frontend/components/icon.cljs
  33. 3 3
      src/main/frontend/components/page.cljs
  34. 1 1
      src/main/frontend/components/page.css
  35. 6 8
      src/main/frontend/components/property/value.cljs
  36. 86 73
      src/main/frontend/components/right_sidebar.cljs
  37. 55 47
      src/main/frontend/components/rtc/indicator.cljs
  38. 1 1
      src/main/frontend/db/model.cljs
  39. 10 8
      src/main/frontend/handler/editor.cljs
  40. 9 8
      src/main/frontend/handler/events.cljs
  41. 26 4
      src/main/frontend/handler/export.cljs
  42. 4 0
      src/main/frontend/handler/repo.cljs
  43. 1 1
      src/main/frontend/handler/route.cljs
  44. 8 0
      src/main/frontend/modules/shortcut/config.cljs
  45. 1 1
      src/main/frontend/search/browser.cljs
  46. 1 1
      src/main/frontend/state.cljs
  47. 11 0
      src/main/frontend/ui.css
  48. 7 1
      src/main/frontend/util.cljc
  49. 10 3
      src/main/frontend/worker/db/migrate.cljs
  50. 69 45
      src/main/frontend/worker/db_worker.cljs
  51. 12 12
      src/main/frontend/worker/search.cljs
  52. 1 0
      src/resources/dicts/en.edn

+ 2 - 2
.github/workflows/build-desktop-release.yml

@@ -43,8 +43,8 @@ on:
       #   type: boolean
       #   required: true
       #   default: true
-  schedule: # Every workday at the 2 P.M. (UTC) we run a scheduled nightly build
-    - cron: '0 14 * * MON-FRI'
+  # schedule: # Every workday at the 2 P.M. (UTC) we run a scheduled nightly build
+  #   - cron: '0 14 * * MON-FRI'
 
 env:
   CLOJURE_VERSION: '1.11.1.1413'

+ 2 - 2
.github/workflows/clj-e2e.yml

@@ -83,10 +83,10 @@ jobs:
           ls -lR ./public
 
       - name: Run e2e tests
-        run: cd clj-e2e && bb dev
+        run: cd clj-e2e && timeout 30m bb dev
         env:
           DEBUG: "pw:api"
-          
+
       - name: Collect screenshots
         if: ${{ failure() }}
         uses: actions/upload-artifact@v4

+ 1 - 1
android/app/build.gradle

@@ -7,7 +7,7 @@ android {
         applicationId "com.logseq.app"
         minSdkVersion rootProject.ext.minSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
-        versionCode 84
+        versionCode 87
         versionName "0.11.0"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         aaptOptions {

+ 7 - 3
clj-e2e/bb.edn

@@ -12,9 +12,13 @@
 
   prn {:task (clojure "-X clojure.core/prn" cli-opts)}
 
-  test {:task (do
-                (clojure "-T:build test")
-                (System/exit 0))}
+  test {:doc "run tests (ns'es ending in '-basic-test')"
+        :task (do (clojure "-M:test -r \".*\\-basic\\-test$\"")
+                  (System/exit 0))}
+
+  extra-test {:doc "run tests (ns'es ending in '-extra-test')"
+              :task (do (clojure "-M:test -r \".*\\-extra\\-test$\"")
+                        (System/exit 0))}
 
   -dev {:depends [serve prn test]}
 

+ 27 - 21
clj-e2e/dev/user.clj

@@ -2,16 +2,17 @@
   "fns used on repl"
   (:require [clojure.test :refer [run-tests run-test]]
             [logseq.e2e.block :as b]
-            [logseq.e2e.commands-test]
+            [logseq.e2e.commands-basic-test]
             [logseq.e2e.config :as config]
             [logseq.e2e.fixtures :as fixtures]
             [logseq.e2e.graph :as graph]
             [logseq.e2e.keyboard :as k]
-            [logseq.e2e.multi-tabs-test]
-            [logseq.e2e.outliner-test]
-            [logseq.e2e.plugins-test]
-            [logseq.e2e.reference-test]
+            [logseq.e2e.multi-tabs-basic-test]
+            [logseq.e2e.outliner-basic-test]
+            [logseq.e2e.plugins-basic-test]
+            [logseq.e2e.reference-basic-test]
             [logseq.e2e.rtc-basic-test]
+            [logseq.e2e.rtc-extra-test]
             [logseq.e2e.util :as util]
             [wally.main :as w]
             [wally.repl :as repl]))
@@ -31,12 +32,12 @@
 
 (defn run-commands-test
   []
-  (->> (future (run-tests 'logseq.e2e.commands-test))
+  (->> (future (run-tests 'logseq.e2e.commands-basic-test))
        (swap! *futures assoc :commands-test)))
 
 (defn run-outliner-test
   []
-  (->> (future (run-tests 'logseq.e2e.outliner-test))
+  (->> (future (run-tests 'logseq.e2e.outliner-basic-test))
        (swap! *futures assoc :outliner-test)))
 
 (defn run-rtc-basic-test
@@ -46,27 +47,32 @@
 
 (defn run-multi-tabs-test
   []
-  (->> (future (run-tests 'logseq.e2e.multi-tabs-test))
+  (->> (future (run-tests 'logseq.e2e.multi-tabs-basic-test))
        (swap! *futures assoc :multi-tabs-test)))
 
 (defn run-reference-test
   []
-  (->> (future (run-tests 'logseq.e2e.reference-test))
+  (->> (future (run-tests 'logseq.e2e.reference-basic-test))
        (swap! *futures assoc :reference-test)))
 
 (defn run-plugins-test
   []
-  (->> (future (run-tests 'logseq.e2e.plugins-test))
+  (->> (future (run-tests 'logseq.e2e.plugins-basic-test))
        (swap! *futures assoc :plugins-test)))
 
-(defn run-all-test
+(defn run-rtc-extra-test
   []
-  (run-tests 'logseq.e2e.commands-test
-             'logseq.e2e.multi-tabs-test
-             'logseq.e2e.outliner-test
+  (->> (future (run-tests 'logseq.e2e.rtc-extra-test))
+       (swap! *futures assoc :rtc-extra-test)))
+
+(defn run-all-basic-test
+  []
+  (run-tests 'logseq.e2e.commands-basic-test
+             'logseq.e2e.multi-tabs-basic-test
+             'logseq.e2e.outliner-basic-test
              'logseq.e2e.rtc-basic-test
-             'logseq.e2e.plugins-test
-             'logseq.e2e.reference-test))
+             'logseq.e2e.plugins-basic-test
+             'logseq.e2e.reference-basic-test))
 
 (defn start
   []
@@ -92,17 +98,17 @@
     (w/wait-for (first (util/get-edit-block-container))
                 {:state :detached}))
 
-  (run-tests 'logseq.e2e.commands-test
-             'logseq.e2e.multi-tabs-test
-             'logseq.e2e.outliner-test
+  (run-tests 'logseq.e2e.commands-basic-test
+             'logseq.e2e.multi-tabs-basic-test
+             'logseq.e2e.outliner-basic-test
              'logseq.e2e.rtc-basic-test)
 
   (do
     (reset! config/*headless true)
     (reset! config/*slow-mo 10)
-    (run-tests 'logseq.e2e.reference-test)
+    (run-tests 'logseq.e2e.reference-basic-test)
     (dotimes [i 10]
-      (run-tests 'logseq.e2e.reference-test)))
+      (run-tests 'logseq.e2e.reference-basic-test)))
 
   ;;
   )

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

@@ -15,6 +15,7 @@
 
 (defn save-block
   [text]
+  (w/click util/editor-q)
   (w/fill util/editor-q text)
   (assert/assert-is-visible (loc/filter util/editor-q :has-text text)))
 

+ 1 - 0
clj-e2e/src/logseq/e2e/rtc.clj

@@ -33,6 +33,7 @@
 
 (defn wait-tx-update-to
   [new-tx]
+  (assert (int? new-tx))
   (loop [i 5]
     (when (zero? i) (throw (ex-info "wait-tx-update-to" {:update-to new-tx})))
     (util/wait-timeout 1000)

+ 2 - 2
clj-e2e/test/logseq/e2e/commands_test.clj → clj-e2e/test/logseq/e2e/commands_basic_test.clj

@@ -1,4 +1,4 @@
-(ns logseq.e2e.commands-test
+(ns logseq.e2e.commands-basic-test
   (:require
    [clj-time.core :as t]
    [clj-time.local :as tl]
@@ -43,7 +43,7 @@
       (k/enter)
       (is (string/includes? (util/get-edit-content) "[["))
       (util/exit-edit)
-      (is (= "b1" (util/get-text ".block-ref"))))))
+      (is (= "b1" (.textContent (second (w/query "a.page-ref"))))))))
 
 (deftest link-test
   (testing "/link"

+ 1 - 1
clj-e2e/test/logseq/e2e/multi_tabs_test.clj → clj-e2e/test/logseq/e2e/multi_tabs_basic_test.clj

@@ -1,4 +1,4 @@
-(ns logseq.e2e.multi-tabs-test
+(ns logseq.e2e.multi-tabs-basic-test
   (:require [clojure.test :refer [deftest is testing use-fixtures]]
             [logseq.e2e.assert :as assert]
             [logseq.e2e.block :as b]

+ 1 - 1
clj-e2e/test/logseq/e2e/outliner_test.clj → clj-e2e/test/logseq/e2e/outliner_basic_test.clj

@@ -1,4 +1,4 @@
-(ns logseq.e2e.outliner-test
+(ns logseq.e2e.outliner-basic-test
   (:require
    [clojure.test :refer [deftest testing is use-fixtures]]
    [logseq.e2e.block :as b]

+ 1 - 1
clj-e2e/test/logseq/e2e/plugins_test.clj → clj-e2e/test/logseq/e2e/plugins_basic_test.clj

@@ -1,4 +1,4 @@
-(ns logseq.e2e.plugins-test
+(ns logseq.e2e.plugins-basic-test
   (:require
    [clojure.string :as string]
    [clojure.test :refer [deftest testing is use-fixtures]]

+ 4 - 4
clj-e2e/test/logseq/e2e/reference_test.clj → clj-e2e/test/logseq/e2e/reference_basic_test.clj

@@ -1,4 +1,4 @@
-(ns logseq.e2e.reference-test
+(ns logseq.e2e.reference-basic-test
   (:require
    [clojure.test :refer [deftest testing is use-fixtures]]
    [logseq.e2e.assert :as assert]
@@ -43,7 +43,7 @@
     (b/wait-editor-text "b2")
     (b/paste)
     (util/exit-edit)
-    (b/assert-blocks-visible ["b1b2" "b2b1"])))
+    (b/assert-blocks-visible ["b1[[b2]]" "b2[[b1]]"])))
 
 (deftest parent-reference
   (testing "parent reference"
@@ -59,7 +59,7 @@
     (b/wait-editor-text "b2")
     (b/paste)
     (util/exit-edit)
-    (b/assert-blocks-visible ["b1b2" "b2b1"])))
+    (b/assert-blocks-visible ["b1[[b2]]" "b2[[b1]]"])))
 
 (deftest cycle-reference
   (testing "cycle reference"
@@ -80,6 +80,6 @@
     (assert/assert-editor-mode)
     (b/paste)
     (util/exit-edit)
-    (b/assert-blocks-visible ["b1b3b2" "b2b1b3" "b3b2b1"])))
+    (b/assert-blocks-visible ["b1[[b3[[b2]]]]" "b2[[b1[[b3]]]]" "b3[[b2[[b1]]]]"])))
 
 ;; TODO: page references

+ 75 - 0
clj-e2e/test/logseq/e2e/rtc_extra_test.clj

@@ -0,0 +1,75 @@
+(ns logseq.e2e.rtc-extra-test
+  (:require
+   [clojure.test :refer [deftest testing is use-fixtures run-tests]]
+   [com.climate.claypoole :as cp]
+   [logseq.e2e.block :as b]
+   [logseq.e2e.fixtures :as fixtures :refer [*page1 *page2]]
+   [logseq.e2e.graph :as graph]
+   [logseq.e2e.rtc :as rtc]
+   [logseq.e2e.util :as util]
+   [wally.main :as w]
+   [wally.repl :as repl]))
+
+(def *graph-name (atom nil))
+(defn cleanup-fixture
+  [f]
+  (f)
+  (w/with-page @*page2
+    (assert (some? @*graph-name))
+    (graph/remove-remote-graph @*graph-name)))
+
+(use-fixtures :once
+  fixtures/open-2-pages
+  ;; cleanup-fixture
+  )
+
+(defn- offline
+  []
+  (.setOffline (.context (w/get-page)) true))
+
+(defn- online
+  []
+  (.setOffline (.context (w/get-page)) false))
+
+(defn- insert-task-blocks
+  [title-prefix]
+  (doseq [status ["Backlog" "Todo" "Doing" "In review" "Done" "Canceled"]
+          priority ["No priority" "Low" "Medium" "High" "Urgent"]]
+    (b/new-block (str title-prefix "-" status "-" priority))
+    (util/input-command status)
+    (util/input-command priority)))
+
+(deftest rtc-extra-test
+  (let [graph-name (str "rtc-extra-test-graph-" (.toEpochMilli (java.time.Instant/now)))]
+    (reset! *graph-name graph-name)
+    (testing "open 2 app instances, add a rtc graph, check this graph available on other instance"
+      (cp/prun!
+       2
+       #(w/with-page %
+          (util/login-test-account))
+       [@*page1 @*page2])
+      (w/with-page @*page1
+        (graph/new-graph graph-name true))
+      (w/with-page @*page2
+        (graph/wait-for-remote-graph graph-name)
+        (graph/switch-graph graph-name true)))
+    (testing "rtc-stop app1, add some task blocks, then rtc-start on app1"
+      (let [*latest-remote-tx (atom nil)]
+        (w/with-page @*page1
+          (offline))
+        (w/with-page @*page2
+          (let [{:keys [_local-tx remote-tx]}
+                (rtc/with-wait-tx-updated
+                  (insert-task-blocks "t1"))]
+            (reset! *latest-remote-tx remote-tx))
+          ;; TODO: more operations
+          (util/exit-edit))
+        (w/with-page @*page1
+          (online)
+          (rtc/wait-tx-update-to @*latest-remote-tx)
+          ;; TODO: check blocks exist
+          )))
+    (testing "cleanup"
+      (w/with-page @*page2
+        (assert (some? @*graph-name))
+        (graph/remove-remote-graph @*graph-name)))))

+ 18 - 20
deps/db/src/logseq/db/common/entity_plus.cljc

@@ -21,7 +21,7 @@
     ;; File graph only attributes. Can these be removed if this is only called in db graphs?
     :block/pre-block? :block/scheduled :block/deadline :block/type :block/name :block/marker
 
-    :block.temp/ast-title :block.temp/search?
+    :block.temp/ast-title
     :block.temp/fully-loaded? :block.temp/ast-body
 
     :db/valueType :db/cardinality :db/ident :db/index
@@ -91,18 +91,16 @@
         db-based? (db-based-graph? db)]
     (if (and db-based? (entity-util/journal? e))
       (get-journal-title db e)
-      (let [search? (get (.-kv e) :block.temp/search?)]
-        (or
-         (when-not (and search? (keyword-identical? k :block/title))
-           (get (.-kv e) k))
-         (let [result (lookup-entity e k default-value)
-               ;; Replace title for pages only, otherwise it'll recursively
-               ;; replace block id refs if there're cycle references of blocks
-               refs (:block/refs e)
-               result' (if (and (string? result) refs)
-                         (db-content/id-ref->title-ref result refs search?)
-                         result)]
-           (or result' default-value)))))))
+      (or
+       (get (.-kv e) k)
+       (let [result (lookup-entity e k default-value)
+             ;; Replace title for pages only, otherwise it'll recursively
+             ;; replace block id refs if there're cycle references of blocks
+             refs (:block/refs e)
+             result' (if (and (string? result) refs)
+                       (db-content/id-ref->title-ref result refs)
+                       result)]
+         (or result' default-value))))))
 
 (defn- lookup-kv-with-default-value
   [db ^Entity e k default-value]
@@ -162,12 +160,12 @@
 
            ;; cache :block/title
            :block/title
-           (or (when-not (get (.-kv e) :block.temp/search?)
-                 (:block.temp/cached-title @(.-cache e)))
-               (let [title (get-block-title e k default-value)]
-                 (vreset! (.-cache e) (assoc @(.-cache e)
-                                             :block.temp/cached-title title))
-                 title))
+           (or
+            (:block.temp/cached-title @(.-cache e))
+            (let [title (get-block-title e k default-value)]
+              (vreset! (.-cache e) (assoc @(.-cache e)
+                                          :block.temp/cached-title title))
+              title))
 
            :block/_parent
            (->> (lookup-entity e k default-value)
@@ -191,7 +189,7 @@
   (let [v @(.-cache this)
         v' (if (:block/title v)
              (assoc v :block/title
-                    (db-content/id-ref->title-ref (:block/title v) (:block/refs this) (:block.temp/search? this)))
+                    (db-content/id-ref->title-ref (:block/title v) (:block/refs this)))
              v)]
     (concat (seq v')
             (seq (.-kv this)))))

+ 42 - 14
deps/db/src/logseq/db/frontend/content.cljs

@@ -4,8 +4,7 @@
             [datascript.core :as d]
             [logseq.common.util :as common-util]
             [logseq.common.util.page-ref :as page-ref]
-            [logseq.db.common.entity-util :as common-entity-util]
-            [logseq.db.frontend.entity-util :as entity-util]))
+            [logseq.db.common.entity-util :as common-entity-util]))
 
 ;; [[uuid]]
 (def id-ref-pattern
@@ -41,22 +40,25 @@
 
 (defn id-ref->title-ref
   "Convert id ref backs to page name refs using refs."
-  [content* refs* search?]
-  (let [refs (filter common-entity-util/page? refs*)
+  [content* refs* & {:keys [replace-block-id?]
+                     :or {replace-block-id? false}}]
+  (let [refs (if replace-block-id?
+               ;; The caller need to handle situations including
+               ;; mutual references and circle references.
+               refs*
+               (filter common-entity-util/page? refs*))
         content (str content*)]
     (if (re-find id-ref-pattern content)
       (reduce
        (fn [content ref]
          (if (:block/title ref)
-           (if (or (entity-util/page? ref) search?)
-             (let [content' (if (not (string/includes? (:block/title ref) " "))
-                              (string/replace content
-                                              (str "#" (page-ref/->page-ref (:block/uuid ref)))
-                                              (str "#" (:block/title ref)))
-                              content)]
-               (string/replace content' (page-ref/->page-ref (:block/uuid ref))
-                               (page-ref/->page-ref (:block/title ref))))
-             content)
+           (let [content' (if (not (string/includes? (:block/title ref) " "))
+                            (string/replace content
+                                            (str "#" (page-ref/->page-ref (:block/uuid ref)))
+                                            (str "#" (:block/title ref)))
+                            content)]
+             (string/replace content' (page-ref/->page-ref (:block/uuid ref))
+                             (page-ref/->page-ref (:block/title ref))))
            content))
        content
        (sort-refs refs))
@@ -125,7 +127,7 @@
   [db item eid]
   (if-let [content (:block/title item)]
     (let [refs (:block/refs (d/entity db eid))]
-      (assoc item :block/title (id-ref->title-ref content refs false)))
+      (assoc item :block/title (id-ref->title-ref content refs)))
     item))
 
 (defn replace-tags-with-id-refs
@@ -164,3 +166,29 @@
     content
     (sort-refs tags))
    (string/trim)))
+
+(defn recur-replace-uuid-in-block-title
+  "Convert id ref (recursively) backs to page name refs, returns replaced title"
+  ([ent]
+   (recur-replace-uuid-in-block-title ent 10))
+  ([ent max-depth]
+   (if (some->> (:block/title ent) (#(re-find id-ref-pattern %)))
+     (let [ref-set (loop [result-refs (:block/refs ent)
+                          current-refs (:block/refs ent)
+                          depth 0]
+                     (if (or (>= depth max-depth) (empty? current-refs))
+                       result-refs
+                       (let [next-refs (set (mapcat :block/refs current-refs))
+                             result-refs' (apply conj result-refs next-refs)]
+                         (if (= (count result-refs') (count result-refs))
+                           result-refs
+                           (recur (apply conj result-refs next-refs) next-refs (inc depth))))))
+           opts {:replace-block-id? true}]
+       (loop [result (id-ref->title-ref (:block/title ent) ref-set opts)
+              last-result nil
+              depth 0]
+         (if (or (>= depth max-depth)
+                 (= last-result result))
+           result
+           (recur (id-ref->title-ref result ref-set opts) result (inc depth)))))
+     (:block/title ent))))

+ 3 - 2
deps/db/src/logseq/db/frontend/malli_schema.cljs

@@ -463,6 +463,7 @@
 (def property-value-placeholder
   [:map
    [:db/ident [:= :logseq.property/empty-placeholder]]
+   [:block/uuid :uuid]
    [:block/tx-id {:optional true} :int]])
 
 (defn entity-dispatch-key [db ent]
@@ -489,10 +490,10 @@
                        :closed-value-block
                        (and (:logseq.property/created-from-property d) (:logseq.property/value d))
                        :property-value-block
-                       (:block/uuid d)
-                       :block
                        (= (:db/ident d) :logseq.property/empty-placeholder)
                        :property-value-placeholder
+                       (:block/uuid d)
+                       :block
                        (:db/ident d)
                        :db-ident-key-value)]
     dispatch-key))

+ 1 - 1
deps/db/src/logseq/db/frontend/schema.cljs

@@ -37,7 +37,7 @@
          (map (juxt :major :minor)
               [(parse-schema-version x) (parse-schema-version y)])))
 
-(def version (parse-schema-version "64.9"))
+(def version (parse-schema-version "64.10"))
 
 (defn major-version
   "Return a number.

+ 2 - 1
deps/db/src/logseq/db/sqlite/create_graph.cljs

@@ -201,7 +201,8 @@
                        (sqlite-util/kv :logseq.kv/graph-initial-schema-version db-schema/version)
                        (sqlite-util/kv :logseq.kv/graph-created-at (common-util/time-ms))
                        ;; Empty property value used by db.type/ref properties
-                       {:db/ident :logseq.property/empty-placeholder}]
+                       {:db/ident :logseq.property/empty-placeholder
+                        :block/uuid (common-uuid/gen-uuid :builtin-block-uuid :logseq.property/empty-placeholder)}]
                        import-type
                        (into (sqlite-util/import-tx import-type)))
         initial-files [{:block/uuid (common-uuid/gen-uuid :builtin-block-uuid "logseq/config.edn")

+ 17 - 12
deps/db/src/logseq/db/sqlite/export.cljs

@@ -814,18 +814,23 @@
 (defn- patch-invalid-keywords
   "Fixes invalids keywords whose name start with a number e.g. :user.property/2ndsomething"
   [m]
-  (walk/postwalk
-   (fn [e]
-     (if (and (keyword? e) (some-> (namespace e) (string/starts-with? "user.")))
-       ;; Copied from create-db-ident-from-name since this may be shortlived
-       (let [sanitized-kw (keyword (namespace e)
-                                   (->> (string/replace-first (name e) #"^(\d)" "NUM-$1")
-                                        (filter #(re-find #"[0-9a-zA-Z*+!_'?<>=-]{1}" %))
-                                        (apply str)))]
-        ;;  (when (not= sanitized-kw e) (prn :sanitize e :-> sanitized-kw))
-         (if (not= sanitized-kw e) sanitized-kw e))
-       e))
-   m))
+  (let [initial-version (:kv/value (first (filter #(= :logseq.kv/graph-initial-schema-version (:db/ident %))
+                                                  (::kv-values m))))]
+    ;; Only ignore patch if initial version is > 64.8 since this fix started with 64.9
+    (if (some-> initial-version (db-schema/compare-schema-version {:major 64 :minor 8}) pos?)
+      m
+      (walk/postwalk
+       (fn [e]
+         (if (and (keyword? e) (some-> (namespace e) (string/starts-with? "user.")))
+           ;; Copied from create-db-ident-from-name since this may be shortlived
+           (let [sanitized-kw (keyword (namespace e)
+                                       (->> (string/replace-first (name e) #"^(\d)" "NUM-$1")
+                                            (filter #(re-find #"[0-9a-zA-Z*+!_'?<>=-]{1}" %))
+                                            (apply str)))]
+             ;; (when (not= sanitized-kw e) (prn :sanitize e :-> sanitized-kw))
+             (if (not= sanitized-kw e) sanitized-kw e))
+           e))
+       m))))
 
 (defn- ensure-export-is-valid
   "Checks that export map is usable by sqlite.build including checking that

+ 1 - 1
resources/forge.config.js

@@ -5,7 +5,7 @@ module.exports = {
   packagerConfig: {
     name: 'Logseq',
     icon: './icons/logseq_big_sur.icns',
-    buildVersion: "85",
+    buildVersion: '87',
     protocols: [
       {
         "protocol": "logseq",

+ 1 - 1
scripts/bump-version.sh

@@ -58,7 +58,7 @@ $SED -i 's/defonce version ".*"/defonce version "'${NEW_VERSION}'"/g' src/main/f
 $SED -i 's/"version": ".*"/"version": "'${NEW_VERSION}'"/g' resources/package.json
 $SED -i 's/versionName ".*"/versionName "'${NEW_VERSION}'"/g' android/app/build.gradle
 $SED -i 's/versionCode .*/versionCode '${NEW_VERSION_CODE}'/g' android/app/build.gradle
-$SED -i 's/buildVersion: .*/buildVersion: '${NEW_VERSION_CODE}',/g' resources/forge.config.js
+$SED -i 's/buildVersion: .*/buildVersion: "'${NEW_VERSION_CODE}'",/g' resources/forge.config.js
 $SED -i 's/MARKETING_VERSION = .*;/MARKETING_VERSION = '${NEW_VERSION}';/g' ios/App/App.xcodeproj/project.pbxproj
 
 git --no-pager diff -U0

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

@@ -13,12 +13,12 @@
             [frontend.handler.notification :as notification]
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.property.file :as property-file]
-            [frontend.util.ref :as ref]
             [frontend.search :as search]
             [frontend.state :as state]
             [frontend.util :as util]
             [frontend.util.cursor :as cursor]
             [frontend.util.file-based.priority :as priority]
+            [frontend.util.ref :as ref]
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [logseq.common.config :as common-config]
@@ -439,11 +439,10 @@
                        (println "draw file created, " path))
                      text)) "Draw a graph with Excalidraw"])
 
-       (when (util/electron?)
-         ["Upload an asset"
-          [[:editor/click-hidden-file-input :id]]
-          "Upload file types like image, pdf, docx, etc.)"
-          :icon/upload])
+       ["Upload an asset"
+        [[:editor/click-hidden-file-input :id]]
+        "Upload file types like image, pdf, docx, etc.)"
+        :icon/upload]
 
        ["Template" [[:editor/input command-trigger nil]
                     [:editor/search-template]] "Insert a created template here"
@@ -473,7 +472,7 @@
         commands)
 
 ;; Allow user to modify or extend, should specify how to extend.
-      
+
       (state/get-commands)
       (when-let [plugin-commands (seq (some->> (state/get-plugins-slash-commands)
                                                (mapv #(vec (concat % [nil :icon/puzzle])))))]

+ 1 - 22
src/main/frontend/common/file/core.cljs

@@ -29,27 +29,6 @@
     :else
     content))
 
-(defn- recur-replace-uuid-in-block-title
-  "Return block-title"
-  [ent max-depth]
-  (let [ref-set (loop [result-refs (:block/refs ent)
-                       current-refs (:block/refs ent)
-                       depth 0]
-                  (if (or (>= depth max-depth) (empty? current-refs))
-                    result-refs
-                    (let [next-refs (set (mapcat :block/refs current-refs))
-                          result-refs' (apply conj result-refs next-refs)]
-                      (if (= (count result-refs') (count result-refs))
-                        result-refs
-                        (recur (apply conj result-refs next-refs) next-refs (inc depth))))))]
-    (loop [result (db-content/id-ref->title-ref (:block/title ent) ref-set true)
-           last-result nil
-           depth 0]
-      (if (or (>= depth max-depth)
-              (= last-result result))
-        result
-        (recur (db-content/id-ref->title-ref result ref-set true) result (inc depth))))))
-
 (defn- transform-content
   [repo db {:block/keys [collapsed? format pre-block? title page properties] :as b} level {:keys [heading-to-list?]} context]
   (let [db-based? (sqlite-util/db-based-graph? repo)
@@ -60,7 +39,7 @@
         markdown? (= :markdown format)
         title (if db-based?
                 ;; replace [[uuid]] with block's content
-                (recur-replace-uuid-in-block-title (d/entity db (:db/id b)) 10)
+                (db-content/recur-replace-uuid-in-block-title (d/entity db (:db/id b)))
                 title)
         content (or title "")
         page-first-child? (= (:db/id b) (ldb/get-first-child db (:db/id page)))

+ 91 - 86
src/main/frontend/components/block.cljs

@@ -656,7 +656,7 @@
 
    All page-names are sanitized except page-name-in-block"
   [state
-   {:keys [contents-page? whiteboard-page? html-export? other-position? show-unique-title? stop-click-event?
+   {:keys [contents-page? whiteboard-page? other-position? show-unique-title? stop-click-event?
            on-context-menu]
     :or {stop-click-event? true}
     :as config}
@@ -701,12 +701,13 @@
                               (reset! *mouse-down? true))))
        :on-pointer-up (fn [e]
                         (when @*mouse-down?
+                          (util/stop e)
                           (state/clear-edit!)
-                          (when-not (or (:disable-click? config)
-                                        (:disable-redirect? config))
+                          (when-not (:disable-click? config)
                             (open-page-ref config page-entity e page-name contents-page?))
                           (reset! *mouse-down? false)))
        :on-key-up (fn [e] (when (and e (= (.-key e) "Enter") (not other-position?))
+                            (util/stop e)
                             (state/clear-edit!)
                             (open-page-ref config page-entity e page-name contents-page?)))}
        on-context-menu
@@ -726,7 +727,7 @@
             (last child)
             (let [{:keys [content children]} (last child)
                   page-name (subs content 2 (- (count content) 2))]
-              (rum/with-key (page-reference html-export? page-name (assoc config :children children) nil) page-name))))
+              (rum/with-key (page-reference (assoc config :children children) page-name nil) page-name))))
         (let [page-component (cond
                                (and label
                                     (string? label)
@@ -943,26 +944,22 @@
           config (assoc config :block entity)]
       (cond
         entity
-        (if (or (ldb/page? entity) (not (:block/page entity)))
-          (let [page-name (some-> (:block/title entity) util/page-name-sanity-lc)
-                whiteboard-page? (model/whiteboard-page? entity)
-                inner (page-inner (assoc config :whiteboard-page? whiteboard-page?) entity children label)
-                modal? (shui-dialog/has-modal?)]
-            (if (and (not (util/mobile?))
-                     (not= page-name (:id config))
-                     (not (false? preview?))
-                     (not disable-preview?)
-                     (not modal?))
-              (page-preview-trigger (assoc config :children inner) entity)
-              inner))
-          (block-reference config (:block/uuid entity)
-                           (if (string? label)
-                             (gp-mldoc/inline->edn label (mldoc/get-default-config :markdown))
-                             label)))
+        (let [page-name (some-> (:block/title entity) util/page-name-sanity-lc)
+              whiteboard-page? (model/whiteboard-page? entity)
+              inner (page-inner (assoc config :whiteboard-page? whiteboard-page?) entity children label)
+              modal? (shui-dialog/has-modal?)]
+          (if (and (not (util/mobile?))
+                   (not= page-name (:id config))
+                   (not (false? preview?))
+                   (not disable-preview?)
+                   (not modal?))
+            (page-preview-trigger (assoc config :children inner) entity)
+            inner))
 
         (and (:block/name page) show-non-exists-page?)
         (page-inner config (merge
-                            {:block/title (:block/name page)
+                            {:block/title (or (:block/title page)
+                                              (:block/name page))
                              :block/name (:block/name page)}
                             page) children label)
 
@@ -977,8 +974,9 @@
 
 (rum/defc page-cp
   [config page]
-  (rum/with-key (page-cp-inner config page)
-    (or (str (:db/id page)) (str (:block/uuid page)) (:block/name page))))
+  (let [id (or (:db/id page) (:block/uuid page) (:block/name page))]
+    (rum/with-key (page-cp-inner config page)
+      (str id))))
 
 (rum/defc asset-reference
   [config title path]
@@ -1068,58 +1066,62 @@
 (declare block-positioned-properties)
 (rum/defc page-reference < rum/reactive db-mixins/query
   "Component for page reference"
-  [html-export? uuid-or-title* {:keys [nested-link? show-brackets? id] :as config} label]
+  [{:keys [html-export? nested-link? show-brackets? id] :as config*} uuid-or-title* label]
   (when uuid-or-title*
     (let [uuid-or-title (if (string? uuid-or-title*)
-                          (string/trim uuid-or-title*)
+                          (let [str-id (string/trim uuid-or-title*)]
+                            (if (util/uuid-string? str-id)
+                              (parse-uuid str-id)
+                              str-id))
                           uuid-or-title*)
-          show-brackets? (if (some? show-brackets?) show-brackets? (state/show-brackets?))
-          contents-page? (= "contents" (string/lower-case (str id)))
-          block* (db/get-page uuid-or-title)
-          block (or (some-> (:db/id block*) db/sub-block) block*)
-          config' (assoc config
-                         :label (mldoc/plain->text label)
-                         :contents-page? contents-page?
-                         :show-icon? true?)
-          asset? (some? (:logseq.property.asset/type block))
-          page? (ldb/page? block)
-          brackets? (and (or show-brackets? nested-link?)
-                         (not html-export?)
-                         (not contents-page?)
-                         page?)]
-      (when-not (= (:db/id block) (:db/id (:block config)))
-        (cond
-          (and asset? (img-audio-video? block))
-          (asset-cp config block)
-
-          (or page? (:block/tags block))
-          [:span.page-reference
-           {:data-ref (str uuid-or-title)}
-           (when brackets?
-             [:span.text-gray-500.bracket page-ref/left-brackets])
-           (when (and (config/db-based-graph?) (ldb/class-instance? (db/entity :logseq.class/Task) block))
-             [:div.inline-block
-              {:style {:margin-right 1
-                       :margin-top -2
-                       :vertical-align "middle"}
-               :on-pointer-down (fn [e]
-                                  (util/stop e))}
-              (block-positioned-properties config block :block-left)])
-           (page-cp config' (if (uuid? uuid-or-title)
-                              {:block/uuid uuid-or-title}
-                              {:block/name uuid-or-title}))
-           (when brackets?
-             [:span.text-gray-500.bracket page-ref/right-brackets])]
-
-          (and (string? uuid-or-title) (string/ends-with? uuid-or-title ".excalidraw"))
-          [:div.draw {:on-click (fn [e]
-                                  (.stopPropagation e))}
-           (excalidraw uuid-or-title (:block/uuid config))]
-
-          :else
-          (page-cp config' (if (uuid? uuid-or-title)
-                             {:block/uuid uuid-or-title}
-                             {:block/name uuid-or-title})))))))
+          self-reference? (when (set? (:ref-set config*))
+                            (contains? (:ref-set config*) uuid-or-title))]
+      (when-not self-reference?
+        (let [config (update config* :ref-set (fn [s]
+                                                (let [bid (:block/uuid (:block config*))]
+                                                  (if (nil? s)
+                                                    #{bid}
+                                                    (conj s bid uuid-or-title)))))
+              show-brackets? (if (some? show-brackets?) show-brackets? (state/show-brackets?))
+              contents-page? (= "contents" (string/lower-case (str id)))
+              block* (db/get-page uuid-or-title)
+              block (or (some-> (:db/id block*) db/sub-block) block*)
+              config' (assoc config
+                             :label (mldoc/plain->text label)
+                             :contents-page? contents-page?
+                             :show-icon? true?)
+              asset? (some? (:logseq.property.asset/type block))
+              brackets? (and (or show-brackets? nested-link?)
+                             (not html-export?)
+                             (not contents-page?))]
+          (when-not (= (:db/id block) (:db/id (:block config)))
+            (cond
+              (and asset? (img-audio-video? block))
+              (asset-cp config block)
+
+              (and (string? uuid-or-title) (string/ends-with? uuid-or-title ".excalidraw"))
+              [:div.draw {:on-click (fn [e]
+                                      (.stopPropagation e))}
+               (excalidraw uuid-or-title (:block/uuid config))]
+
+              :else
+              [:span.page-reference
+               {:data-ref (str uuid-or-title)}
+               (when brackets?
+                 [:span.text-gray-500.bracket page-ref/left-brackets])
+               (when (and (config/db-based-graph?) (ldb/class-instance? (db/entity :logseq.class/Task) block))
+                 [:div.inline-block
+                  {:style {:margin-right 1
+                           :margin-top -2
+                           :vertical-align "middle"}
+                   :on-pointer-down (fn [e]
+                                      (util/stop e))}
+                  (block-positioned-properties config block :block-left)])
+               (page-cp config' (if (uuid? uuid-or-title)
+                                  {:block/uuid uuid-or-title}
+                                  {:block/name uuid-or-title}))
+               (when brackets?
+                 [:span.text-gray-500.bracket page-ref/right-brackets])])))))))
 
 (defn- latex-environment-content
   [name option content]
@@ -1340,13 +1342,18 @@
          (set-block! block)))
      [])
     (when-not self-reference?
-      (if block
+      (cond
+        (config/db-based-graph?)
+        (page-reference config block-id label)
+
+        block
         (let [config' (update config :ref-set (fn [s]
                                                 (let [bid (:block/uuid (:block config))]
                                                   (if (nil? s)
                                                     #{bid}
                                                     (conj s bid block-id)))))]
           (block-reference-aux config' block label))
+        :else
         (invalid-node-ref block-id)))))
 
 (defn- render-macro
@@ -1475,7 +1482,7 @@
       (block-reference config id label))
 
     (not (string/includes? s "."))
-    (page-reference (:html-export? config) s config label)
+    (page-reference config s label)
 
     (path/protocol-url? s)
     (->elem :a {:href s
@@ -1507,9 +1514,9 @@
        (map-inline config label)))
 
     :else
-    (page-reference (:html-export? config) s config label)))
+    (page-reference config s label)))
 
-(defn- link-cp [config html-export? link]
+(defn- link-cp [config link]
   (let [{:keys [url label title metadata full_text]} link]
     (match url
       ["Block_ref" id]
@@ -1533,7 +1540,7 @@
           (let [label* (if (seq (mldoc/plain->text label)) label nil)]
             (if (and (string? page) (string/blank? page))
               [:span (ref/->page-ref page)]
-              (page-reference (:html-export? config) page config label*)))))
+              (page-reference config page label*)))))
 
       ["Embed_data" src]
       (image-link config url src nil metadata full_text)
@@ -1555,7 +1562,7 @@
                 block (db/entity [:block/uuid id])]
             (if (:block/pre-block? block)
               (let [page (:block/page block)]
-                (page-reference html-export? (:block/name page) config label))
+                (page-reference config (:block/name page) label))
               (block-reference config (:link path) label)))
 
           (= protocol "file")
@@ -1977,7 +1984,7 @@
     (nested-link config html-export? link)
 
     ["Link" link]
-    (link-cp config html-export? link)
+    (link-cp config link)
 
     [(:or "Verbatim" "Code") s]
     [:code s]
@@ -2052,7 +2059,6 @@
         selected? (contains? selected block-id)]
     (when-not selected?
       (util/clear-selection!)
-      (state/conj-selection-block! (gdom/getElement block-id) :down)
       (editor-handler/highlight-block! uuid)))
 
   (editor-handler/block->data-transfer! uuid event false)
@@ -2171,7 +2177,7 @@
                                         (reset! *bullet-dragging? true)
                                         (util/stop-propagation event)
                                         (bullet-drag-start event block uuid block-id))
-                       :on-drag-end (fn [_]
+                       :on-drag-end (fn [_e]
                                       (reset! *bullet-dragging? false))
                        :blockid (str uuid)
                        :class (str (when collapsed? "bullet-closed")
@@ -3039,7 +3045,6 @@
      (block-content config block edit-input-id block-id *show-query?))))
 
 (rum/defcs ^:large-vars/cleanup-todo block-content-or-editor < rum/reactive
-  (rum/local false ::hover?)
   [state config {:block/keys [uuid] :as block} {:keys [edit-input-id block-id edit? hide-block-refs-count? refs-count *hide-block-refs? *show-query?]}]
   (let [format (if (config/db-based-graph? (state/get-current-repo))
                  :markdown
@@ -3076,7 +3081,10 @@
                         :format format}
                        edit-input-id
                        config))]
-         [:div.flex.flex-1.w-full.block-content-wrapper {:style {:display "flex"}}
+         [:div.flex.flex-1.w-full.block-content-wrapper
+          {:style {:display "flex"}}
+          (when-let [actions-cp (:page-title-actions-cp config)]
+            (actions-cp block))
           (block-content-with-error config block edit-input-id block-id *show-query? editor-box)
 
           (when (and (not hide-block-refs-count?)
@@ -3182,7 +3190,6 @@
                   rest)
         config (assoc config
                       :breadcrumb? true
-                      :disable-redirect? true
                       :disable-preview? true
                       :stop-click-event? false)]
     (when (seq parents)
@@ -3615,8 +3622,6 @@
 
         [:div.flex.flex-col.w-full
          [:div.block-main-content.flex.flex-row.gap-2
-          (when-let [actions-cp (:page-title-actions-cp config)]
-            (actions-cp block))
           (when page-icon
             page-icon)
 

+ 8 - 2
src/main/frontend/components/block.css

@@ -524,10 +524,16 @@
   }
 }
 
-.block-main-content {
+.ls-page-title-container .block-content-wrapper {
+  .ls-page-title-actions {
+    @apply absolute -top-4 opacity-0;
+    left: -2px;
+  }
+
   &:hover {
-    & > .db-page-title-actions {
+    & > .ls-page-title-actions {
       @apply delay-300 transition-opacity opacity-100;
+
     }
   }
 }

+ 1 - 1
src/main/frontend/components/class.cljs

@@ -13,7 +13,7 @@
     (when (seq children)
       [:ul
        (for [child (sort-by :block/title children)]
-         (let [title [:li.ml-2 (block/page-reference false (:block/uuid child) {:show-brackets? false} nil)]]
+         (let [title [:li.ml-2 (block/page-reference {:show-brackets? false} (:block/uuid child) nil)]]
            (if (seq (:logseq.property.class/_extends child))
              (ui/foldable
               title

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

@@ -933,8 +933,13 @@
   nil)
 
 (defn- on-mouse-up
-  [_e]
-  (editor-handler/show-action-bar!))
+  [e]
+  (when-not (or (.closest (.-target e) ".block-control-wrap")
+                (.closest (.-target e) "button")
+                (.closest (.-target e) "input")
+                (.closest (.-target e) "textarea")
+                (.closest (.-target e) "a"))
+    (editor-handler/show-action-bar!)))
 
 (rum/defcs ^:large-vars/cleanup-todo root-container < rum/reactive
   (mixins/event-mixin

+ 26 - 23
src/main/frontend/components/editor.cljs

@@ -134,6 +134,20 @@
                                                  :other-attrs {:block/link (:db/id page')}}))))
     (page-handler/on-chosen-handler input id pos format)))
 
+(defn- matched-pages-with-new-page [partial-matched-pages db-tag? q]
+  (if (or (db/page-exists? q (if db-tag?
+                               #{:logseq.class/Tag}
+                               ;; Page existence here should be the same as entity-util/page?.
+                                ;; Don't show 'New page' if a page has any of these tags
+                               db-class/page-classes))
+          (and db-tag? (some ldb/class? (:block/_alias (db/get-page q)))))
+    partial-matched-pages
+    (if db-tag?
+      (concat [{:block/title (str (t :new-tag) " " q)}]
+              partial-matched-pages)
+      (cons {:block/title (str (t :new-page) " " q)}
+            partial-matched-pages))))
+
 (rum/defc page-search-aux
   [id format embed? db-tag? q current-pos input pos]
   (let [db-based? (config/db-based-graph? (state/get-current-repo))
@@ -157,25 +171,11 @@
                                          date/nlp-pages)
                                     (take 10))))
                            ;; reorder, shortest and starts-with first.
-                           (let [matched-pages-with-new-page
-                                 (fn [partial-matched-pages]
-                                   (if (or (db/page-exists? q (if db-tag?
-                                                                #{:logseq.class/Tag}
-                                                                ;; Page existence here should be the same as entity-util/page?.
-                                                                ;; Don't show 'New page' if a page has any of these tags
-                                                                db-class/page-classes))
-                                           (and db-tag? (some ldb/class? (:block/_alias (db/get-page q)))))
-                                     partial-matched-pages
-                                     (if db-tag?
-                                       (concat [{:block/title (str (t :new-tag) " " q)}]
-                                               partial-matched-pages)
-                                       (cons {:block/title (str (t :new-page) " " q)}
-                                             partial-matched-pages))))]
-                             (if (and (seq matched-pages)
-                                      (gstring/caseInsensitiveStartsWith (:block/title (first matched-pages)) q))
-                               (cons (first matched-pages)
-                                     (matched-pages-with-new-page (rest matched-pages)))
-                               (matched-pages-with-new-page matched-pages))))]
+                           (if (and (seq matched-pages)
+                                    (gstring/caseInsensitiveStartsWith (:block/title (first matched-pages)) q))
+                             (cons (first matched-pages)
+                                   (matched-pages-with-new-page (rest matched-pages) db-tag? q))
+                             (matched-pages-with-new-page matched-pages db-tag? q)))]
       [:<>
        (ui/auto-complete
         matched-pages'
@@ -184,7 +184,9 @@
                         (page-handler/page-not-exists-handler input id q current-pos))
          :item-render (fn [block _chosen?]
                         (let [block' (if-let [id (:block/uuid block)]
-                                       (or (db/entity [:block/uuid id]) block)
+                                       (if-let [e (db/entity [:block/uuid id])]
+                                         (assoc e :block/title (:block/title block))
+                                         block)
                                        block)]
                           [:div.flex.flex-col
                            (when (and (:block/uuid block') (:block/parent block'))
@@ -218,10 +220,11 @@
                                  (ui/icon "letter-n" {:size 14}))])
 
                             (let [title (if db-tag?
-                                          (let [target (first (:block/_alias block'))]
+                                          (let [target (first (:block/_alias block'))
+                                                title (:block/title block)]
                                             (if (ldb/class? target)
-                                              (str (:block/title block') " -> alias: " (:block/title target))
-                                              (:block/title block')))
+                                              (str title " -> alias: " (:block/title target))
+                                              title))
                                           (block-handler/block-unique-title block'))]
                               (search-handler/highlight-exact-query title q))]]))
          :empty-placeholder [:div.text-gray-500.text-sm.px-4.py-2 (if db-tag?

+ 49 - 46
src/main/frontend/components/export.cljs

@@ -12,7 +12,6 @@
             [frontend.handler.export.opml :as export-opml]
             [frontend.handler.export.text :as export-text]
             [frontend.handler.notification :as notification]
-            [frontend.idb :as idb]
             [frontend.image :as image]
             [frontend.mobile.util :as mobile-util]
             [frontend.state :as state]
@@ -33,51 +32,55 @@
     [:div.flex.flex-col.gap-4
      [:div.font-medium.opacity-50
       "Schedule backup"]
-     (if backup-folder
-       [:div.flex.flex-row.items-center.gap-1.text-sm
-        [:div.opacity-50 (str "Backup folder:")]
-        backup-folder
-        (shui/button
-         {:variant :ghost
-          :class "!px-1 !py-1"
-          :title "Change backup folder"
-          :on-click (fn []
-                      (p/do!
-                       (db/transact! [[:db/retractEntity :logseq.kv/graph-backup-folder]])
-                       (reset! *backup-folder nil)))
-          :size :sm}
-         (ui/icon "edit"))]
-       (shui/button
-        {:variant :default
-         :on-click (fn []
-                     (p/let [result (utils/openDirectory #js {:mode "readwrite"})
-                             handle (first result)
-                             folder-name (.-name handle)]
-                       (idb/set-item!
-                        (str "handle/" (js/btoa repo) "/" folder-name) handle)
-                       (db/transact! [(ldb/kv :logseq.kv/graph-backup-folder folder-name)])
-                       (reset! *backup-folder folder-name)))}
-        "Set backup folder first"))
-     [:div.opacity-50.text-sm
-      "Backup will be created every hour."]
+     (if (utils/nfsSupported)
+       [:<>
+        (if backup-folder
+          [:div.flex.flex-row.items-center.gap-1.text-sm
+           [:div.opacity-50 (str "Backup folder:")]
+           backup-folder
+           (shui/button
+            {:variant :ghost
+             :class "!px-1 !py-1"
+             :title "Change backup folder"
+             :on-click (fn []
+                         (p/do!
+                          (db/transact! [[:db/retractEntity :logseq.kv/graph-backup-folder]])
+                          (reset! *backup-folder nil)))
+             :size :sm}
+            (ui/icon "edit"))]
+          (shui/button
+           {:variant :default
+            :on-click (fn []
+                        (p/let [[folder-name _handle] (export/choose-backup-folder repo)]
+                          (reset! *backup-folder folder-name)))}
+           "Set backup folder first"))
+        [:div.opacity-50.text-sm
+         "Backup will be created every hour."]
 
-     (when backup-folder
-       (shui/button
-        {:variant :default
-         :on-click (fn []
-                     (->
-                      (p/let [result (export/backup-db-graph repo)]
-                        (case result
-                          true
-                          (notification/show! "Backup successful!" :success)
-                          :graph-not-changed
-                          (notification/show! "Graph has not been updated since last export." :success)
-                          nil)
-                        (export/auto-db-backup! repo {:backup-now? false}))
-                      (p/catch (fn [error]
-                                 (println "Failed to backup.")
-                                 (js/console.error error)))))}
-        "Backup now"))]))
+        (when backup-folder
+          (shui/button
+           {:variant :default
+            :on-click (fn []
+                        (->
+                         (p/let [result (export/backup-db-graph repo :set-folder)]
+                           (case result
+                             true
+                             (notification/show! "Backup successful!" :success)
+                             :graph-not-changed
+                             (notification/show! "Graph has not been updated since last export." :success)
+                             nil)
+                           (export/auto-db-backup! repo {:backup-now? false}))
+                         (p/catch (fn [error]
+                                    (println "Failed to backup.")
+                                    (js/console.error error)))))}
+           "Backup now"))]
+       [:div
+        [:span "Your browser doesn't support "]
+        [:a
+         {:href "https://developer.chrome.com/docs/capabilities/web-apis/file-system-access"
+          :target "_blank"}
+         "The File System Access API"]
+        [:span ", please switch to a Chromium-based browser."]])]))
 
 (rum/defc export
   []
@@ -133,7 +136,7 @@
             "Export debug transit file"]
            [:p.text-sm.opacity-70.mb-0 "Any sensitive data will be removed in the exported transit file, you can send it to us for debugging."]])
 
-        (when (and db-based? util/web-platform? (utils/nfsSupported))
+        (when (and db-based? util/web-platform?)
           [:div
            [:hr]
            (auto-backup)])]])))

+ 1 - 4
src/main/frontend/components/file_based/hierarchy.cljs

@@ -60,10 +60,7 @@
                (when (and (string? page) page)
                  (let [full-page (->> (take (inc idx) namespace)
                                       util/string-join-path)]
-                   (block/page-reference false
-                                         full-page
-                                         {}
-                                         page))))
+                   (block/page-reference {} full-page page))))
              (interpose [:span.mx-2.opacity-30 "/"]))])]
         {:default-collapsed? false
          :title-trigger? true})])))

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

@@ -402,8 +402,10 @@
             (str "collab-" current-repo))
           (rtc-indicator/indicator)])
 
-       (when (user-handler/logged-in?)
-         (rtc-indicator/downloading-detail))
+      (when (user-handler/logged-in?)
+        (rtc-indicator/downloading-detail))
+      (when (user-handler/logged-in?)
+        (rtc-indicator/uploading-detail))
 
        (when (and current-repo
                   (not (config/demo-graph? current-repo))

+ 4 - 3
src/main/frontend/components/icon.cljs

@@ -29,9 +29,10 @@
         opts (dissoc opts :color?)
         item (cond
                (and (= :emoji (:type icon')) (:id icon'))
-               [:em-emoji (merge {:id (:id icon')
-                                  :style {:line-height 1}}
-                                 opts)]
+               [:span.ui__icon
+                [:em-emoji (merge {:id (:id icon')
+                                   :style {:line-height 1}}
+                                  opts)]]
 
                (and (= :tabler-icon (:type icon')) (:id icon'))
                (ui/icon (:id icon') opts))]

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

@@ -420,11 +420,11 @@
 
 (rum/defc db-page-title-actions
   [page]
-  [:div.absolute.-top-4.left-0.opacity-0.db-page-title-actions
+  [:div.ls-page-title-actions
    [:div.flex.flex-row.items-center.gap-2
     (when-not (:logseq.property/icon (db/entity (:db/id page)))
       (shui/button
-       {:variant :outline
+       {:variant :ghost
         :size :sm
         :class "px-2 py-0 h-6 text-xs text-muted-foreground"
         :on-click (fn [e]
@@ -434,7 +434,7 @@
        "Add icon"))
 
     (shui/button
-     {:variant :outline
+     {:variant :ghost
       :size :sm
       :class "px-2 py-0 h-6 text-xs text-muted-foreground"
       :on-click (fn [e]

+ 1 - 1
src/main/frontend/components/page.css

@@ -92,7 +92,7 @@
     }
   }
 
-  .db-page-title-actions {
+  .ls-page-title-actions {
     &:has(button[data-popup-active]) {
       @apply opacity-100;
     }

+ 6 - 8
src/main/frontend/components/property/value.cljs

@@ -33,6 +33,7 @@
             [lambdaisland.glogi :as log]
             [logseq.common.util.macro :as macro-util]
             [logseq.db :as ldb]
+            [logseq.db.frontend.content :as db-content]
             [logseq.db.frontend.entity-util :as entity-util]
             [logseq.db.frontend.property :as db-property]
             [logseq.db.frontend.property.type :as db-property-type]
@@ -748,7 +749,7 @@
                              id (:db/id node)
                              [header label] (if (integer? id)
                                               (let [node-title (if (seq (:logseq.property/classes property))
-                                                                 (:block/title node)
+                                                                 (db-content/recur-replace-uuid-in-block-title node)
                                                                  (block-handler/block-unique-title node))
                                                     title (subs node-title 0 256)
                                                     node (or (db/entity id) node)
@@ -756,7 +757,7 @@
                                                     header (when-not (db/page? node)
                                                              (when-let [breadcrumb (state/get-component :block/breadcrumb)]
                                                                [:div.text-xs.opacity-70
-                                                                (breadcrumb {:search? true} (state/get-current-repo) (:block/uuid block) {})]))
+                                                                (breadcrumb {:search? true} (state/get-current-repo) (:block/uuid node) {})]))
                                                     label [:div.flex.flex-row.items-center.gap-1
                                                            (when-not (or (:logseq.property/classes property)
                                                                          (= (:db/ident property) :block/tags))
@@ -1092,10 +1093,7 @@
        (closed-value-item value opts)
 
        (or (entity-util/page? value)
-           (and (seq (:block/tags value))
-                ;; FIXME: page-cp should be renamed to node-cp and
-                ;; support this case and maybe other complex cases.
-                (not (string/includes? (:block/title value) "[["))))
+           (seq (:block/tags value)))
        (when value
          (let [opts {:disable-preview? true
                      :tag? tag?
@@ -1158,7 +1156,7 @@
     (if editing?
       (popup-content nil)
       (let [show! (fn [e]
-                    (util/stop e)
+                    (state/clear-selection!)
                     (let [target (when e (.-target e))]
                       (when-not (or config/publishing?
                                     (util/shift-key? e)
@@ -1173,7 +1171,7 @@
          {:ref *el
           :id trigger-id
           :tabIndex 0
-          :on-click show!
+          :on-pointer-down show!
           :on-key-down (fn [e]
                          (case (util/ekey e)
                            ("Backspace" "Delete")

+ 86 - 73
src/main/frontend/components/right_sidebar.cljs

@@ -38,10 +38,11 @@
 (rum/defc block-cp < rum/reactive
   [repo idx block]
   (let [id (:block/uuid block)]
-    (page/page-cp {:parameters  {:path {:name (str id)}}
-                   :sidebar?    true
-                   :sidebar/idx idx
-                   :repo        repo})))
+    [:div.mt-2
+     (page/page-cp {:parameters  {:path {:name (str id)}}
+                    :sidebar?    true
+                    :sidebar/idx idx
+                    :repo        repo})]))
 
 (defn get-scrollable-container
   []
@@ -68,77 +69,89 @@
                          :sidebar-key sidebar-key} repo block-id {:indent? false})]
      (block-cp repo idx block)]))
 
+(rum/defc search-title < rum/reactive
+  [*input]
+  (let [input (rum/react *input)
+        input' (if (string/blank? input) "Blank input" input)]
+    [:span.overflow-hidden.text-ellipsis input']))
+
+(rum/defc sidebar-search
+  [repo block-type init-key input *input]
+  (rum/with-key
+    (cmdk/cmdk-block {:initial-input input
+                      :sidebar? true
+                      :on-input-change (fn [new-value]
+                                         (reset! *input new-value))
+                      :on-input-blur (fn [new-value]
+                                       (state/sidebar-replace-block! [repo input block-type]
+                                                                     [repo new-value block-type]))})
+    (str init-key)))
+
 (defn- <build-sidebar-item
   [repo idx db-id block-type *db-id init-key]
-  (p/do!
-   (db-async/<get-block repo db-id)
-   (let [lookup (cond
-                  (integer? db-id) db-id
-                  (uuid? db-id) [:block/uuid db-id]
-                  :else nil)
-         entity (when lookup (db/entity repo lookup))
-         page? (ldb/page? entity)
-         block-render (fn []
-                        (when entity
-                          (if page?
-                            [[:.flex.items-center.page-title.gap-1
-                              (icon/get-node-icon-cp entity {:class "text-md"})
-                              [:span.overflow-hidden.text-ellipsis (:block/title entity)]]
-                             (page-cp repo (str (:block/uuid entity)))]
-                            (block-with-breadcrumb repo entity idx [repo db-id block-type] false))))]
-     (case (keyword block-type)
-       :contents
-       (when-let [page (db/get-page "Contents")]
-         [[:.flex.items-center (ui/icon "list-details" {:class "text-md mr-2"}) (t :right-side-bar/contents)]
-          (page-cp repo (str (:block/uuid page)))])
-
-       :help
-       [[:.flex.items-center (ui/icon "help" {:class "text-md mr-2"}) (t :right-side-bar/help)] (onboarding/help)]
-
-       :page-graph
-       [[:.flex.items-center (ui/icon "hierarchy" {:class "text-md mr-2"}) (t :right-side-bar/page-graph)]
-        (page/page-graph)]
-
-       :block-ref
-       (let [lookup (if (integer? db-id) db-id [:block/uuid db-id])]
-         (when-let [block (db/entity repo lookup)]
-           [(t :right-side-bar/block-ref)
-            (block-with-breadcrumb repo block idx [repo db-id block-type] true)]))
-
-       :block
-       (block-render)
-
-       :page
-       (block-render)
-
-       :search
-       [[:.flex.items-center.page-title
-         (ui/icon "search" {:class "text-md mr-2"})
-         (let [input (rum/react *db-id)
-               input' (if (string/blank? input) "Blank input" input)]
-           [:span.overflow-hidden.text-ellipsis input'])]
-        (rum/with-key
-          (cmdk/cmdk-block {:initial-input db-id
-                            :sidebar? true
-                            :on-input-change (fn [new-value]
-                                               (reset! *db-id new-value))
-                            :on-input-blur (fn [new-value]
-                                             (state/sidebar-replace-block! [repo db-id block-type]
-                                                                           [repo new-value block-type]))})
-          (str init-key))]
-
-       :shortcut-settings
-       [[:.flex.items-center (ui/icon "command" {:class "text-md mr-2"}) (t :help/shortcuts)]
-        (shortcut-settings)]
-       :rtc
-       [[:.flex.items-center (ui/icon "cloud" {:class "text-md mr-2"}) "(Dev) RTC"]
-        (rtc-debug-ui/rtc-debug-ui)]
-
-       :profiler
-       [[:.flex.items-center (ui/icon "cloud" {:class "text-md mr-2"}) "(Dev) Profiler"]
-        (profiler/profiler)]
-
-       ["" [:span]]))))
+  (->
+   (p/do!
+    (when-not (contains? #{:contents :search} block-type)
+      (db-async/<get-block repo db-id))
+    (let [lookup (cond
+                   (integer? db-id) db-id
+                   (uuid? db-id) [:block/uuid db-id]
+                   :else nil)
+          entity (when lookup (db/entity repo lookup))
+          page? (ldb/page? entity)
+          block-render (fn []
+                         (when entity
+                           (if page?
+                             [[:.flex.items-center.page-title.gap-1
+                               (icon/get-node-icon-cp entity {:class "text-md"})
+                               [:span.overflow-hidden.text-ellipsis (:block/title entity)]]
+                              (page-cp repo (str (:block/uuid entity)))]
+                             (block-with-breadcrumb repo entity idx [repo db-id block-type] false))))]
+      (case (keyword block-type)
+        :contents
+        (when-let [page (db/get-page "Contents")]
+          [[:.flex.items-center (ui/icon "list-details" {:class "text-md mr-2"}) (t :right-side-bar/contents)]
+           (page-cp repo (str (:block/uuid page)))])
+
+        :help
+        [[:.flex.items-center (ui/icon "help" {:class "text-md mr-2"}) (t :right-side-bar/help)] (onboarding/help)]
+
+        :page-graph
+        [[:.flex.items-center (ui/icon "hierarchy" {:class "text-md mr-2"}) (t :right-side-bar/page-graph)]
+         (page/page-graph)]
+
+        :block-ref
+        (let [lookup (if (integer? db-id) db-id [:block/uuid db-id])]
+          (when-let [block (db/entity repo lookup)]
+            [(t :right-side-bar/block-ref)
+             (block-with-breadcrumb repo block idx [repo db-id block-type] true)]))
+
+        :block
+        (block-render)
+
+        :page
+        (block-render)
+
+        :search
+        [[:.flex.items-center.page-title
+          (ui/icon "search" {:class "text-md mr-2"})
+          (search-title *db-id)]
+         (sidebar-search repo block-type init-key db-id *db-id)]
+
+        :shortcut-settings
+        [[:.flex.items-center (ui/icon "command" {:class "text-md mr-2"}) (t :help/shortcuts)]
+         (shortcut-settings)]
+        :rtc
+        [[:.flex.items-center (ui/icon "cloud" {:class "text-md mr-2"}) "(Dev) RTC"]
+         (rtc-debug-ui/rtc-debug-ui)]
+
+        :profiler
+        [[:.flex.items-center (ui/icon "cloud" {:class "text-md mr-2"}) "(Dev) Profiler"]
+         (profiler/profiler)]
+
+        ["" [:span]])))
+   (p/catch (fn [error]
+              (js/console.error error)))))
 
 (defonce *drag-to
   (atom nil))

+ 55 - 47
src/main/frontend/components/rtc/indicator.cljs

@@ -1,7 +1,6 @@
 (ns frontend.components.rtc.indicator
   "RTC state indicator"
-  (:require [cljs-time.core :as t]
-            [clojure.pprint :as pprint]
+  (:require [clojure.pprint :as pprint]
             [frontend.common.missionary :as c.m]
             [frontend.db :as db]
             [frontend.flows :as flows]
@@ -138,19 +137,25 @@
              pprint/pprint
              with-out-str)]])]))
 
-(defn- downloading?
-  [detail-info]
-  (when-let [{:keys [created-at sub-type]} (first (:download-logs detail-info))]
-    (and (not= :download-completed sub-type)
-         (> 600 ;; 10min
-            (/ (- (t/now) created-at) 1000)))))
-
-(defn- uploading?
-  [detail-info]
-  (when-let [{:keys [created-at sub-type]} (first (:upload-logs detail-info))]
-    (and (not= :upload-completed sub-type)
-         (> 600
-            (/ (- (t/now) created-at) 1000)))))
+(rum/defc indicator
+  []
+  (let [detail-info                 (hooks/use-flow-state (m/watch *detail-info))
+        _                           (hooks/use-flow-state flows/current-login-user-flow)
+        online?                     (hooks/use-flow-state flows/network-online-event-flow)
+        rtc-state                   (:rtc-state detail-info)
+        unpushed-block-update-count (:pending-local-ops detail-info)
+        {:keys [local-tx remote-tx]} detail-info]
+    [:div.cp__rtc-sync
+     [:div.hidden {"data-testid" "rtc-tx"} (pr-str {:local-tx local-tx :remote-tx remote-tx})]
+     [:div.cp__rtc-sync-indicator.flex.flex-row.items-center.gap-1
+      (shui/button-ghost-icon :cloud
+                              {:on-click #(shui/popup-show! (.-target %)
+                                                            (details online?)
+                                                            {:align "end"})
+                               :class (util/classnames [{:cloud true
+                                                         :on (and online? (= :open rtc-state))
+                                                         :idle (and online? (= :open rtc-state) (zero? unpushed-block-update-count))
+                                                         :queuing (pos? unpushed-block-update-count)}])})]]))
 
 (def ^:private *accumulated-download-logs (atom []))
 (c.m/run-background-task
@@ -163,6 +168,17 @@
         (swap! *accumulated-download-logs (fn [logs] (take 20 (conj logs log)))))))
   rtc-flows/rtc-download-log-flow))
 
+(def ^:private *accumulated-upload-logs (atom []))
+(c.m/run-background-task
+ ::update-accumulated-upload-logs
+ (m/reduce
+  (fn [_ log]
+    (when log
+      (if (= :upload-completed (:sub-type log))
+        (reset! *accumulated-upload-logs [])
+        (swap! *accumulated-upload-logs (fn [logs] (take 20 (conj logs log)))))))
+  rtc-flows/rtc-upload-log-flow))
+
 (defn- accumulated-logs-flow
   [*acc-logs]
   (->> (m/watch *acc-logs)
@@ -181,39 +197,14 @@
        (for [log download-logs]
          [:div (:message log)])])))
 
-(rum/defc indicator
+(rum/defc uploading-logs
   []
-  (let [detail-info                 (hooks/use-flow-state (m/watch *detail-info))
-        _                           (hooks/use-flow-state flows/current-login-user-flow)
-        online?                     (hooks/use-flow-state flows/network-online-event-flow)
-        uploading?'                 (uploading? detail-info)
-        downloading?'               (downloading? detail-info)
-        rtc-state                   (:rtc-state detail-info)
-        unpushed-block-update-count (:pending-local-ops detail-info)
-        {:keys [local-tx remote-tx]} detail-info]
-    [:div.cp__rtc-sync
-     [:div.hidden {"data-testid" "rtc-tx"} (pr-str {:local-tx local-tx :remote-tx remote-tx})]
-     [:div.cp__rtc-sync-indicator.flex.flex-row.items-center.gap-1
-      (when downloading?'
-        (shui/button
-         {:class   "opacity-50"
-          :variant :ghost
-          :size    :sm}
-         "Downloading..."))
-      (when uploading?'
-        (shui/button
-         {:class   "opacity-50"
-          :variant :ghost
-          :size    :sm}
-         "Uploading..."))
-      (shui/button-ghost-icon :cloud
-                              {:on-click #(shui/popup-show! (.-target %)
-                                                            (details online?)
-                                                            {:align "end"})
-                               :class (util/classnames [{:cloud true
-                                                         :on (and online? (= :open rtc-state))
-                                                         :idle (and online? (= :open rtc-state) (zero? unpushed-block-update-count))
-                                                         :queuing (pos? unpushed-block-update-count)}])})]]))
+  (let [upload-logs-flow (accumulated-logs-flow *accumulated-upload-logs)
+        upload-logs (hooks/use-flow-state upload-logs-flow)]
+    (when (seq upload-logs)
+      [:div
+       (for [log upload-logs]
+         [:div (:message log)])])))
 
 (def ^:private downloading?-flow
   (->> rtc-flows/rtc-download-log-flow
@@ -231,3 +222,20 @@
                                    (downloading-logs)
                                    {:align "end"})}
      "Downloading...")))
+
+(def ^:private upload?-flow
+  (->> rtc-flows/rtc-upload-log-flow
+       (m/eduction (map (fn [log] (not= :upload-completed (:sub-type log)))))
+       (c.m/continue-flow false)))
+
+(rum/defc uploading-detail
+  []
+  (when (true? (hooks/use-flow-state upload?-flow))
+    (shui/button
+     {:class   "opacity-50"
+      :variant :ghost
+      :size    :sm
+      :on-click #(shui/popup-show! (.-target %)
+                                   (uploading-logs)
+                                   {:align "end"})}
+     "Uploading...")))

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

@@ -99,7 +99,7 @@ independent of format as format specific heading characters are stripped"
                   (let [block (db-utils/entity repo block-id)
                         ref-tags (distinct (concat (:block/tags block) (:block/refs block)))]
                     (= (-> block-content
-                           (db-content/id-ref->title-ref ref-tags true)
+                           (db-content/id-ref->title-ref ref-tags)
                            (db-content/content-id-ref->page ref-tags)
                            heading-content->route-name)
                        (string/lower-case external-content))))

+ 10 - 8
src/main/frontend/handler/editor.cljs

@@ -1252,15 +1252,22 @@
 
 (defonce *action-bar-timeout (atom nil))
 
+(defn popup-exists?
+  [id]
+  (some->> (shui-popup/get-popups)
+           (some #(some-> % (:id) (str) (string/includes? (str id))))))
+
 (defn show-action-bar!
   [& {:keys [delay]
       :or {delay 200}}]
-  (when (config/db-based-graph?)
+  (when (and (config/db-based-graph?) (not (popup-exists? :selection-action-bar)))
     (when-let [timeout @*action-bar-timeout]
       (js/clearTimeout timeout))
     (state/pub-event! [:editor/hide-action-bar])
-    (let [timeout (js/setTimeout #(state/pub-event! [:editor/show-action-bar]) delay)]
-      (reset! *action-bar-timeout timeout))))
+    (when (seq (remove (fn [b] (dom/has-class? b "ls-table-cell"))
+                       (state/get-selection-blocks)))
+      (let [timeout (js/setTimeout #(state/pub-event! [:editor/show-action-bar]) delay)]
+        (reset! *action-bar-timeout timeout)))))
 
 (defn- select-block-up-down
   [direction]
@@ -3333,11 +3340,6 @@
           (cursor/select-up-down input direction anchor cursor-rect)))
       (select-block-up-down direction))))
 
-(defn popup-exists?
-  [id]
-  (some->> (shui-popup/get-popups)
-           (some #(some-> % (:id) (str) (string/includes? (str id))))))
-
 (defn editor-commands-popup-exists?
   []
   (popup-exists? "editor.commands"))

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

@@ -196,14 +196,15 @@
 
 (defmethod handle :capture-error [[_ {:keys [error payload]}]]
   (let [[user-uuid graph-uuid tx-id] @sync/graphs-txid
-        payload (assoc payload
-                       :user-id user-uuid
-                       :graph-id graph-uuid
-                       :tx-id tx-id
-                       :db-based (config/db-based-graph? (state/get-current-repo))
-                       :schema-version (str db-schema/version)
-                       :db-schema-version (when-let [db (frontend.db/get-db)]
-                                            (str (:kv/value (frontend.db/entity db :logseq.kv/schema-version)))))]
+        payload (merge
+                 {:schema-version (str db-schema/version)
+                  :db-schema-version (when-let [db (frontend.db/get-db)]
+                                       (str (:kv/value (frontend.db/entity db :logseq.kv/schema-version))))
+                  :user-id user-uuid
+                  :graph-id graph-uuid
+                  :tx-id tx-id
+                  :db-based (config/db-based-graph? (state/get-current-repo))}
+                 payload)]
     (Sentry/captureException error
                              (bean/->js {:tags payload}))))
 

+ 26 - 4
src/main/frontend/handler/export.cljs

@@ -250,11 +250,33 @@
                (.remove (.-handle file))))
            old-versioned-files)))
 
-(defn backup-db-graph
+(defn choose-backup-folder
   [repo]
+  (p/let [result (utils/openDirectory #js {:mode "readwrite"})
+          handle (first result)
+          folder-name (.-name handle)]
+    (js/console.dir handle)
+    (idb/set-item!
+     (str "handle/" (js/btoa repo) "/" folder-name) handle)
+    (db/transact! [(ldb/kv :logseq.kv/graph-backup-folder folder-name)])
+    [folder-name handle]))
+
+(defn backup-db-graph
+  [repo _backup-type]
   (when (and repo (= repo (state/get-current-repo)))
     (when-let [backup-folder (ldb/get-key-value (db/get-db repo) :logseq.kv/graph-backup-folder)]
-      (p/let [handle (idb/get-item (str "handle/" (js/btoa repo) "/" backup-folder))
+      ;; ensure file handle exists
+      ;; ask user to choose a folder again when access expires
+      (p/let [handle (try
+                       (idb/get-item (str "handle/" (js/btoa repo) "/" backup-folder))
+                       (catch :default _e
+                         (throw (ex-info "Backup file handle no longer exists" {:repo repo}))))
+              [_folder handle] (try
+                                 (utils/verifyPermission handle true)
+                                 [backup-folder handle]
+                                 (catch :default e
+                                   (js/console.error e)
+                                   (choose-backup-folder repo)))
               repo-name (common-sqlite/sanitize-db-name repo)]
         (if handle
           (->
@@ -297,9 +319,9 @@
     (when (and (config/db-based-graph? repo) util/web-platform? (utils/nfsSupported))
       (cancel-db-backup!)
 
-      (when backup-now? (backup-db-graph repo))
+      (when backup-now? (backup-db-graph repo :backup-now))
 
     ;; run backup every hour
-      (let [interval (js/setInterval #(backup-db-graph repo)
+      (let [interval (js/setInterval #(backup-db-graph repo :auto)
                                      (* 1 60 60 1000))]
         (reset! *backup-interval interval)))))

+ 4 - 0
src/main/frontend/handler/repo.cljs

@@ -210,3 +210,7 @@
                           {:content (str "The graph '" graph "' already exists. Please try again with another name.")
                            :status :error}])
        (create-db full-graph-name opts)))))
+
+(defn fix-broken-graph!
+  [graph]
+  (state/<invoke-db-worker :thread-api/fix-broken-graph graph))

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

@@ -84,7 +84,7 @@
      (let [page (db/get-page page-name)
            whiteboard? (db/whiteboard-page? page)]
        (if (and (not config/dev?)
-                (or (ldb/hidden? page)
+                (or (and (ldb/hidden? page) (not (ldb/property? page)))
                     (and (ldb/built-in? page) (ldb/private-built-in-page? page))))
          (notification/show! "Cannot go to an internal page." :warning)
          (if-let [source (and (not ignore-alias?) (db/get-alias-source-page (state/get-current-repo) (:db/id page)))]

+ 8 - 0
src/main/frontend/modules/shortcut/config.cljs

@@ -19,6 +19,7 @@
             [frontend.handler.paste :as paste-handler]
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.plugin-config :as plugin-config-handler]
+            [frontend.handler.repo :as repo-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.search :as search-handler]
             [frontend.handler.ui :as ui-handler]
@@ -592,6 +593,11 @@
                 :inactive (not (util/electron?))
                 :fn commit/show-commit-modal!}
 
+   :dev/fix-broken-graph {:binding []
+                          :inactive (not (state/developer-mode?))
+                          :db-graph? true
+                          :fn #(repo-handler/fix-broken-graph! (state/get-current-repo))}
+
    :dev/replace-graph-with-db-file {:binding []
                                     :inactive (or (not (util/electron?)) (not (state/developer-mode?)))
                                     :fn :frontend.handler.common.developer/replace-graph-with-db-file}
@@ -859,6 +865,7 @@
           :dev/show-page-ast
           :dev/replace-graph-with-db-file
           :dev/validate-db
+          :dev/fix-broken-graph
           :ui/customize-appearance])
         (with-meta {:before m/enable-when-not-editing-mode!}))
 
@@ -1051,6 +1058,7 @@
      :dev/show-page-ast
      :dev/replace-graph-with-db-file
      :dev/validate-db
+     :dev/fix-broken-graph
      :ui/clear-all-notifications]
 
     :shortcut.category/plugins

+ 1 - 1
src/main/frontend/search/browser.cljs

@@ -19,7 +19,7 @@
             result (state/<invoke-db-worker :thread-api/search-build-blocks-indice repo)
             blocks (if file-based?
                      (->> result
-                            ;; remove built-in properties from content
+                          ;; remove built-in properties from content
                           (map
                            #(update % :content
                                     (fn [content]

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

@@ -1877,7 +1877,7 @@ Similar to re-frame subscriptions"
       (if (and page
                ;; TODO: Use config/dev? when it's not a circular dep
                (not goog.DEBUG)
-               (or (ldb/hidden? page)
+               (or (and (ldb/hidden? page) (not (ldb/property? page)))
                    (and (ldb/built-in? page) (ldb/private-built-in-page? page))))
         (pub-event! [:notification/show {:content "Cannot open an internal page." :status :warning}])
         (when db-id

+ 11 - 0
src/main/frontend/ui.css

@@ -275,6 +275,17 @@ html.is-mobile {
   display: inline-block;
 }
 
+.ui__icon svg {
+  filter: brightness(0.8);
+  transition: filter .15s;
+  will-change: filter;
+}
+
+.ui__icon:hover svg {
+  filter: brightness(1);
+  transition-duration: .15s;
+}
+
 .type-icon {
   @apply text-base text-center flex items-center justify-center rounded border mr-2 relative;
 

+ 7 - 1
src/main/frontend/util.cljc

@@ -790,6 +790,11 @@
      (->> blocks
           (remove (fn [b] (= "true" (d/attr b "data-embed")))))))
 
+#?(:cljs
+   (defn remove-property-value-blocks [blocks]
+     (->> blocks
+          (remove (fn [b] (d/has-class? b "property-value-container"))))))
+
 #?(:cljs
    (defn get-selected-text
      []
@@ -898,7 +903,8 @@
    (defn get-prev-block-non-collapsed-non-embed
      [block]
      (when-let [blocks (->> (get-blocks-noncollapse)
-                            remove-embedded-blocks)]
+                            remove-embedded-blocks
+                            remove-property-value-blocks)]
        (when-let [index (.indexOf blocks block)]
          (let [idx (dec index)]
            (when (>= idx 0)

+ 10 - 3
src/main/frontend/worker/db/migrate.cljs

@@ -906,6 +906,12 @@
          move-top-parents-to-library
          update-children-parent-and-order)))))
 
+
+(defn- empty-placeholder-add-block-uuid
+  [_conn _search-db]
+  [{:db/ident :logseq.property/empty-placeholder
+    :block/uuid (common-uuid/gen-uuid :builtin-block-uuid :logseq.property/empty-placeholder)}])
+
 (def ^:large-vars/cleanup-todo schema-version->updates
   "A vec of tuples defining datascript migrations. Each tuple consists of the
    schema version integer and a migration map. A migration map can have keys of :properties, :classes
@@ -1005,8 +1011,8 @@
    [62 {:fix remove-block-schema}]
    [63 {:properties [:logseq.property.table/pinned-columns]}]
    [64 {:fix update-view-filter}]
-   ;;;; schema-version format: "<major>.<minor>"
-   ;;;; int number equals to "<major>" (without <minor>)
+;;;; schema-version format: "<major>.<minor>"
+;;;; int number equals to "<major>" (without <minor>)
    ["64.1" {:properties [:logseq.property.view/group-by-property]
             :fix add-view-icons}]
    ["64.2" {:properties [:logseq.property.view/feature-type]
@@ -1018,7 +1024,8 @@
    ["64.6" {:fix cardinality-one-multiple-values}]
    ["64.7" {:fix rename-repeated-properties}]
    ["64.8" {:fix rename-task-properties}]
-   ["64.9" {:fix fix-rename-parent-to-extends}]])
+   ["64.9" {:fix empty-placeholder-add-block-uuid}]
+   ["64.10" {:fix fix-rename-parent-to-extends}]])
 
 (let [[major minor] (last (sort (map (comp (juxt :major :minor) db-schema/parse-schema-version first)
                                      schema-version->updates)))

+ 69 - 45
src/main/frontend/worker/db_worker.cljs

@@ -100,39 +100,43 @@
   [^js pool data]
   (.importDb ^js pool repo-path data))
 
-(comment
-  (defn- get-all-datoms-from-sqlite-db
-    [db]
-    (some->> (.exec db #js {:sql "select * from kvs"
-                            :rowMode "array"})
-             bean/->clj
-             (mapcat
-              (fn [[_addr content _addresses]]
-                (let [content' (sqlite-util/transit-read content)
-                      datoms (when (map? content')
-                               (:keys content'))]
-                  datoms)))
-             distinct
-             (map (fn [[e a v t]]
-                    (d/datom e a v t)))))
-
-  (defn- rebuild-db-from-datoms!
-    "Persistent-sorted-set has been broken, used addresses can't be found"
-    [datascript-conn sqlite-db import-type]
-    (let [datoms (get-all-datoms-from-sqlite-db sqlite-db)
-          db (d/init-db [] db-schema/schema
-                        {:storage (storage/storage @datascript-conn)})
-          db (d/db-with db
-                        (map (fn [d]
-                               [:db/add (:e d) (:a d) (:v d) (:t d)]) datoms))]
-      (prn :debug :rebuild-db-from-datoms :datoms-count (count datoms))
-    ;; export db first
-      (when-not import-type
-        (worker-util/post-message :notification ["The SQLite db will be exported to avoid any data-loss." :warning false])
-        (worker-util/post-message :export-current-db []))
-      (.exec sqlite-db #js {:sql "delete from kvs"})
-      (d/reset-conn! datascript-conn db)
-      (db-migrate/fix-db! datascript-conn))))
+(defn- get-all-datoms-from-sqlite-db
+  [db]
+  (some->> (.exec db #js {:sql "select * from kvs"
+                          :rowMode "array"})
+           bean/->clj
+           (mapcat
+            (fn [[_addr content _addresses]]
+              (let [content' (sqlite-util/transit-read content)
+                    datoms (when (map? content')
+                             (:keys content'))]
+                datoms)))
+           distinct
+           (map (fn [[e a v t]]
+                  (d/datom e a v t)))))
+
+(defn- rebuild-db-from-datoms!
+  "Persistent-sorted-set has been broken, used addresses can't be found"
+  [datascript-conn sqlite-db]
+  (let [datoms (get-all-datoms-from-sqlite-db sqlite-db)
+        db (d/init-db [] db-schema/schema
+                      {:storage (storage/storage @datascript-conn)})
+        db (d/db-with db
+                      (map (fn [d]
+                             [:db/add (:e d) (:a d) (:v d) (:t d)]) datoms))]
+    (prn :debug :rebuild-db-from-datoms :datoms-count (count datoms))
+    (worker-util/post-message :notification ["The SQLite db will be exported to avoid any data-loss." :warning false])
+    (worker-util/post-message :export-current-db [])
+    (.exec sqlite-db #js {:sql "delete from kvs"})
+    (d/reset-conn! datascript-conn db)
+    (db-migrate/fix-db! datascript-conn)))
+
+(defn- fix-broken-graph
+  [graph]
+  (let [conn (worker-state/get-datascript-conn graph)
+        sqlite-db (worker-state/get-sqlite-conn graph)]
+    (when (and conn sqlite-db)
+      (rebuild-db-from-datoms! conn sqlite-db))))
 
 (comment
   (defn- gc-kvs-table!
@@ -180,9 +184,25 @@
         (when (and compare-result (not (neg? compare-result))) ; >= 64.8
           (worker-util/post-message :capture-error
                                     {:error "db-missing-addresses-v2"
-                                     :payload {:missing-addresses missing-addresses}}))))
+                                     :payload {:missing-addresses (str missing-addresses)
+                                               :db-schema-version (str version-in-db)}}))))
     missing-addresses))
 
+(def get-to-delete-unused-addresses-sql
+  "WITH to_delete(addr) AS (
+     SELECT value
+     FROM json_each(?)
+   ),
+  referenced(addr) AS (
+    SELECT json_each.value
+    FROM kvs
+    JOIN json_each(kvs.addresses)
+    WHERE kvs.addr NOT IN (SELECT addr FROM to_delete)
+      AND json_each.value IN (SELECT addr FROM to_delete)
+  )
+  SELECT addr FROM to_delete
+  WHERE addr NOT IN (SELECT addr FROM referenced)")
+
 (defn upsert-addr-content!
   "Upsert addr+data-seq. Update sqlite-cli/upsert-addr-content! when making changes"
   [db data delete-addrs*]
@@ -193,19 +213,19 @@
                          (.exec tx #js {:sql "INSERT INTO kvs (addr, content, addresses) values ($addr, $content, $addresses) on conflict(addr) do update set content = $content, addresses = $addresses"
                                         :bind item}))))
     (when (seq delete-addrs)
-      (.transaction db (fn [tx]
-                         (doseq [addr delete-addrs]
-                           (.exec tx #js {:sql "Delete from kvs WHERE addr = ? AND NOT EXISTS (SELECT 1 FROM json_each(addresses) WHERE value = ?);"
-                                          :bind #js [addr]}))))
+      (let [result (.exec db #js {:sql get-to-delete-unused-addresses-sql
+                                  :bind (js/JSON.stringify (clj->js delete-addrs))
+                                  :rowMode "array"})
+            non-refed-addrs (map #(aget % 0) result)]
+        (when (seq non-refed-addrs)
+          (.transaction db (fn [tx]
+                             (doseq [addr non-refed-addrs]
+                               (.exec tx #js {:sql "Delete from kvs where addr = ?"
+                                              :bind #js [addr]}))))))
       (let [missing-addrs (when worker-util/dev?
                             (seq (find-missing-addresses nil db {:delete-addrs delete-addrs})))]
-        (if (seq missing-addrs)
-          (worker-util/post-message :notification [(str "Bug!! Missing addresses: " missing-addrs) :error false])
-          (when (seq delete-addrs)
-            (.transaction db (fn [tx]
-                               (doseq [addr delete-addrs]
-                                 (.exec tx #js {:sql "Delete from kvs WHERE addr = ? AND NOT EXISTS (SELECT 1 FROM json_each(addresses) WHERE value = ?);"
-                                                :bind #js [addr]}))))))))))
+        (when (seq missing-addrs)
+          (worker-util/post-message :notification [(str "Bug!! Missing addresses: " missing-addrs) :error false]))))))
 
 (defn restore-data-from-addr
   "Update sqlite-cli/restore-data-from-addr when making changes"
@@ -771,6 +791,10 @@
   [repo]
   (get-all-page-titles-with-cache repo))
 
+(def-thread-api :thread-api/fix-broken-graph
+  [graph]
+  (fix-broken-graph graph))
+
 (comment
   (def-thread-api :general/dangerousRemoveAllDbs
     []

+ 12 - 12
src/main/frontend/worker/search.cljs

@@ -10,6 +10,7 @@
             [logseq.common.util :as common-util]
             [logseq.common.util.namespace :as ns-util]
             [logseq.db :as ldb]
+            [logseq.db.frontend.content :as db-content]
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.graph-parser.text :as text]))
 
@@ -193,7 +194,8 @@ DROP TRIGGER IF EXISTS blocks_au;
 (defn- page-or-object?
   [entity]
   (and (or (ldb/page? entity) (ldb/object? entity))
-       (not (ldb/hidden? entity))))
+       (not (ldb/hidden? entity))
+       (not (ldb/hidden? (:block/page entity)))))
 
 (defn get-all-fuzzy-supported-blocks
   "Only pages and objects are supported now."
@@ -205,7 +207,9 @@ DROP TRIGGER IF EXISTS blocks_au;
                           (map :e)))
         blocks (->> (distinct (concat page-ids object-ids))
                     (map #(d/entity db %)))]
-    (remove ldb/hidden? blocks)))
+    (->> blocks
+         (remove ldb/hidden?)
+         (remove #(ldb/hidden? (:block/page %))))))
 
 (defn- sanitize
   [content]
@@ -219,13 +223,9 @@ DROP TRIGGER IF EXISTS blocks_au;
              (ldb/closed-value? block)
              (and (string? title) (> (count title) 10000))
              (string/blank? title))        ; empty page or block
-      ;; Should properties be included in the search indice?
-      ;; It could slow down the search indexing, also it can be confusing
-      ;; if the showing properties are not useful to users.
-      ;; (let [content (if (and db-based? (seq (:block/properties block)))
-      ;;                 (str content (when (not= content "") "\n") (get-db-properties-str db properties))
-      ;;                 content)])
-    (let [title (ldb/get-title-with-parents (assoc block :block.temp/search? true))]
+    (let [title (-> block
+                    (update :block/title ldb/get-title-with-parents)
+                    db-content/recur-replace-uuid-in-block-title)]
       (when uuid
         {:id (str uuid)
          :page (str (or (:block/uuid page) uuid))
@@ -370,11 +370,11 @@ DROP TRIGGER IF EXISTS blocks_au;
                                     set)
                                blocks-to-add-set)]
       {:blocks-to-remove     (->>
-                              (keep #(d/entity db-before %) blocks-to-remove-set)
-                              (remove ldb/hidden?))
+                              (keep #(d/entity db-before %) blocks-to-remove-set))
        :blocks-to-add        (->>
                               (keep #(d/entity db-after %) blocks-to-add-set')
-                              (remove ldb/hidden?))})))
+                              (remove ldb/hidden?)
+                              (remove #(ldb/hidden? (:block/page %))))})))
 
 (defn- get-affected-blocks
   [repo tx-report]

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

@@ -786,4 +786,5 @@
   :dev/show-page-ast "(Dev) Show page AST"
   :dev/replace-graph-with-db-file "(Dev) Replace graph with its db.sqlite file"
   :dev/validate-db "(Dev) Validate current graph"
+  :dev/fix-broken-graph "(Dev) Fix current broken graph"
   :window/close "Close window"}}