浏览代码

Merge branch 'whiteboards' into enhance/whiteboards-ui

Konstantinos Kaloutas 3 年之前
父节点
当前提交
a454d11c33
共有 100 个文件被更改,包括 1599 次插入787 次删除
  1. 2 1
      .carve/ignore
  2. 2 2
      android/app/build.gradle
  3. 4 0
      deps/graph-parser/.carve/ignore
  4. 7 1
      deps/graph-parser/src/logseq/graph_parser/block.cljs
  5. 2 1
      deps/graph-parser/src/logseq/graph_parser/cli.cljs
  6. 3 3
      deps/graph-parser/src/logseq/graph_parser/config.cljs
  7. 2 1
      deps/graph-parser/src/logseq/graph_parser/mldoc.cljc
  8. 14 1
      deps/graph-parser/src/logseq/graph_parser/property.cljs
  9. 39 11
      deps/graph-parser/src/logseq/graph_parser/text.cljs
  10. 1 7
      deps/graph-parser/src/logseq/graph_parser/util.cljs
  11. 5 0
      deps/graph-parser/src/logseq/graph_parser/util/page_ref.cljs
  12. 14 1
      deps/graph-parser/test/logseq/graph_parser/property_test.cljs
  13. 6 1
      deps/graph-parser/test/logseq/graph_parser/text_test.cljs
  14. 138 1
      deps/graph-parser/test/logseq/graph_parser_test.cljs
  15. 60 0
      e2e-tests/context-menu.spec.ts
  16. 5 0
      e2e-tests/fixtures.ts
  17. 3 3
      e2e-tests/page-search.spec.ts
  18. 1 1
      libs/package.json
  19. 1 1
      libs/src/LSPlugin.caller.ts
  20. 1 1
      libs/src/LSPlugin.core.ts
  21. 10 0
      libs/src/LSPlugin.ts
  22. 5 1
      libs/src/LSPlugin.user.ts
  23. 25 9
      resources/css/common.css
  24. 0 0
      resources/js/lsplugin.core.js
  25. 1 1
      resources/js/preload.js
  26. 1 1
      resources/package.json
  27. 1 1
      shadow-cljs.edn
  28. 18 0
      src/electron/electron/find_in_page.cljs
  29. 8 1
      src/electron/electron/handler.cljs
  30. 2 2
      src/electron/electron/window.cljs
  31. 11 1
      src/main/electron/listener.cljs
  32. 51 22
      src/main/frontend/components/block.cljs
  33. 207 192
      src/main/frontend/components/content.cljs
  34. 2 4
      src/main/frontend/components/content.css
  35. 87 0
      src/main/frontend/components/find_in_page.cljs
  36. 21 0
      src/main/frontend/components/find_in_page.css
  37. 2 1
      src/main/frontend/components/journal.cljs
  38. 1 1
      src/main/frontend/components/page.cljs
  39. 4 2
      src/main/frontend/components/query_table.cljs
  40. 124 64
      src/main/frontend/components/reference.cljs
  41. 8 0
      src/main/frontend/components/reference.css
  42. 1 1
      src/main/frontend/components/search.cljs
  43. 34 12
      src/main/frontend/components/sidebar.cljs
  44. 1 1
      src/main/frontend/db.cljs
  45. 3 41
      src/main/frontend/db/model.cljs
  46. 10 3
      src/main/frontend/db/query_dsl.cljs
  47. 8 13
      src/main/frontend/db/react.cljs
  48. 22 41
      src/main/frontend/handler/block.cljs
  49. 2 14
      src/main/frontend/handler/common.cljs
  50. 6 39
      src/main/frontend/handler/editor.cljs
  51. 1 0
      src/main/frontend/handler/events.cljs
  52. 8 5
      src/main/frontend/handler/page.cljs
  53. 54 1
      src/main/frontend/handler/search.cljs
  54. 27 7
      src/main/frontend/modules/outliner/core.cljs
  55. 10 11
      src/main/frontend/modules/outliner/pipeline.cljs
  56. 20 11
      src/main/frontend/modules/outliner/tree.cljs
  57. 4 3
      src/main/frontend/modules/shortcut/before.cljs
  58. 21 1
      src/main/frontend/modules/shortcut/config.cljs
  59. 10 8
      src/main/frontend/modules/shortcut/data_helper.cljs
  60. 3 0
      src/main/frontend/modules/shortcut/dicts.cljc
  61. 3 3
      src/main/frontend/search.cljs
  62. 12 6
      src/main/frontend/state.cljs
  63. 41 40
      src/main/frontend/ui.cljs
  64. 0 5
      src/main/frontend/ui.css
  65. 5 7
      src/main/frontend/util.cljc
  66. 8 6
      src/main/frontend/utils.js
  67. 1 1
      src/main/frontend/version.cljs
  68. 27 6
      src/main/logseq/api.cljs
  69. 42 9
      src/test/frontend/db/query_dsl_test.cljs
  70. 19 0
      src/test/frontend/test/frontend_node_test_runner.cljs
  71. 8 0
      src/test/frontend/test/helper.clj
  72. 16 12
      src/test/frontend/test/node_test_runner.cljs
  73. 4 0
      templates/config.edn
  74. 95 28
      tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx
  75. 3 1
      tldraw/apps/tldraw-logseq/src/components/inputs/ToggleInput.tsx
  76. 25 0
      tldraw/apps/tldraw-logseq/src/lib/color.ts
  77. 2 2
      tldraw/apps/tldraw-logseq/src/lib/shapes/BoxShape.tsx
  78. 2 2
      tldraw/apps/tldraw-logseq/src/lib/shapes/DotShape.tsx
  79. 2 2
      tldraw/apps/tldraw-logseq/src/lib/shapes/EllipseShape.tsx
  80. 2 2
      tldraw/apps/tldraw-logseq/src/lib/shapes/HTMLShape.tsx
  81. 1 1
      tldraw/apps/tldraw-logseq/src/lib/shapes/HighlighterShape.tsx
  82. 2 2
      tldraw/apps/tldraw-logseq/src/lib/shapes/LineShape.tsx
  83. 17 17
      tldraw/apps/tldraw-logseq/src/lib/shapes/LogseqPortalShape.tsx
  84. 1 2
      tldraw/apps/tldraw-logseq/src/lib/shapes/PenShape.tsx
  85. 1 1
      tldraw/apps/tldraw-logseq/src/lib/shapes/PencilShape.tsx
  86. 2 2
      tldraw/apps/tldraw-logseq/src/lib/shapes/PolygonShape.tsx
  87. 45 34
      tldraw/apps/tldraw-logseq/src/lib/shapes/TextShape.tsx
  88. 4 4
      tldraw/apps/tldraw-logseq/src/lib/shapes/YouTubeShape.tsx
  89. 2 0
      tldraw/apps/tldraw-logseq/src/lib/shapes/index.ts
  90. 14 1
      tldraw/apps/tldraw-logseq/src/lib/shapes/style-props.tsx
  91. 0 2
      tldraw/apps/tldraw-logseq/src/lib/shapes/text/TextLabel.tsx
  92. 1 1
      tldraw/apps/tldraw-logseq/src/lib/tools/LogseqPortalTool/states/CreatingState.tsx
  93. 8 2
      tldraw/apps/tldraw-logseq/src/styles.css
  94. 7 1
      tldraw/demo/postcss.config.js
  95. 14 1
      tldraw/demo/src/App.jsx
  96. 8 0
      tldraw/demo/src/logseq-styles.css
  97. 1 1
      tldraw/demo/src/main.jsx
  98. 0 8
      tldraw/demo/tailwind.config.js
  99. 0 10
      tldraw/packages/core/src/lib/TLApi/TLApi.ts
  100. 10 13
      tldraw/packages/core/src/lib/TLApp/TLApp.ts

+ 2 - 1
.carve/ignore

@@ -75,7 +75,8 @@ frontend.util/trace!
 frontend.util.pool/terminate-pool!
 frontend.util.pool/terminate-pool!
 ;; Repl fn
 ;; Repl fn
 frontend.util.property/add-page-properties
 frontend.util.property/add-page-properties
-;; Test runner used by shadow
+;; Test runners used by shadow
 frontend.test.node-test-runner/main
 frontend.test.node-test-runner/main
+frontend.test.frontend-node-test-runner/main
 ;; Test runner for nbb
 ;; Test runner for nbb
 logseq.graph-parser.nbb-test-runner/run-tests
 logseq.graph-parser.nbb-test-runner/run-tests

+ 2 - 2
android/app/build.gradle

@@ -6,8 +6,8 @@ android {
         applicationId "com.logseq.app"
         applicationId "com.logseq.app"
         minSdkVersion rootProject.ext.minSdkVersion
         minSdkVersion rootProject.ext.minSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
-        versionCode 35
-        versionName "0.8.1"
+        versionCode 36
+        versionName "0.8.2"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         aaptOptions {
         aaptOptions {
              // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
              // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

+ 4 - 0
deps/graph-parser/.carve/ignore

@@ -18,3 +18,7 @@ logseq.graph-parser.util.page-ref/left-and-right-brackets
 logseq.graph-parser.util.page-ref/->page-ref
 logseq.graph-parser.util.page-ref/->page-ref
 ;; API
 ;; API
 logseq.graph-parser.util.page-ref/get-page-name!
 logseq.graph-parser.util.page-ref/get-page-name!
+;; API
+logseq.graph-parser.property/->block-content
+;; API
+logseq.graph-parser.property/property-value-from-content

+ 7 - 1
deps/graph-parser/src/logseq/graph_parser/block.cljs

@@ -157,6 +157,8 @@
              distinct)
              distinct)
     []))
     []))
 
 
+;; TODO: Use text/parse-property to determine refs rather than maintain this similar
+;; implementation to parse-property
 (defn- get-page-ref-names-from-properties
 (defn- get-page-ref-names-from-properties
   [format properties user-config]
   [format properties user-config]
   (let [page-refs (->>
   (let [page-refs (->>
@@ -174,7 +176,11 @@
                             (and (string? v)
                             (and (string? v)
                                  (not (gp-mldoc/link? format v)))
                                  (not (gp-mldoc/link? format v)))
                             (let [v (string/trim v)
                             (let [v (string/trim v)
-                                  result (text/split-page-refs-without-brackets v {:un-brackets? false})]
+                                  result (if (:rich-property-values? user-config)
+                                           (if (gp-util/wrapped-by-quotes? v)
+                                             []
+                                             (text/extract-page-refs-and-tags v))
+                                           (text/split-page-refs-without-brackets v {:un-brackets? false}))]
                               (if (coll? result)
                               (if (coll? result)
                                 (map text/page-ref-un-brackets! result)
                                 (map text/page-ref-un-brackets! result)
                                 []))
                                 []))

+ 2 - 1
deps/graph-parser/src/logseq/graph_parser/cli.cljs

@@ -44,7 +44,8 @@ TODO: Fail fast when process exits 1"
 
 
 (defn- parse-files
 (defn- parse-files
   [conn files {:keys [config] :as options}]
   [conn files {:keys [config] :as options}]
-  (let [extract-options (merge {:date-formatter (gp-config/get-date-formatter config)}
+  (let [extract-options (merge {:date-formatter (gp-config/get-date-formatter config)
+                                :user-config config}
                                (select-keys options [:verbose]))]
                                (select-keys options [:verbose]))]
     (mapv
     (mapv
      (fn [{:file/keys [path content]}]
      (fn [{:file/keys [path content]}]

+ 3 - 3
deps/graph-parser/src/logseq/graph_parser/config.cljs

@@ -1,7 +1,6 @@
 (ns logseq.graph-parser.config
 (ns logseq.graph-parser.config
   "Config that is shared between graph-parser and rest of app"
   "Config that is shared between graph-parser and rest of app"
-  (:require [logseq.graph-parser.util :as gp-util]
-            [clojure.set :as set]
+  (:require [clojure.set :as set]
             [clojure.string :as string]))
             [clojure.string :as string]))
 
 
 (def app-name
 (def app-name
@@ -12,7 +11,8 @@
 
 
 (defn local-asset?
 (defn local-asset?
   [s]
   [s]
-  (gp-util/safe-re-find (re-pattern (str "^[./]*" local-assets-dir)) s))
+  (and (string? s)
+       (re-find (re-pattern (str "^[./]*" local-assets-dir)) s)))
 
 
 (defonce default-draw-directory "draws")
 (defonce default-draw-directory "draws")
 ;; TODO read configurable value?
 ;; TODO read configurable value?

+ 2 - 1
deps/graph-parser/src/logseq/graph_parser/mldoc.cljc

@@ -154,7 +154,8 @@
                           (remove string/blank?)))
                           (remove string/blank?)))
           tags (:tags properties)
           tags (:tags properties)
           tags (->> (->vec-concat tags filetags)
           tags (->> (->vec-concat tags filetags)
-                    (remove string/blank?))
+                    (remove string/blank?)
+                    vec)
           properties (assoc properties :tags tags :alias alias)
           properties (assoc properties :tags tags :alias alias)
           properties (-> properties
           properties (-> properties
                          (update :filetags (constantly filetags)))
                          (update :filetags (constantly filetags)))

+ 14 - 1
deps/graph-parser/src/logseq/graph_parser/property.cljs

@@ -8,6 +8,19 @@
 
 
 (def colons "Property delimiter for markdown mode" "::")
 (def colons "Property delimiter for markdown mode" "::")
 
 
+(defn ->block-content
+  "Creates a block content string from properties map"
+  [properties]
+  (->> properties
+       (map #(str (name (key %)) (str colons " ") (val %)))
+       (string/join "\n")))
+
+(defn property-value-from-content
+  "Extracts full property value from block content"
+  [property content]
+  (second (re-find (re-pattern (str property colons "\\s+(.*)"))
+                   content)))
+
 (defn properties-ast?
 (defn properties-ast?
   [block]
   [block]
   (and
   (and
@@ -60,7 +73,7 @@
   [content]
   [content]
   (when content
   (when content
     (and (string/includes? content properties-start)
     (and (string/includes? content properties-start)
-         (gp-util/safe-re-find properties-end-pattern content))))
+         (re-find properties-end-pattern content))))
 
 
 (defn ->new-properties
 (defn ->new-properties
   "New syntax: key:: value"
   "New syntax: key:: value"

+ 39 - 11
deps/graph-parser/src/logseq/graph_parser/text.cljs

@@ -5,6 +5,7 @@
             [clojure.set :as set]
             [clojure.set :as set]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util :as gp-util]
+            [logseq.graph-parser.property :as gp-property]
             [logseq.graph-parser.util.page-ref :as page-ref :refer [right-brackets]]))
             [logseq.graph-parser.util.page-ref :as page-ref :refer [right-brackets]]))
 
 
 (defn get-file-basename
 (defn get-file-basename
@@ -109,8 +110,8 @@
 
 
      (and (string? s)
      (and (string? s)
             ;; Either a page ref, a tag or a comma separated collection
             ;; Either a page ref, a tag or a comma separated collection
-            (or (gp-util/safe-re-find page-ref/page-ref-re s)
-                (gp-util/safe-re-find #"[\,|,|#|\"]+" s)))
+            (or (re-find page-ref/page-ref-re s)
+                (re-find #"[\,|,|#|\"]+" s)))
      (let [result (->> (sep-by-quotes s)
      (let [result (->> (sep-by-quotes s)
                        (mapcat
                        (mapcat
                         (fn [s]
                         (fn [s]
@@ -199,7 +200,35 @@
 (defonce non-parsing-properties
 (defonce non-parsing-properties
   (atom #{"background-color" "background_color"}))
   (atom #{"background-color" "background_color"}))
 
 
+(defn parse-non-string-property-value
+  "Return parsed non-string property value or nil if none is found"
+  [v]
+  (cond
+    (= v "true")
+    true
+
+    (= v "false")
+    false
+
+    (re-find #"^\d+$" v)
+    (parse-long v)))
+
+(def ^:private page-ref-or-tag-re
+  (re-pattern (str "#?" (page-ref/->page-ref-re-str "(.*?)") "|"
+                   ;; Don't capture punctuation at end of a tag
+                   "#([\\S]+[^\\s.!,])")))
+
+(defn extract-page-refs-and-tags
+  "Returns set of page-refs and tags in given string or returns string if none
+  are found"
+  [string]
+  (let [refs (map #(or (second %) (get % 2))
+                  (re-seq page-ref-or-tag-re string))]
+    (if (seq refs) (set refs) string)))
+
 (defn parse-property
 (defn parse-property
+  "Property value parsing that takes into account built-in properties, format
+  and user config"
   ([k v config-state]
   ([k v config-state]
    (parse-property :markdown k v config-state))
    (parse-property :markdown k v config-state))
   ([format k v config-state]
   ([format k v config-state]
@@ -212,14 +241,6 @@
                    (get config-state :ignored-page-references-keywords)) k)
                    (get config-state :ignored-page-references-keywords)) k)
        v
        v
 
 
-       (= v "true")
-       true
-       (= v "false")
-       false
-
-       (and (not= k "alias") (gp-util/safe-re-find #"^\d+$" v))
-       (parse-long v)
-
        (gp-util/wrapped-by-quotes? v) ; wrapped in ""
        (gp-util/wrapped-by-quotes? v) ; wrapped in ""
        v
        v
 
 
@@ -229,5 +250,12 @@
        (gp-mldoc/link? format v)
        (gp-mldoc/link? format v)
        v
        v
 
 
+       (contains? gp-property/editable-linkable-built-in-properties (keyword k))
+       (split-page-refs-without-brackets v)
+
        :else
        :else
-       (split-page-refs-without-brackets v)))))
+       (if-some [res (parse-non-string-property-value v)]
+         res
+         (if (:rich-property-values? config-state)
+           (extract-page-refs-and-tags v)
+           (split-page-refs-without-brackets v)))))))

+ 1 - 7
deps/graph-parser/src/logseq/graph_parser/util.cljs

@@ -6,12 +6,6 @@
             [logseq.graph-parser.log :as log]
             [logseq.graph-parser.log :as log]
             [cljs.reader :as reader]))
             [cljs.reader :as reader]))
 
 
-(defn safe-re-find
-  "Copy of frontend.util/safe-re-find. Too basic to couple to main app"
-  [pattern s]
-  (when (string? s)
-    (re-find pattern s)))
-
 (defn path-normalize
 (defn path-normalize
   "Normalize file path (for reading paths from FS, not required by writting)"
   "Normalize file path (for reading paths from FS, not required by writting)"
   [s]
   [s]
@@ -40,7 +34,7 @@
 (defn tag-valid?
 (defn tag-valid?
   [tag-name]
   [tag-name]
   (when (string? tag-name)
   (when (string? tag-name)
-    (not (safe-re-find #"[# \t\r\n]+" tag-name))))
+    (not (re-find #"[# \t\r\n]+" tag-name))))
 
 
 (defn safe-subs
 (defn safe-subs
   ([s start]
   ([s start]

+ 5 - 0
deps/graph-parser/src/logseq/graph_parser/util/page_ref.cljs

@@ -27,6 +27,11 @@ a logseq page-ref e.g. [[page name]]"
   [page-name]
   [page-name]
   (str left-brackets page-name right-brackets))
   (str left-brackets page-name right-brackets))
 
 
+(defn ->page-ref-re-str
+  "Create a page ref regex escaped string given a page name"
+  [page-name]
+  (string/replace (->page-ref page-name) #"([\[\]])" "\\$1"))
+
 (defn get-page-name
 (defn get-page-name
   "Extracts page-name from page-ref string"
   "Extracts page-name from page-ref string"
   [s]
   [s]

+ 14 - 1
deps/graph-parser/test/logseq/graph_parser/property_test.cljs

@@ -1,5 +1,5 @@
 (ns logseq.graph-parser.property-test
 (ns logseq.graph-parser.property-test
-  (:require [cljs.test :refer [are deftest]]
+  (:require [cljs.test :refer [are deftest is]]
             [logseq.graph-parser.property :as gp-property]))
             [logseq.graph-parser.property :as gp-property]))
 
 
 (deftest test->new-properties
 (deftest test->new-properties
@@ -24,3 +24,16 @@
 
 
     "hello\n:PROPERTIES:\n:foo: bar\n:nice\n:END:\nnice"
     "hello\n:PROPERTIES:\n:foo: bar\n:nice\n:END:\nnice"
     "hello\nfoo:: bar\n:nice\nnice"))
     "hello\nfoo:: bar\n:nice\nnice"))
+
+(deftest property-value-from-content
+  (is (= "62b38254-4be7-4627-a2b7-6d9ee20999e5"
+         (gp-property/property-value-from-content
+          "id"
+          "type:: blog-posting\ndesc:: nice walkthrough on creating a blog with #nbb\nid:: 62b38254-4be7-4627-a2b7-6d9ee20999e5"))
+      "Pulls value from end of block content")
+
+  (is (= "nice walkthrough on creating a blog with #nbb"
+         (gp-property/property-value-from-content
+          "desc"
+          "type:: blog-posting\ndesc:: nice walkthrough on creating a blog with #nbb\nid:: 62b38254-4be7-4627-a2b7-6d9ee20999e5"))
+      "Pulls value from middle of block content"))

+ 6 - 1
deps/graph-parser/test/logseq/graph_parser/text_test.cljs

@@ -1,5 +1,5 @@
 (ns logseq.graph-parser.text-test
 (ns logseq.graph-parser.text-test
-  (:require [cljs.test :refer [are deftest testing]]
+  (:require [cljs.test :refer [are deftest testing is]]
             [logseq.graph-parser.text :as text]))
             [logseq.graph-parser.text :as text]))
 
 
 (deftest test-get-page-name
 (deftest test-get-page-name
@@ -109,4 +109,9 @@
       :tags "\"[[foo]], [[bar]]\"" "\"[[foo]], [[bar]]\""
       :tags "\"[[foo]], [[bar]]\"" "\"[[foo]], [[bar]]\""
       :tags "baz, \"[[foo]], [[bar]]\"" #{"baz"})))
       :tags "baz, \"[[foo]], [[bar]]\"" #{"baz"})))
 
 
+(deftest extract-page-refs-and-tags
+  (is (= #{"cljs" "nbb" "js" "amazing"}
+       (text/extract-page-refs-and-tags "This project is written with #cljs, #nbb and #js. #amazing!"))
+      "Don't extract punctation at end of a tag"))
+
 #_(cljs.test/test-ns 'logseq.graph-parser.text-test)
 #_(cljs.test/test-ns 'logseq.graph-parser.text-test)

+ 138 - 1
deps/graph-parser/test/logseq/graph_parser_test.cljs

@@ -1,9 +1,10 @@
 (ns logseq.graph-parser-test
 (ns logseq.graph-parser-test
-  (:require [cljs.test :refer [deftest testing is]]
+  (:require [cljs.test :refer [deftest testing is are]]
             [clojure.string :as string]
             [clojure.string :as string]
             [logseq.graph-parser :as graph-parser]
             [logseq.graph-parser :as graph-parser]
             [logseq.db :as ldb]
             [logseq.db :as ldb]
             [logseq.graph-parser.block :as gp-block]
             [logseq.graph-parser.block :as gp-block]
+            [logseq.graph-parser.property :as gp-property]
             [datascript.core :as d]))
             [datascript.core :as d]))
 
 
 (def foo-edn
 (def foo-edn
@@ -97,3 +98,139 @@
     (test-property-order 4))
     (test-property-order 4))
   (testing "Sort order and persistence of 10 properties"
   (testing "Sort order and persistence of 10 properties"
     (test-property-order 10)))
     (test-property-order 10)))
+
+(defn- quoted-property-values-test
+  [user-config]
+  (let [conn (ldb/start-conn)
+        _ (graph-parser/parse-file conn
+                                   "foo.md"
+                                   "- desc:: \"#foo is not a ref\""
+                                   {:extract-options {:user-config user-config}})
+        block (->> (d/q '[:find (pull ?b [* {:block/refs [*]}])
+                       :in $
+                       :where [?b :block/properties]]
+                     @conn)
+                (map first)
+                first)]
+    (is (= {:desc "\"#foo is not a ref\""}
+           (:block/properties block))
+        "Quoted value is unparsed")
+    (is (= ["desc"]
+           (map :block/original-name (:block/refs block)))
+        "No refs from property value")))
+
+(deftest quoted-property-values
+  (testing "With default config"
+    (quoted-property-values-test {}))
+  (testing "With :rich-property-values config"
+    (quoted-property-values-test {:rich-property-values? true})))
+
+(deftest page-properties-persistence
+  (testing "Non-string property values"
+    (let [conn (ldb/start-conn)]
+      (graph-parser/parse-file conn
+                               "lythe-of-heaven.md"
+                               "rating:: 8\nrecommend:: true\narchive:: false"
+                               {})
+      (is (= {:rating 8 :recommend true :archive false}
+             (->> (d/q '[:find (pull ?b [*])
+                         :in $
+                         :where [?b :block/properties]]
+                       @conn)
+                  (map (comp :block/properties first))
+                  first)))))
+
+  (testing "Linkable built-in properties"
+    (let [conn (ldb/start-conn)
+          _ (graph-parser/parse-file conn
+                                     "lol.md"
+                                     "alias:: 233\ntags:: fun, facts"
+                                     {})
+          block (->> (d/q '[:find (pull ?b [:block/properties {:block/alias [:block/name]} {:block/tags [:block/name]}])
+                            :in $
+                            :where [?b :block/name "lol"]]
+                          @conn)
+                     (map first)
+                     first)]
+
+      (is (= {:block/alias [{:block/name "233"}]
+              :block/tags [{:block/name "fun"} {:block/name "facts"}]
+              :block/properties {:alias ["233"] :tags ["fun" "facts"]}}
+             block))
+
+      (is (every? vector? (vals (:block/properties block)))
+          "Linked built-in property values as vectors provides for easier transforms"))))
+
+(defn- property-relationships-test
+  "Runs tests on page properties and block properties. file-properties is what is
+  visible in a file and db-properties is what is pulled out from the db"
+  [file-properties db-properties user-config]
+  (let [conn (ldb/start-conn)
+        page-content (gp-property/->block-content file-properties)
+        ;; Create Block properties from given page ones
+        block-property-transform (fn [m] (update-keys m #(keyword (str "block-" (name %)))))
+        block-content (gp-property/->block-content (block-property-transform file-properties))
+        _ (graph-parser/parse-file conn
+                                   "property-relationships.md"
+                                   (str page-content "\n- " block-content)
+                                   {:extract-options {:user-config user-config}})
+        pages (->> (d/q '[:find (pull ?b [* :block/properties])
+                          :in $
+                          :where [?b :block/name] [?b :block/properties]]
+                        @conn)
+                   (map first))
+        _ (assert (= 1 (count pages)))
+        blocks (->> (d/q '[:find (pull ?b [:block/pre-block? :block/properties
+                                           {:block/refs [:block/original-name]}])
+                           :in $
+                           :where [?b :block/properties] [(missing? $ ?b :block/name)]]
+                         @conn)
+                    (map first)
+                    (map (fn [m] (update m :block/refs #(map :block/original-name %)))))
+        block-db-properties (block-property-transform db-properties)]
+
+    (is (= db-properties (:block/properties (first pages)))
+        "page has expected properties")
+
+    (is (= [true nil] (map :block/pre-block? blocks))
+        "page has 2 blocks, one of which is a pre-block")
+
+    (is (= [db-properties block-db-properties]
+           (map :block/properties blocks))
+        "pre-block/page and block have expected properties")
+
+    ;; has expected refs
+    (are [db-props refs]
+         (= (->> (vals db-props)
+                 ;; ignore string values
+                 (mapcat #(if (coll? %) % []))
+                 (concat (map name (keys db-props)))
+                 set)
+            (set refs))
+         ; pre-block/page has expected refs
+         db-properties (first (map :block/refs blocks))
+         ;; block has expected refs
+         block-db-properties (second (map :block/refs blocks)))))
+
+(deftest property-relationships
+  (let [properties {:single-link "[[bar]]"
+                    :multi-link "[[Logseq]] is the fastest #triples #[[text editor]]"
+                    :desc "This is a multiple sentence description. It has one [[link]]"
+                    :comma-prop "one, two,three"}]
+    (testing "With default config"
+      (property-relationships-test
+       properties
+       {:single-link #{"bar"}
+        :multi-link #{"Logseq" "is the fastest" "triples" "text editor"}
+        :desc #{"This is a multiple sentence description. It has one" "link"}
+        :comma-prop #{"one" "two" "three"}}
+       {}))
+
+    (testing "With :rich-property-values config"
+      (property-relationships-test
+       properties
+       {:single-link #{"bar"}
+        :multi-link #{"Logseq" "triples" "text editor"}
+        :desc #{"link"}
+        :comma-prop "one, two,three"}
+       {:rich-property-values? true}))))

+ 60 - 0
e2e-tests/context-menu.spec.ts

@@ -0,0 +1,60 @@
+import { expect } from '@playwright/test'
+import { test } from './fixtures'
+import { createRandomPage } from './utils'
+
+test('open context menu', async ({ page }) => {
+    await createRandomPage(page)
+
+    await page.locator('span.bullet-container >> nth=0').click({button: "right"})
+
+    await expect(page.locator('#custom-context-menu')).toBeVisible()
+})
+
+test('close context menu on esc', async ({ page }) => {
+    await createRandomPage(page)
+
+    await page.locator('span.bullet-container >> nth=0').click({button: "right"})
+
+    await page.keyboard.press('Escape')
+
+    await expect(page.locator('#custom-context-menu')).toHaveCount(0)
+})
+
+test('close context menu by left clicking on empty space', async ({ page }) => {
+    await createRandomPage(page)
+
+    await page.locator('span.bullet-container >> nth=0').click({button: "right"})
+
+    await page.mouse.click(0, 200, {button: "left"})
+
+    await expect(page.locator('#custom-context-menu')).toHaveCount(0)
+})
+
+test('close context menu by clicking on a menu item', async ({ page }) => {
+    await createRandomPage(page)
+
+    await page.locator('span.bullet-container >> nth=0').click({button: "right"})
+
+    await page.locator('#custom-context-menu .menu-link >> nth=1').click()
+
+    await expect(page.locator('#custom-context-menu')).toHaveCount(0)
+})
+
+test('close context menu by clicking on a block', async ({ page, block }) => {
+    await createRandomPage(page)
+
+    await block.mustType('fist Block')
+    await block.enterNext()
+
+    await page.locator('span.bullet-container >> nth=-1').click({button: "right"})
+
+    const elementHandle = page.locator('.block-content >> nth=0');
+
+    const box = await elementHandle.boundingBox();
+    expect(box).toBeTruthy()
+    if (box) {
+        await page.mouse.click(box.x + box.width - 5, box.y + box.height / 2);
+    }
+
+    await expect(page.locator('#custom-context-menu')).toHaveCount(0)
+})

+ 5 - 0
e2e-tests/fixtures.ts

@@ -106,6 +106,11 @@ base.beforeEach(async () => {
   if (page) {
   if (page) {
     await page.keyboard.press('Escape')
     await page.keyboard.press('Escape')
     await page.keyboard.press('Escape')
     await page.keyboard.press('Escape')
+
+    const rightSidebar = page.locator('.cp__right-sidebar-inner')
+    if (await rightSidebar.isVisible()) {
+      await page.click('button.toggle-right-sidebar', {delay: 100})
+    }
   }
   }
 })
 })
 
 

+ 3 - 3
e2e-tests/page-search.spec.ts

@@ -38,7 +38,7 @@ import { IsMac, createRandomPage, newBlock, newInnerBlock, randomString, lastBlo
   await page.fill('[placeholder="Search or create page"]', 'Einführung in die Allgemeine Sprachwissenschaft' + rand)
   await page.fill('[placeholder="Search or create page"]', 'Einführung in die Allgemeine Sprachwissenschaft' + rand)
 
 
   await page.waitForTimeout(500)
   await page.waitForTimeout(500)
-  const results = await page.$$('#ui__ac-inner .block')
+  const results = await page.$$('#ui__ac-inner>div')
   expect(results.length).toEqual(3) // 2 blocks + 1 page
   expect(results.length).toEqual(3) // 2 blocks + 1 page
   await page.keyboard.press("Escape")
   await page.keyboard.press("Escape")
 })
 })
@@ -68,7 +68,7 @@ async function alias_test(page: Page, page_name: string, search_kws: string[]) {
   await page.waitForTimeout(500)
   await page.waitForTimeout(500)
 
 
   // build target Page with alias
   // build target Page with alias
-  // the target page will contains the content in 
+  // the target page will contains the content in
   //   alias_test_content_1,
   //   alias_test_content_1,
   //   alias_test_content_2, and
   //   alias_test_content_2, and
   //   alias_test_content_3 sequentialy, to validate the target page state
   //   alias_test_content_3 sequentialy, to validate the target page state
@@ -127,7 +127,7 @@ async function alias_test(page: Page, page_name: string, search_kws: string[]) {
     await page.fill('[placeholder="Search or create page"]', kw_name)
     await page.fill('[placeholder="Search or create page"]', kw_name)
     await page.waitForTimeout(500)
     await page.waitForTimeout(500)
 
 
-    const results = await page.$$('#ui__ac-inner .block')
+    const results = await page.$$('#ui__ac-inner>div')
     expect(results.length).toEqual(3) // page + block + alias property
     expect(results.length).toEqual(3) // page + block + alias property
 
 
     // test search results
     // test search results

+ 1 - 1
libs/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@logseq/libs",
   "name": "@logseq/libs",
-  "version": "0.0.7",
+  "version": "0.0.8",
   "description": "Logseq SDK libraries",
   "description": "Logseq SDK libraries",
   "main": "dist/lsplugin.user.js",
   "main": "dist/lsplugin.user.js",
   "typings": "index.d.ts",
   "typings": "index.d.ts",

+ 1 - 1
libs/src/LSPlugin.caller.ts

@@ -266,7 +266,7 @@ class LSPluginCaller extends EventEmitter {
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
       timer = setTimeout(() => {
       timer = setTimeout(() => {
         reject(new Error(`handshake Timeout`))
         reject(new Error(`handshake Timeout`))
-      }, 3 * 1000) // 3secs
+      }, 8 * 1000) // 8 secs
 
 
       handshake
       handshake
         .then((refChild: ParentAPI) => {
         .then((refChild: ParentAPI) => {

+ 1 - 1
libs/src/LSPlugin.core.ts

@@ -907,7 +907,7 @@ class PluginLocal extends EventEmitter<'loaded'
 
 
       this._dispose(cleanInjectedScripts.bind(this))
       this._dispose(cleanInjectedScripts.bind(this))
     } catch (e) {
     } catch (e) {
-      debug('[Load Plugin Error] ', e)
+      console.error('[Load Plugin Error] ', e)
       this.logger?.error(e)
       this.logger?.error(e)
 
 
       this._status = PluginLocalLoadStatus.ERROR
       this._status = PluginLocalLoadStatus.ERROR

+ 10 - 0
libs/src/LSPlugin.ts

@@ -140,6 +140,8 @@ export interface AppUserConfigs {
 
 
   currentGraph: string
   currentGraph: string
   showBracket: boolean
   showBracket: boolean
+  enabledFlashcards: boolean
+  enabledJournals: boolean
 
 
   [key: string]: any
   [key: string]: any
 }
 }
@@ -410,6 +412,7 @@ export interface IAppProxy {
 
 
   // hook events
   // hook events
   onCurrentGraphChanged: IUserHook
   onCurrentGraphChanged: IUserHook
+  onGraphAfterIndexed: IUserHook<{repo: string}>
   onThemeModeChanged: IUserHook<{ mode: 'dark' | 'light' }>
   onThemeModeChanged: IUserHook<{ mode: 'dark' | 'light' }>
   onThemeChanged: IUserHook<Partial<{name: string, mode: string, pid: string, url: string}>>
   onThemeChanged: IUserHook<Partial<{name: string, mode: string, pid: string, url: string}>>
   onBlockRendererSlotted: IUserSlotHook<{ uuid: BlockUUID }>
   onBlockRendererSlotted: IUserSlotHook<{ uuid: BlockUUID }>
@@ -551,6 +554,12 @@ export interface IEditorProxy extends Record<string, any> {
     namespace: BlockPageName
     namespace: BlockPageName
   ) => Promise<Array<PageEntity> | null>
   ) => Promise<Array<PageEntity> | null>
 
 
+  /**
+   * Create a unique UUID string which can then be assigned to a block.
+   * @added 0.0.8
+   */
+  newBlockUUID: () => Promise<string>
+
   /**
   /**
    * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-reddit-hot-news
    * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-reddit-hot-news
    *
    *
@@ -565,6 +574,7 @@ export interface IEditorProxy extends Record<string, any> {
       before: boolean
       before: boolean
       sibling: boolean
       sibling: boolean
       isPageBlock: boolean
       isPageBlock: boolean
+      customUUID: string
       properties: {}
       properties: {}
     }>
     }>
   ) => Promise<BlockEntity | null>
   ) => Promise<BlockEntity | null>

+ 5 - 1
libs/src/LSPlugin.user.ts

@@ -189,6 +189,10 @@ const app: Partial<IAppProxy> = {
 let registeredCmdUid = 0
 let registeredCmdUid = 0
 
 
 const editor: Partial<IEditorProxy> = {
 const editor: Partial<IEditorProxy> = {
+  newBlockUUID(this: LSPluginUser): Promise<string> {
+    return this._execCallableAPIAsync('new_block_uuid')
+  },
+
   registerSlashCommand(
   registerSlashCommand(
     this: LSPluginUser,
     this: LSPluginUser,
     tag: string,
     tag: string,
@@ -273,7 +277,7 @@ const editor: Partial<IEditorProxy> = {
     } else {
     } else {
       this.App.pushState('page', { name: pageName }, { anchor })
       this.App.pushState('page', { name: pageName }, { anchor })
     }
     }
-  },
+  }
 }
 }
 
 
 const db: Partial<IDBProxy> = {
 const db: Partial<IDBProxy> = {

+ 25 - 9
resources/css/common.css

@@ -843,8 +843,14 @@ i.ti {
 
 
 .heading-bg {
 .heading-bg {
   border-radius: 50%;
   border-radius: 50%;
-  width: 12px;
-  height: 12px;
+  width: 14px;
+  height: 14px;
+
+  &.remove {
+    @apply border flex items-center justify-center;
+
+    border-color: var(--border-color);
+  }
 }
 }
 
 
 /** endregion **/
 /** endregion **/
@@ -901,20 +907,30 @@ button.menu:focus {
   background-color: var(--ls-menu-hover-color, #f4f5f7);
   background-color: var(--ls-menu-hover-color, #f4f5f7);
 }
 }
 
 
+.menu-links-wrapper {
+  @apply py-2 rounded-md shadow-lg overflow-y-auto;
+
+  max-height: calc(100vh - 100px) !important;
+  background-color: var(--ls-primary-background-color, #fff);
+  min-width: 12rem;
+}
+
+.menu-backdrop {
+  @apply w-full h-full fixed top-0 left-0;
+
+  z-index: var(--ls-z-index-level-1);
+}
+
 .menu-link {
 .menu-link {
   background-color: var(--ls-primary-background-color, #fff);
   background-color: var(--ls-primary-background-color, #fff);
   color: var(--ls-primary-text-color);
   color: var(--ls-primary-text-color);
   user-select: none;
   user-select: none;
 }
 }
 
 
-.menu-link:first-of-type {
-  border-top-left-radius: var(--ls-border-radius-low);
-  border-top-right-radius: var(--ls-border-radius-low);
-}
+.menu-separator {
+  @apply my-1;
 
 
-.menu-link:last-of-type {
-  border-bottom-left-radius: var(--ls-border-radius-low);
-  border-bottom-right-radius: var(--ls-border-radius-low);
+  opacity: .5;
 }
 }
 
 
 a.login {
 a.login {

文件差异内容过多而无法显示
+ 0 - 0
resources/js/lsplugin.core.js


+ 1 - 1
resources/js/preload.js

@@ -72,7 +72,7 @@ contextBridge.exposeInMainWorld('apis', {
 
 
   showItemInFolder (fullpath) {
   showItemInFolder (fullpath) {
     if (IS_WIN32) {
     if (IS_WIN32) {
-      shell.openPath(path.dirname(fullpath))
+      shell.openPath(path.dirname(fullpath).replaceAll("/", "\\"))
     } else {
     } else {
       shell.showItemInFolder(fullpath)
       shell.showItemInFolder(fullpath)
     }
     }

+ 1 - 1
resources/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "Logseq",
   "name": "Logseq",
-  "version": "0.8.1",
+  "version": "0.8.2",
   "main": "electron.js",
   "main": "electron.js",
   "author": "Logseq",
   "author": "Logseq",
   "license": "AGPL-3.0",
   "license": "AGPL-3.0",

+ 1 - 1
shadow-cljs.edn

@@ -72,7 +72,7 @@
          :devtools        {:enabled false}
          :devtools        {:enabled false}
          ;; disable :static-fns to allow for with-redefs and repl development
          ;; disable :static-fns to allow for with-redefs and repl development
          :compiler-options {:static-fns false}
          :compiler-options {:static-fns false}
-         :main            frontend.test.node-test-runner/main}
+         :main            frontend.test.frontend-node-test-runner/main}
 
 
   :publishing {:target        :browser
   :publishing {:target        :browser
                :module-loader true
                :module-loader true

+ 18 - 0
src/electron/electron/find_in_page.cljs

@@ -0,0 +1,18 @@
+(ns electron.find-in-page
+  (:require [electron.utils :as utils]
+            [cljs-bean.core :as bean]))
+
+(defn find!
+  [^js window search option]
+  (when window
+    (let [contents ^js (.-webContents window)]
+      (.findInPage contents search option)
+      (.on contents "found-in-page"
+           (fn [_event result]
+             (utils/send-to-renderer window "foundInPage" (bean/->clj result))))
+      true)))
+
+(defn clear!
+  [^js window]
+  (when window
+    (.stopFindInPage ^js (.-webContents window) "clearSelection")))

+ 8 - 1
src/electron/electron/handler.cljs

@@ -21,7 +21,8 @@
             [electron.plugin :as plugin]
             [electron.plugin :as plugin]
             [electron.window :as win]
             [electron.window :as win]
             [electron.file-sync-rsapi :as rsapi]
             [electron.file-sync-rsapi :as rsapi]
-            [electron.backup-file :as backup-file]))
+            [electron.backup-file :as backup-file]
+            [electron.find-in-page :as find]))
 
 
 (defmulti handle (fn [_window args] (keyword (first args))))
 (defmulti handle (fn [_window args] (keyword (first args))))
 
 
@@ -536,6 +537,12 @@
     (f)
     (f)
     (state/set-state! :window/once-persist-done nil)))
     (state/set-state! :window/once-persist-done nil)))
 
 
+(defmethod handle :find-in-page [^js win [_ search option]]
+  (find/find! win search (bean/->js option)))
+
+(defmethod handle :clear-find-in-page [^js win [_]]
+  (find/clear! win))
+
 (defn set-ipc-handler! [window]
 (defn set-ipc-handler! [window]
   (let [main-channel "main"]
   (let [main-channel "main"]
     (.handle ipcMain main-channel
     (.handle ipcMain main-channel

+ 2 - 2
src/electron/electron/window.cljs

@@ -118,7 +118,7 @@
 (defn setup-window-listeners!
 (defn setup-window-listeners!
   [^js win]
   [^js win]
   (when win
   (when win
-    (let [web-contents (. win -webContents)          
+    (let [web-contents (. win -webContents)
           new-win-handler
           new-win-handler
           (fn [e url]
           (fn [e url]
             (let [url (if (string/starts-with? url "file:")
             (let [url (if (string/starts-with? url "file:")
@@ -140,7 +140,7 @@
 
 
           context-menu-handler
           context-menu-handler
           (context-menu/setup-context-menu! win)]
           (context-menu/setup-context-menu! win)]
-      
+
       (doto web-contents
       (doto web-contents
         (.on "new-window" new-win-handler)
         (.on "new-window" new-win-handler)
         (.on "will-navigate" will-navigate-handler))
         (.on "will-navigate" will-navigate-handler))

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

@@ -17,7 +17,8 @@
             [frontend.ui :as ui]
             [frontend.ui :as ui]
             [frontend.handler.notification :as notification]
             [frontend.handler.notification :as notification]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.repo :as repo-handler]
-            [frontend.handler.user :as user]))
+            [frontend.handler.user :as user]
+            [dommy.core :as dom]))
 
 
 (defn persist-dbs!
 (defn persist-dbs!
   []
   []
@@ -124,6 +125,15 @@
                                        :on-error   error-f}]
                                        :on-error   error-f}]
                          (repo-handler/persist-db! repo handlers))))
                          (repo-handler/persist-db! repo handlers))))
 
 
+  (js/window.apis.on "foundInPage"
+                     (fn [data]
+                       (let [data' (bean/->clj data)]
+                         (state/set-state! [:ui/find-in-page :matches] data')
+                         (dom/remove-style! (dom/by-id "search-in-page-input") :visibility)
+                         (dom/set-text! (dom/by-id "search-in-page-placeholder") "")
+                         (ui/focus-element "search-in-page-input")
+                         true)))
+
   (js/window.apis.on "loginCallback"
   (js/window.apis.on "loginCallback"
                      (fn [code]
                      (fn [code]
                        (user/login-callback code)))
                        (user/login-callback code)))

+ 51 - 22
src/main/frontend/components/block.cljs

@@ -63,6 +63,7 @@
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.text :as text]
             [logseq.graph-parser.text :as text]
+            [logseq.graph-parser.property :as gp-property]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util.block-ref :as block-ref]
             [logseq.graph-parser.util.block-ref :as block-ref]
             [logseq.graph-parser.util.page-ref :as page-ref]
             [logseq.graph-parser.util.page-ref :as page-ref]
@@ -1821,9 +1822,15 @@
   [:span ", "])
   [:span ", "])
 
 
 (rum/defc property-cp
 (rum/defc property-cp
-  [config block k v]
-  (let [date (and (= k :date) (date/get-locale-string (str v)))
-        property-pages-enabled? (contains? #{true nil} (:property-pages/enabled? (state/get-config)))]
+  [config block k value]
+  (let [date (and (= k :date) (date/get-locale-string (str value)))
+        user-config (state/get-config)
+        ;; In this mode and when value is a set of refs, display full property text
+        ;; because :block/properties value only contains refs but user wants to see text
+        v (if (and (:rich-property-values? user-config) (coll? value))
+            (gp-property/property-value-from-content (name k) (:block/content block))
+            value)
+        property-pages-enabled? (contains? #{true nil} (:property-pages/enabled? user-config))]
     [:div
     [:div
      (if property-pages-enabled?
      (if property-pages-enabled?
        (page-cp (assoc config :property? true) {:block/name (subs (str k) 1)})
        (page-cp (assoc config :property? true) {:block/name (subs (str k) 1)})
@@ -2163,9 +2170,10 @@
       [:div.more (ui/icon "dots-circle-horizontal" {:style {:fontSize 16}})])]])
       [:div.more (ui/icon "dots-circle-horizontal" {:style {:fontSize 16}})])]])
 
 
 (rum/defcs block-content-or-editor < rum/reactive
 (rum/defcs block-content-or-editor < rum/reactive
-  (rum/local true :hide-block-refs?)
+  (rum/local true ::hide-block-refs?)
   [state config {:block/keys [uuid format] :as block} edit-input-id block-id heading-level edit? hide-block-refs-count?]
   [state config {:block/keys [uuid format] :as block} edit-input-id block-id heading-level edit? hide-block-refs-count?]
-  (let [*hide-block-refs? (get state :hide-block-refs?)
+  (let [*hide-block-refs? (get state ::hide-block-refs?)
+        hide-block-refs? @*hide-block-refs?
         editor-box (get config :editor-box)
         editor-box (get config :editor-box)
         editor-id (str "editor-" edit-input-id)
         editor-id (str "editor-" edit-input-id)
         slide? (:slide? config)
         slide? (:slide? config)
@@ -2222,7 +2230,7 @@
 
 
              (block-refs-count block *hide-block-refs?)])]
              (block-refs-count block *hide-block-refs?)])]
 
 
-         (when (and (not @*hide-block-refs?) (> refs-count 0))
+         (when (and (not hide-block-refs?) (> refs-count 0))
            (let [refs-cp (state/get-component :block/linked-references)]
            (let [refs-cp (state/get-component :block/linked-references)]
              (refs-cp uuid)))]))))
              (refs-cp uuid)))]))))
 
 
@@ -2246,6 +2254,7 @@
         block-el-id (str "ls-block-" blocks-container-id "-" uuid)
         block-el-id (str "ls-block-" blocks-container-id "-" uuid)
         config {:id (str uuid)
         config {:id (str uuid)
                 :db/id (:db/id block-entity)
                 :db/id (:db/id block-entity)
+                :block/uuid uuid
                 :block? true
                 :block? true
                 :editor-box (state/get-component :editor/box)}
                 :editor-box (state/get-component :editor/box)}
         edit-input-id (str "edit-block-" blocks-container-id "-" uuid)
         edit-input-id (str "edit-block-" blocks-container-id "-" uuid)
@@ -2468,9 +2477,7 @@
         *navigating-block (get state ::navigating-block)
         *navigating-block (get state ::navigating-block)
         navigating-block (rum/react *navigating-block)
         navigating-block (rum/react *navigating-block)
         navigated? (and (not= (:block/uuid block) navigating-block) navigating-block)
         navigated? (and (not= (:block/uuid block) navigating-block) navigating-block)
-        block (if (or navigated?
-                      custom-query?
-                      (and ref? (:block/uuid config)))
+        block (if navigated?
                 (let [block (db/pull [:block/uuid navigating-block])
                 (let [block (db/pull [:block/uuid navigating-block])
                       blocks (db/get-paginated-blocks repo (:db/id block)
                       blocks (db/get-paginated-blocks repo (:db/id block)
                                                       {:scoped-block-id (:db/id block)})
                                                       {:scoped-block-id (:db/id block)})
@@ -2613,7 +2620,7 @@
   (let [repo (state/get-current-repo)
   (let [repo (state/get-current-repo)
         ref? (:ref? config)
         ref? (:ref? config)
         custom-query? (boolean (:custom-query? config))]
         custom-query? (boolean (:custom-query? config))]
-    (if (and ref? (not custom-query?) (not (:ref-query-child? config)))
+    (if (and (or ref? custom-query?) (not (:ref-query-child? config)))
       (ui/lazy-visible
       (ui/lazy-visible
        (fn [] (block-container-inner state repo config block)))
        (fn [] (block-container-inner state repo config block)))
       (block-container-inner state repo config block))))
       (block-container-inner state repo config block))))
@@ -2997,7 +3004,8 @@
    (ui/block-error "Query Error:" {:content (:query q)})
    (ui/block-error "Query Error:" {:content (:query q)})
    (ui/lazy-visible
    (ui/lazy-visible
     (fn [] (custom-query* config q))
     (fn [] (custom-query* config q))
-    {:debug-id q})))
+    {:debug-id q
+     :trigger-once? false})))
 
 
 (defn admonition
 (defn admonition
   [config type result]
   [config type result]
@@ -3344,7 +3352,7 @@
              (assoc state
              (assoc state
                     ::initial-block    first-block
                     ::initial-block    first-block
                     ::navigating-block (atom (:block/uuid first-block)))))}
                     ::navigating-block (atom (:block/uuid first-block)))))}
-  [state blocks config]
+  [state block config]
   (let [repo (state/get-current-repo)
   (let [repo (state/get-current-repo)
         *navigating-block (::navigating-block state)
         *navigating-block (::navigating-block state)
         navigating-block (rum/react *navigating-block)
         navigating-block (rum/react *navigating-block)
@@ -3357,10 +3365,10 @@
                  (let [block navigating-block-entity]
                  (let [block navigating-block-entity]
                    (db/get-paginated-blocks repo (:db/id block)
                    (db/get-paginated-blocks repo (:db/id block)
                                             {:scoped-block-id (:db/id block)}))
                                             {:scoped-block-id (:db/id block)}))
-                 blocks)]
+                 [block])]
     [:div
     [:div
      (when (:breadcrumb-show? config)
      (when (:breadcrumb-show? config)
-       (breadcrumb config (state/get-current-repo) navigating-block
+       (breadcrumb config (state/get-current-repo) (or navigating-block (:block/uuid block))
                    {:show-page? false
                    {:show-page? false
                     :navigating-block *navigating-block}))
                     :navigating-block *navigating-block}))
      (blocks-container blocks (assoc config
      (blocks-container blocks (assoc config
@@ -3375,8 +3383,7 @@
    (cond-> option
    (cond-> option
      (:document/mode? config) (assoc :class "doc-mode"))
      (:document/mode? config) (assoc :class "doc-mode"))
    (cond
    (cond
-     (and (or (:ref? config) (:custom-query? config))
-          (:group-by-page? config))
+     (and (:custom-query? config) (:group-by-page? config))
      [:div.flex.flex-col
      [:div.flex.flex-col
       (let [blocks (sort-by (comp :block/journal-day first) > blocks)]
       (let [blocks (sort-by (comp :block/journal-day first) > blocks)]
         (for [[page blocks] blocks]
         (for [[page blocks] blocks]
@@ -3384,8 +3391,7 @@
            (fn []
            (fn []
              (let [alias? (:block/alias? page)
              (let [alias? (:block/alias? page)
                    page (db/entity (:db/id page))
                    page (db/entity (:db/id page))
-                   blocks (tree/non-consecutive-blocks->vec-tree blocks)
-                   parent-blocks (group-by :block/parent blocks)]
+                   blocks' (tree/non-consecutive-blocks->vec-tree blocks)]
                [:div.my-2 (cond-> {:key (str "page-" (:db/id page))}
                [:div.my-2 (cond-> {:key (str "page-" (:db/id page))}
                             (:ref? config)
                             (:ref? config)
                             (assoc :class "color-level px-2 sm:px-7 py-2 rounded"))
                             (assoc :class "color-level px-2 sm:px-7 py-2 rounded"))
@@ -3393,11 +3399,34 @@
                  [:div
                  [:div
                   (page-cp config page)
                   (page-cp config page)
                   (when alias? [:span.text-sm.font-medium.opacity-50 " Alias"])]
                   (when alias? [:span.text-sm.font-medium.opacity-50 " Alias"])]
-                 (for [[parent blocks] parent-blocks]
+                 (for [block blocks']
                    (rum/with-key
                    (rum/with-key
-                     (breadcrumb-with-container blocks config)
-                     (:db/id parent)))
-                 {:debug-id page})])))))]
+                     (breadcrumb-with-container block config)
+                     (:db/id block)))
+                 {:debug-id page
+                  :trigger-once? false})])))))]
+
+     (and (:ref? config) (:group-by-page? config))
+     [:div.flex.flex-col
+      (let [blocks (sort-by (comp :block/journal-day first) > blocks)]
+        (for [[page parent-blocks] blocks]
+         (ui/lazy-visible
+          (fn []
+            (let [alias? (:block/alias? page)
+                  page (db/entity (:db/id page))]
+              [:div.my-2 (cond-> {:key (str "page-" (:db/id page))}
+                           (:ref? config)
+                           (assoc :class "color-level px-2 sm:px-7 py-2 rounded"))
+               (ui/foldable
+                [:div
+                 (page-cp config page)
+                 (when alias? [:span.text-sm.font-medium.opacity-50 " Alias"])]
+                (for [block parent-blocks]
+                  (let [block' (update block :block/children tree/non-consecutive-blocks->vec-tree)]
+                    (rum/with-key
+                      (breadcrumb-with-container block' config)
+                      (:db/id block'))))
+                {:debug-id page})])))))]
 
 
      (and (:group-by-page? config)
      (and (:group-by-page? config)
           (vector? (first blocks)))
           (vector? (first blocks)))

+ 207 - 192
src/main/frontend/components/content.cljs

@@ -55,35 +55,43 @@
 
 
 (rum/defc custom-context-menu-content
 (rum/defc custom-context-menu-content
   []
   []
-  [:div#custom-context-menu
-   [:div.py-1.rounded-md.bg-base-3.shadow-xs
-    (ui/menu-link
-     {:key "cut"
-      :on-click #(editor-handler/cut-selection-blocks true)}
-     "Cut")
-    (ui/menu-link
-     {:key "copy"
-      :on-click editor-handler/copy-selection-blocks}
-     "Copy")
-    (ui/menu-link
-     {:key "copy as"
-      :on-click (fn [_]
-                  (let [block-uuids (editor-handler/get-selected-toplevel-block-uuids)]
-                    (state/set-modal!
-                     #(export/export-blocks block-uuids))))}
-     "Copy as")
-    (ui/menu-link
-     {:key "copy block refs"
-      :on-click editor-handler/copy-block-refs}
-     "Copy block refs")
-    (ui/menu-link
-     {:key "copy block embeds"
-      :on-click editor-handler/copy-block-embeds}
-     "Copy block embeds")
-    (ui/menu-link
-     {:key "cycle todos"
-      :on-click editor-handler/cycle-todos!}
-     "Cycle todos")]])
+  [:.menu-links-wrapper
+   (ui/menu-link
+    {:key "cut"
+     :on-click #(editor-handler/cut-selection-blocks true)}
+    "Cut"
+    nil)
+   (ui/menu-link
+    {:key "copy"
+     :on-click editor-handler/copy-selection-blocks}
+    "Copy"
+    nil)
+   (ui/menu-link
+    {:key "copy as"
+     :on-click (fn [_]
+                 (let [block-uuids (editor-handler/get-selected-toplevel-block-uuids)]
+                   (state/set-modal!
+                    #(export/export-blocks block-uuids))))}
+    "Copy as..."
+    nil)
+   (ui/menu-link
+    {:key "copy block refs"
+     :on-click editor-handler/copy-block-refs}
+    "Copy block refs"
+    nil)
+   (ui/menu-link
+    {:key "copy block embeds"
+     :on-click editor-handler/copy-block-embeds}
+    "Copy block embeds"
+    nil)
+   
+   [:hr.menu-separator]
+
+   (ui/menu-link
+    {:key "cycle todos"
+     :on-click editor-handler/cycle-todos!}
+    "Cycle todos"
+    nil)])
 
 
 ;; FIXME: Make it configurable
 ;; FIXME: Make it configurable
 (def block-background-colors
 (def block-background-colors
@@ -145,192 +153,199 @@
                                           (editor-handler/set-block-property! block-id :template-including-parent false))
                                           (editor-handler/set-block-property! block-id :template-including-parent false))
                                         (state/hide-custom-context-menu!)))))))])
                                         (state/hide-custom-context-menu!)))))))])
       (ui/menu-link
       (ui/menu-link
-       {:key "Make template"
+       {:key "Make a Template"
         :on-click (fn [e]
         :on-click (fn [e]
                     (util/stop e)
                     (util/stop e)
                     (reset! edit? true))}
                     (reset! edit? true))}
-       "Make template"))))
+       "Make a Template"
+       nil))))
 
 
 (rum/defc ^:large-vars/cleanup-todo block-context-menu-content
 (rum/defc ^:large-vars/cleanup-todo block-context-menu-content
   [_target block-id]
   [_target block-id]
-
-  (let [*el-ref (rum/use-ref nil)]
-
-    (rum/use-effect!
-     (fn []
-       (js/setTimeout
-        (fn []
-          (let [^js el (rum/deref *el-ref)
-               {:keys [x y]} (util/calc-delta-rect-offset el js/document.documentElement)]
-           (set! (.. el -style -transform)
-                 (str "translate3d(" (if (neg? x) x 0) "px," (if (neg? y) (- y 10) 0) "px" ",0)"))))
-        10)
-       #())
-     [])
-
     (when-let [block (db/entity [:block/uuid block-id])]
     (when-let [block (db/entity [:block/uuid block-id])]
       (let [properties (:block/properties block)
       (let [properties (:block/properties block)
             heading? (true? (:heading properties))]
             heading? (true? (:heading properties))]
-        [:div#custom-context-menu
-         {:ref *el-ref}
-         [:div.py-1.rounded-md.bg-base-3.shadow-xs
-          [:div.flex-row.flex.justify-between.py-4.pl-2
-           [:div.flex-row.flex.justify-between
-            (for [color block-background-colors]
-              [:a.m-2.shadow-sm
-               {:on-click (fn [_e]
-                            (editor-handler/set-block-property! block-id "background-color" color))}
-               [:div.heading-bg {:style {:background-color color}}]])]
-           [:a.text-sm
+        [:.menu-links-wrapper
+         [:div.flex-row.flex.justify-between.pb-2.pt-1.px-2
+          [:div.flex-row.flex.justify-between
+           (for [color block-background-colors]
+             [:a.m-2.shadow-sm
+              {:on-click (fn [_e]
+                           (editor-handler/set-block-property! block-id "background-color" color))}
+              [:div.heading-bg {:style {:background-color color}}]])
+           [:a.m-2.shadow-sm
             {:title    (t :remove-background)
             {:title    (t :remove-background)
-             :style    {:margin-right 14
-                        :margin-top   4}
              :on-click (fn [_e]
              :on-click (fn [_e]
                          (editor-handler/remove-block-property! block-id "background-color"))}
                          (editor-handler/remove-block-property! block-id "background-color"))}
-            "Clear"]]
-
-          (ui/menu-link
-           {:key      "Convert heading"
-            :on-click (fn [_e]
-                        (if heading?
-                          (editor-handler/remove-block-property! block-id :heading)
-                          (editor-handler/set-block-property! block-id :heading true)))}
-           (if heading?
-             "Convert back to a block"
-             "Convert to a heading"))
-
-          (ui/menu-link
-           {:key      "Open in sidebar"
-            :on-click (fn [_e]
-                        (editor-handler/open-block-in-sidebar! block-id))}
-           "Open in sidebar")
-
-          (ui/menu-link
-           {:key      "Copy block ref"
-            :on-click (fn [_e]
-                        (editor-handler/copy-block-ref! block-id block-ref/->block-ref))}
-           "Copy block ref")
-
-          (ui/menu-link
-           {:key      "Copy block embed"
-            :on-click (fn [_e]
-                        (editor-handler/copy-block-ref! block-id #(util/format "{{embed ((%s))}}" %)))}
-           "Copy block embed")
+            [:div.heading-bg.remove "-"]]]]
+         
+         [:hr.menu-separator]
+
+         (ui/menu-link
+          {:key      "Open in sidebar"
+           :on-click (fn [_e]
+                       (editor-handler/open-block-in-sidebar! block-id))}
+          "Open in sidebar"
+          ["shift" "click"])
+
+         [:hr.menu-separator]
+
+         (ui/menu-link
+          {:key      "Copy block ref"
+           :on-click (fn [_e]
+                       (editor-handler/copy-block-ref! block-id block-ref/->block-ref))}
+          "Copy block ref"
+          nil)
+
+         (ui/menu-link
+          {:key      "Copy block embed"
+           :on-click (fn [_e]
+                       (editor-handler/copy-block-ref! block-id #(util/format "{{embed ((%s))}}" %)))}
+          "Copy block embed"
+          nil)
 
 
           ;; TODO Logseq protocol mobile support
           ;; TODO Logseq protocol mobile support
-          (when (util/electron?)
-            (ui/menu-link
-             {:key      "Copy block URL"
-              :on-click (fn [_e]
-                          (let [current-repo (state/get-current-repo)
-                                tap-f (fn [block-id]
-                                        (url-util/get-logseq-graph-uuid-url nil current-repo block-id))]
-                            (editor-handler/copy-block-ref! block-id tap-f)))}
-             "Copy block URL"))
-
-          (block-template block-id)
-
-          (ui/menu-link
-           {:key      "Copy as"
-            :on-click (fn [_]
-                        (state/set-modal! #(export/export-blocks [block-id])))}
-           "Copy as")
-
-          (if (srs/card-block? block)
-            (ui/menu-link
-             {:key      "Preview Card"
-              :on-click #(srs/preview (:db/id block))}
-             "Preview Card")
-            (ui/menu-link
-             {:key      "Make a Card"
-              :on-click #(srs/make-block-a-card! block-id)}
-             "Make a Card"))
-
-          (ui/menu-link
-           {:key      "Cut"
-            :on-click (fn [_e]
-                        (editor-handler/cut-block! block-id))}
-           "Cut")
-
-          (ui/menu-link
-           {:key      "Expand all"
-            :on-click (fn [_e]
-                        (editor-handler/expand-all! block-id))}
-           "Expand all")
-
-          (ui/menu-link
-           {:key      "Collapse all"
-            :on-click (fn [_e]
-                        (editor-handler/collapse-all! block-id {}))}
-           "Collapse all")
-
-          (when (state/sub [:plugin/simple-commands])
-            (when-let [cmds (state/get-plugins-commands-with-type :block-context-menu-item)]
-              (for [[_ {:keys [key label] :as cmd} action pid] cmds]
-                (ui/menu-link
-                 {:key      key
-                  :on-click #(commands/exec-plugin-simple-command!
-                              pid (assoc cmd :uuid block-id) action)}
-                 label))))
-
-          (when (state/sub [:ui/developer-mode?])
-            (ui/menu-link
-             {:key      "(Dev) Show block data"
-              :on-click (fn []
-                          (let [block-data (with-out-str (pprint/pprint (db/pull [:block/uuid block-id])))]
-                            (println block-data)
-                            (notification/show!
-                             [:div
-                              [:pre.code block-data]
-                              [:br]
-                              (ui/button "Copy to clipboard"
-                                :on-click #(.writeText js/navigator.clipboard block-data))]
-                             :success
-                             false)))}
-             "(Dev) Show block data"))]]))))
+         (when (util/electron?)
+           (ui/menu-link
+            {:key      "Copy block URL"
+             :on-click (fn [_e]
+                         (let [current-repo (state/get-current-repo)
+                               tap-f (fn [block-id]
+                                       (url-util/get-logseq-graph-uuid-url nil current-repo block-id))]
+                           (editor-handler/copy-block-ref! block-id tap-f)))}
+            "Copy block URL"
+            nil))
+
+         (ui/menu-link
+          {:key      "Copy as"
+           :on-click (fn [_]
+                       (state/set-modal! #(export/export-blocks [block-id])))}
+          "Copy as..."
+          nil)
+
+         (ui/menu-link
+          {:key      "Cut"
+           :on-click (fn [_e]
+                       (editor-handler/cut-block! block-id))}
+          "Cut"
+          nil)
+
+         [:hr.menu-separator]
+
+         (ui/menu-link
+          {:key      "Convert heading"
+           :on-click (fn [_e]
+                       (if heading?
+                         (editor-handler/remove-block-property! block-id :heading)
+                         (editor-handler/set-block-property! block-id :heading true)))}
+          (if heading?
+            "Convert back to a block"
+            "Convert to a heading")
+          nil)
+
+         (block-template block-id)
+
+         (if (srs/card-block? block)
+           (ui/menu-link
+            {:key      "Preview Card"
+             :on-click #(srs/preview (:db/id block))}
+            "Preview Card"
+            nil)
+           (ui/menu-link
+            {:key      "Make a Card"
+             :on-click #(srs/make-block-a-card! block-id)}
+            "Make a Flashcard"
+            nil))
+
+         [:hr.menu-separator]
+
+         (ui/menu-link
+          {:key      "Expand all"
+           :on-click (fn [_e]
+                       (editor-handler/expand-all! block-id))}
+          "Expand all"
+          nil)
+
+         (ui/menu-link
+          {:key      "Collapse all"
+           :on-click (fn [_e]
+                       (editor-handler/collapse-all! block-id {}))}
+          "Collapse all"
+          nil)
+
+         (when (state/sub [:plugin/simple-commands])
+           (when-let [cmds (state/get-plugins-commands-with-type :block-context-menu-item)]
+             (for [[_ {:keys [key label] :as cmd} action pid] cmds]
+               (ui/menu-link
+                {:key      key
+                 :on-click #(commands/exec-plugin-simple-command!
+                             pid (assoc cmd :uuid block-id) action)}
+                label
+                nil))))
+
+         (when (state/sub [:ui/developer-mode?])
+           (ui/menu-link
+            {:key      "(Dev) Show block data"
+             :on-click (fn []
+                         (let [block-data (with-out-str (pprint/pprint (db/pull [:block/uuid block-id])))]
+                           (println block-data)
+                           (notification/show!
+                            [:div
+                             [:pre.code block-data]
+                             [:br]
+                             (ui/button "Copy to clipboard"
+                                        :on-click #(.writeText js/navigator.clipboard block-data))]
+                            :success
+                            false)))}
+            "(Dev) Show block data"
+            nil))])))
 
 
 (rum/defc block-ref-custom-context-menu-content
 (rum/defc block-ref-custom-context-menu-content
   [block block-ref-id]
   [block block-ref-id]
   (when (and block block-ref-id)
   (when (and block block-ref-id)
-    [:div#custom-context-menu
-     [:div.py-1.rounded-md.bg-base-3.shadow-xs
-      (ui/menu-link
-       {:key "open-in-sidebar"
-        :on-click (fn []
-                    (state/sidebar-add-block!
-                     (state/get-current-repo)
-                     block-ref-id
-                     :block-ref))}
-       "Open in sidebar")
-      (ui/menu-link
-       {:key "copy"
-        :on-click (fn [] (editor-handler/copy-current-ref block-ref-id))}
-       "Copy this reference")
-      (ui/menu-link
-       {:key "delete"
-        :on-click (fn [] (editor-handler/delete-current-ref! block block-ref-id))}
-       "Delete this reference")
-      (ui/menu-link
-       {:key "replace-with-text"
-        :on-click (fn [] (editor-handler/replace-ref-with-text! block block-ref-id))}
-       "Replace with text")
-      (ui/menu-link
-       {:key "replace-with-embed"
-        :on-click (fn [] (editor-handler/replace-ref-with-embed! block block-ref-id))}
-       "Replace with embed")]]))
+    [:.menu-links-wrapper
+     (ui/menu-link
+      {:key "open-in-sidebar"
+       :on-click (fn []
+                   (state/sidebar-add-block!
+                    (state/get-current-repo)
+                    block-ref-id
+                    :block-ref))}
+      "Open in sidebar"
+      ["shift" "click"])
+     (ui/menu-link
+      {:key "copy"
+       :on-click (fn [] (editor-handler/copy-current-ref block-ref-id))}
+      "Copy this reference"
+      nil)
+     (ui/menu-link
+      {:key "delete"
+       :on-click (fn [] (editor-handler/delete-current-ref! block block-ref-id))}
+      "Delete this reference"
+      nil)
+     (ui/menu-link
+      {:key "replace-with-text"
+       :on-click (fn [] (editor-handler/replace-ref-with-text! block block-ref-id))}
+      "Replace with text"
+      nil)
+     (ui/menu-link
+      {:key "replace-with-embed"
+       :on-click (fn [] (editor-handler/replace-ref-with-embed! block block-ref-id))}
+      "Replace with embed"
+      nil)]))
 
 
 (rum/defc page-title-custom-context-menu-content
 (rum/defc page-title-custom-context-menu-content
   [page]
   [page]
   (when-not (string/blank? page)
   (when-not (string/blank? page)
     (let [page-menu-options (page-menu/page-menu page)]
     (let [page-menu-options (page-menu/page-menu page)]
-      [:div#custom-context-menu
-       [:div.py-1.rounded-md.bg-base-3.shadow-xs
-        (for [{:keys [title options]} page-menu-options]
-          (ui/menu-link
-           (merge
-            {:key title}
-            options)
-           title))]])))
+      [:.menu-links-wrapper
+       (for [{:keys [title options]} page-menu-options]
+         (ui/menu-link
+          (merge
+           {:key title}
+           options)
+          title
+          nil))])))
 
 
 ;; TODO: content could be changed
 ;; TODO: content could be changed
 ;; Also, keyboard bindings should only be activated after
 ;; Also, keyboard bindings should only be activated after

+ 2 - 4
src/main/frontend/components/content.css

@@ -12,10 +12,8 @@
 }
 }
 
 
 #custom-context-menu {
 #custom-context-menu {
-  @apply rounded-md shadow-lg transition ease-out duration-100 transform
-  opacity-100 scale-100 absolute overflow-y-auto;
+  @apply transition ease-out duration-100 transform
+  opacity-100 scale-100 absolute;
 
 
-  max-height: calc(100vh - 100px) !important;;
-  overflow-y: scroll;
   z-index: calc(var(--ls-z-index-level-1) + 1);
   z-index: calc(var(--ls-z-index-level-1) + 1);
 }
 }

+ 87 - 0
src/main/frontend/components/find_in_page.cljs

@@ -0,0 +1,87 @@
+(ns frontend.components.find-in-page
+  (:require [rum.core :as rum]
+            [frontend.ui :as ui]
+            [frontend.state :as state]
+            [frontend.util :as util]
+            [frontend.handler.search :as search-handler :refer [debounced-search]]
+            [goog.dom :as gdom]
+            [frontend.mixins :as mixins]
+            [clojure.string :as string]))
+
+(rum/defc search-input
+  [q matches]
+  [:div.flex.w-48.relative
+   [:input#search-in-page-input.form-input.block.sm:text-sm.sm:leading-5.my-2.border-none.mr-4.outline-none
+    {:auto-focus true
+     :placeholder "Find in page"
+     :aria-label "Find in page"
+     :value q
+     :on-change (fn [e]
+                  (let [value (util/evalue e)]
+                    (state/set-state! [:ui/find-in-page :q] value)
+                    (debounced-search)))}]
+   (when-not (string/blank? q)
+     (when-let [total (:matches matches)]
+      [:div.text-sm.absolute.top-2.right-0.py-2.px-4
+       (:activeMatchOrdinal matches 0)
+       "/"
+       total]))
+   [:div#search-in-page-placeholder.absolute.top-2.left-0.p-2.sm:text-sm]])
+
+(rum/defc search-inner < rum/static
+  (mixins/event-mixin
+   (fn [state]
+     (mixins/hide-when-esc-or-outside
+      state
+      :node (gdom/getElement "search-in-page")
+      :on-hide (fn []
+                 (search-handler/electron-exit-find-in-page!)))))
+  [{:keys [matches match-case? q]}]
+  [:div#search-in-page.flex.flex-row.absolute.top-2.right-4.shadow-lg.px-2.py-1.faster-fade-in.items-center
+
+   (search-input q matches)
+
+   (ui/button
+    (ui/icon "letter-case")
+    :on-click (fn []
+                (state/update-state! [:ui/find-in-page :match-case?] not)
+                (debounced-search))
+    :intent "link"
+    :small? true
+    :title "Match case"
+    :class (str (when match-case? "active ") "text-lg"))
+
+   (ui/button
+    (ui/icon "caret-up")
+    :on-click (fn []
+                (state/set-state! [:ui/find-in-page :backward?] true)
+                (debounced-search))
+    :intent "link"
+    :small? true
+    :class "text-lg"
+    :title "Previous result")
+
+   (ui/button
+    (ui/icon "caret-down")
+    :on-click (fn []
+                (state/set-state! [:ui/find-in-page :backward?] false)
+                (debounced-search))
+    :intent "link"
+    :small? true
+    :class "text-lg"
+    :title "Next result")
+
+   (ui/button
+    (ui/icon "x")
+    :on-click (fn []
+                (search-handler/electron-exit-find-in-page!))
+    :intent "link"
+    :small? true
+    :class "text-lg"
+    :title "Close")])
+
+(rum/defc search < rum/reactive
+  []
+  (let [{:keys [active?] :as opt} (state/sub :ui/find-in-page)]
+    (when active?
+      (search-inner opt))))

+ 21 - 0
src/main/frontend/components/find_in_page.css

@@ -0,0 +1,21 @@
+#search-in-page {
+  z-index: 999;
+  background-color: var(--ls-primary-background-color);
+
+  .form-input:focus {
+      box-shadow: none;
+  }
+
+  .ui__button[intent='link'],
+  .ui__button[intent='link']:focus {
+      border-color: none;
+  }
+
+  .ui__button {
+      margin-top: 0;
+
+      &.active {
+        color: var(--ls-link-text-color);
+      }
+  }
+}

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

@@ -59,7 +59,8 @@
         (blocks-cp repo page)
         (blocks-cp repo page)
         (ui/lazy-visible
         (ui/lazy-visible
          (fn [] (blocks-cp repo page))
          (fn [] (blocks-cp repo page))
-         {:debug-id (str "journal-blocks " page)}))
+         {:trigger-once? false
+          :debug-id (str "journal-blocks " page)}))
 
 
       {})
       {})
 
 

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

@@ -433,7 +433,7 @@
          (tagged-pages repo page-name))
          (tagged-pages repo page-name))
 
 
        ;; referenced blocks
        ;; referenced blocks
-       (when-not whiteboard?
+       (when-not (or block? whiteboard?)
          [:div {:key "page-references"}
          [:div {:key "page-references"}
           (rum/with-key
           (rum/with-key
             (reference/references route-page-name)
             (reference/references route-page-name)

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

@@ -10,7 +10,8 @@
             [frontend.util.property :as property]
             [frontend.util.property :as property]
             [frontend.format.block :as block]
             [frontend.format.block :as block]
             [medley.core :as medley]
             [medley.core :as medley]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [frontend.modules.outliner.tree :as tree]))
 
 
 ;; TODO: extract to table utils
 ;; TODO: extract to table utils
 (defn- sort-result-by
 (defn- sort-result-by
@@ -82,7 +83,8 @@
   (rum/local false ::select?)
   (rum/local false ::select?)
   [state config current-block result {:keys [page?]} map-inline page-cp ->elem inline-text]
   [state config current-block result {:keys [page?]} map-inline page-cp ->elem inline-text]
   (when current-block
   (when current-block
-    (let [p-sort-by (keyword (get-in current-block [:block/properties :query-sort-by]))
+    (let [result (tree/filter-top-level-blocks result)
+          p-sort-by (keyword (get-in current-block [:block/properties :query-sort-by]))
           p-desc? (get-in current-block [:block/properties :query-sort-desc])
           p-desc? (get-in current-block [:block/properties :query-sort-desc])
           select? (get state ::select?)
           select? (get state ::select?)
           *sort-by-item (get state ::sort-by-item)
           *sort-by-item (get state ::sort-by-item)

+ 124 - 64
src/main/frontend/components/reference.cljs

@@ -13,19 +13,49 @@
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util :as util]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [frontend.modules.outliner.tree :as tree]))
 
 
 (defn- frequencies-sort
 (defn- frequencies-sort
   [references]
   [references]
   (sort-by second #(> %1 %2) references))
   (sort-by second #(> %1 %2) references))
 
 
+(defn filtered-refs
+  [page-name filters filters-atom filtered-references]
+  [:div.flex.gap-1.flex-wrap
+   (for [[ref-name ref-count] filtered-references]
+     (when ref-name
+       (let [lc-reference (string/lower-case ref-name)]
+         (ui/button
+           [:span
+            ref-name
+            (when ref-count [:sup " " ref-count])]
+           :on-click (fn [e]
+                       (swap! filters-atom #(if (nil? (get filters lc-reference))
+                                              (assoc % lc-reference (not (.-shiftKey e)))
+                                              (dissoc % lc-reference)))
+                       (page-handler/save-filter! page-name @filters-atom))
+           :small? true
+           :intent "link"
+           :key ref-name))))])
+
 (rum/defcs filter-dialog-inner < rum/reactive (rum/local "" ::filterSearch)
 (rum/defcs filter-dialog-inner < rum/reactive (rum/local "" ::filterSearch)
-  [state filters-atom _close-fn references page-name]
+  [state filters-atom *references page-name]
   (let [filter-search (get state ::filterSearch)
   (let [filter-search (get state ::filterSearch)
+        references (rum/react *references)
         filtered-references  (frequencies-sort
         filtered-references  (frequencies-sort
                               (if (= @filter-search "")
                               (if (= @filter-search "")
                                 references
                                 references
-                                (search/fuzzy-search references @filter-search :limit 500 :extract-fn first)))]
+                                (search/fuzzy-search references @filter-search :limit 500 :extract-fn first)))
+        filters (rum/react filters-atom)
+        includes (keep (fn [[page include?]]
+                         (let [page' (model-db/get-page-original-name page)]
+                           (when include? [page'])))
+                       filters)
+        excludes (keep (fn [[page include?]]
+                         (let [page' (model-db/get-page-original-name page)]
+                           (when-not include? [page'])))
+                       filters)]
     [:div.ls-filters.filters
     [:div.ls-filters.filters
      [:div.sm:flex.sm:items-start
      [:div.sm:flex.sm:items-start
       [:div.mx-auto.flex-shrink-0.flex.items-center.justify-center.h-12.w-12.rounded-full.bg-gray-200.text-gray-500.sm:mx-0.sm:h-10.sm:w-10
       [:div.mx-auto.flex-shrink-0.flex.items-center.justify-center.h-12.w-12.rounded-full.bg-gray-200.text-gray-500.sm:mx-0.sm:h-10.sm:w-10
@@ -34,6 +64,16 @@
        [:h3#modal-headline.text-lg.leading-6.font-medium "Filter"]
        [:h3#modal-headline.text-lg.leading-6.font-medium "Filter"]
        [:span.text-xs
        [:span.text-xs
         "Click to include and shift-click to exclude. Click again to remove."]]]
         "Click to include and shift-click to exclude. Click again to remove."]]]
+     (when (seq filters)
+       [:div.cp__filters.mb-4.ml-2
+        (when (seq includes)
+          [:div.flex.flex-row.flex-wrap.center-items
+           [:div.mr-1.font-medium.py-1 "Includes: "]
+           (filtered-refs page-name filters filters-atom includes)])
+        (when (seq excludes)
+          [:div.flex.flex-row.flex-wrap
+           [:div.mr-1.font-medium.py-1 "Excludes: " ]
+           (filtered-refs page-name filters filters-atom excludes)])])
      [:div.cp__filters-input-panel.flex
      [:div.cp__filters-input-panel.flex
       (ui/icon "search")
       (ui/icon "search")
       [:input.cp__filters-input.w-full
       [:input.cp__filters-input.w-full
@@ -41,30 +81,17 @@
         :auto-focus true
         :auto-focus true
         :on-change (fn [e]
         :on-change (fn [e]
                      (reset! filter-search (util/evalue e)))}]]
                      (reset! filter-search (util/evalue e)))}]]
-     (when (seq filtered-references)
-       (let [filters (rum/react filters-atom)]
-         [:div.mt-5.sm:mt-4.sm:flex.sm.gap-1.flex-wrap
-          (for [[ref-name ref-count] filtered-references]
-            (when ref-name
-              (let [lc-reference (string/lower-case ref-name)
-                    filtered (get filters lc-reference)
-                    color (condp = filtered
-                            true "text-green-400"
-                            false "text-red-400"
-                            nil)]
-                [:button.border.rounded.px-1.mb-1.mr-1.select-none
-                 {:key ref-name :class color :style {:border-color "currentColor"}
-                  :on-click (fn [e]
-                              (swap! filters-atom #(if (nil? (get filters lc-reference))
-                                                     (assoc % lc-reference (not (.-shiftKey e)))
-                                                     (dissoc % lc-reference)))
-                              (page-handler/save-filter! page-name @filters-atom))}
-                 ref-name [:sub " " ref-count]])))]))]))
+     (let [all-filters (set (keys filters))
+           refs (remove (fn [[page _]] (all-filters (util/page-name-sanity-lc page)))
+                        filtered-references)]
+       (when (seq refs)
+         [:div.mt-4
+          (filtered-refs page-name filters filters-atom refs)]))]))
 
 
 (defn filter-dialog
 (defn filter-dialog
-  [filters-atom references page-name]
-  (fn [close-fn]
-    (filter-dialog-inner filters-atom close-fn references page-name)))
+  [filters-atom *references page-name]
+  (fn []
+    (filter-dialog-inner filters-atom *references page-name)))
 
 
 (rum/defc block-linked-references < rum/reactive db-mixins/query
 (rum/defc block-linked-references < rum/reactive db-mixins/query
   [block-id]
   [block-id]
@@ -80,16 +107,10 @@
      (content/content block-id
      (content/content block-id
                       {:hiccup ref-hiccup})]))
                       {:hiccup ref-hiccup})]))
 
 
-(rum/defc references-inner < rum/reactive db-mixins/query
-  [page-name block-id filters *filtered-ref-blocks ref-pages]
+(rum/defc references-inner
+  [page-name filters filtered-ref-blocks]
   [:div.references-blocks
   [:div.references-blocks
-   (let [ref-blocks (if block-id
-                      (db/get-block-referenced-blocks block-id)
-                      (db/get-page-referenced-blocks page-name))
-         filtered-ref-blocks (if block-id
-                               ref-blocks
-                               (block-handler/get-filtered-ref-blocks ref-blocks filters ref-pages))
-         ref-hiccup (block/->hiccup filtered-ref-blocks
+   (let [ref-hiccup (block/->hiccup filtered-ref-blocks
                                     {:id page-name
                                     {:id page-name
                                      :ref? true
                                      :ref? true
                                      :breadcrumb-show? true
                                      :breadcrumb-show? true
@@ -97,24 +118,21 @@
                                      :editor-box editor/box
                                      :editor-box editor/box
                                      :filters filters}
                                      :filters filters}
                                     {})]
                                     {})]
-     (reset! *filtered-ref-blocks filtered-ref-blocks)
      (content/content page-name {:hiccup ref-hiccup}))])
      (content/content page-name {:hiccup ref-hiccup}))])
 
 
 (rum/defc references-cp
 (rum/defc references-cp
-  [repo page-entity page-name block-id filters-atom filter-state n-ref]
+  [page-name filters filters-atom filter-state total filter-n filtered-ref-blocks *ref-pages]
   (let [threshold (state/get-linked-references-collapsed-threshold)
   (let [threshold (state/get-linked-references-collapsed-threshold)
-        default-collapsed? (>= n-ref threshold)
-        filters (when (seq filter-state)
-                  (-> (group-by second filter-state)
-                      (update-vals #(map first %))))
-        *filtered-ref-blocks (atom nil)
-        *collapsed? (atom nil)
-        ref-pages (when-not block-id
-                    (block-handler/get-blocks-refed-pages repo page-entity))]
+        default-collapsed? (>= total threshold)
+        *collapsed? (atom nil)]
     (ui/foldable
     (ui/foldable
      [:div.flex.flex-row.flex-1.justify-between.items-center
      [:div.flex.flex-row.flex-1.justify-between.items-center
-      [:h2.font-bold.opacity-50 (str n-ref " Linked Reference"
-                                     (when (> n-ref 1) "s"))]
+      [:h2.font-bold.opacity-50 (str
+                                 (when (seq filters)
+                                   (str filter-n " of "))
+                                 total
+                                 " Linked Reference"
+                                 (when (> total 1) "s"))]
       [:a.filter.fade-link
       [:a.filter.fade-link
        {:title "Filter"
        {:title "Filter"
         :on-mouse-over (fn [_e]
         :on-mouse-over (fn [_e]
@@ -124,10 +142,8 @@
         :on-mouse-down (fn [e]
         :on-mouse-down (fn [e]
                          (util/stop-propagation e))
                          (util/stop-propagation e))
         :on-click (fn []
         :on-click (fn []
-                    (let [ref-pages (map :block/original-name ref-pages)
-                          references (frequencies ref-pages)]
-                      (state/set-modal! (filter-dialog filters-atom references page-name)
-                                        {:center? true})))}
+                    (state/set-modal! (filter-dialog filters-atom *ref-pages page-name)
+                                      {:center? true}))}
        (ui/icon "filter" {:class (cond
        (ui/icon "filter" {:class (cond
                                    (empty? filter-state)
                                    (empty? filter-state)
                                    ""
                                    ""
@@ -140,36 +156,81 @@
                           :style {:fontSize 24}})]]
                           :style {:fontSize 24}})]]
 
 
      (fn []
      (fn []
-       (references-inner page-name block-id filters *filtered-ref-blocks ref-pages))
+       (references-inner page-name filters filtered-ref-blocks))
 
 
      {:default-collapsed? default-collapsed?
      {:default-collapsed? default-collapsed?
       :title-trigger? true
       :title-trigger? true
       :init-collapsed (fn [collapsed-atom]
       :init-collapsed (fn [collapsed-atom]
                         (reset! *collapsed? collapsed-atom))})))
                         (reset! *collapsed? collapsed-atom))})))
 
 
+(defn- get-filtered-children
+  [block parent->blocks]
+  (let [children (get parent->blocks (:db/id block))]
+    (set
+     (loop [blocks children
+            result (vec children)]
+       (if (empty? blocks)
+         result
+         (let [fb (first blocks)
+               children (get parent->blocks (:db/id fb))]
+           (recur
+            (concat children (rest blocks))
+            (conj result fb))))))))
+
 (rum/defcs references* < rum/reactive db-mixins/query
 (rum/defcs references* < rum/reactive db-mixins/query
+  (rum/local nil ::ref-pages)
   {:init (fn [state]
   {:init (fn [state]
            (let [page-name (first (:rum/args state))
            (let [page-name (first (:rum/args state))
                  filters (when page-name
                  filters (when page-name
-                           (atom (page-handler/get-filters (string/lower-case page-name))))]
+                           (atom (page-handler/get-filters (util/page-name-sanity-lc page-name))))]
              (assoc state ::filters filters)))}
              (assoc state ::filters filters)))}
   [state page-name]
   [state page-name]
   (when page-name
   (when page-name
-    (let [page-name (string/lower-case page-name)
-          page-entity (db/entity [:block/name page-name])
+    (let [page-name (util/page-name-sanity-lc page-name)
+          *ref-pages (::ref-pages state)
           repo (state/get-current-repo)
           repo (state/get-current-repo)
           filters-atom (get state ::filters)
           filters-atom (get state ::filters)
           filter-state (rum/react filters-atom)
           filter-state (rum/react filters-atom)
-          block-id (parse-uuid page-name)
-          id (if block-id
-               (:db/id (db/pull [:block/uuid block-id]))
-               (:db/id page-entity))
-          n-ref (model-db/get-linked-references-count id)]
-      (when (or (seq filter-state) (> n-ref 0))
+          ref-blocks (db/get-page-referenced-blocks page-name)
+          page-id (:db/id (db/entity repo [:block/name page-name]))
+          aliases (db/page-alias-set repo page-name)
+          aliases-exclude-self (set (remove #{page-id} aliases))
+          top-level-blocks (filter (fn [b] (some aliases (set (map :db/id (:block/refs b))))) ref-blocks)
+          top-level-blocks-ids (set (map :db/id top-level-blocks))
+          filters (when (seq filter-state)
+                    (-> (group-by second filter-state)
+                        (update-vals #(map first %))))
+          filtered-ref-blocks (block-handler/filter-blocks ref-blocks filters)
+          total (count top-level-blocks)
+          filtered-top-blocks (filter (fn [b] (top-level-blocks-ids (:db/id b))) filtered-ref-blocks)
+          filter-n (count filtered-top-blocks)
+          parent->blocks (group-by (fn [x] (:db/id (x :block/parent))) filtered-ref-blocks)
+          result (->> (group-by :block/page filtered-top-blocks)
+                      (map (fn [[page blocks]]
+                             (let [blocks (sort-by (fn [b] (not= (:db/id page) (:db/id (:block/parent b)))) blocks)
+                                   result (map (fn [block]
+                                                 (let [filtered-children (get-filtered-children block parent->blocks)
+                                                       refs (when-not (contains? top-level-blocks-ids (:db/id (:block/parent block)))
+                                                              (block-handler/get-blocks-refed-pages aliases (cons block filtered-children)))
+                                                       block' (assoc (tree/block-entity->map block) :block/children filtered-children)]
+                                                   [block' refs])) blocks)
+                                   blocks' (map first result)
+                                   page' (if (contains? aliases-exclude-self (:db/id page))
+                                           {:db/id (:db/id page)
+                                            :block/alias? true
+                                            :block/journal-day (:block/journal-day page)}
+                                           page)]
+                               [[page' blocks'] (mapcat second result)]))))
+          filtered-ref-blocks' (map first result)
+          ref-pages (->>
+                     (mapcat second result)
+                     (map :block/original-name)
+                     frequencies)]
+      (reset! *ref-pages ref-pages)
+      (when (or (seq filter-state) (> filter-n 0))
         [:div.references.flex-1.flex-row
         [:div.references.flex-1.flex-row
          [:div.content.pt-6
          [:div.content.pt-6
-          (references-cp repo page-entity page-name block-id
-                         filters-atom filter-state n-ref)]]))))
+          (references-cp page-name filters filters-atom filter-state total filter-n filtered-ref-blocks' *ref-pages)]]))))
 
 
 (rum/defc references
 (rum/defc references
   [page-name]
   [page-name]
@@ -178,8 +239,7 @@
    (ui/lazy-visible
    (ui/lazy-visible
     (fn []
     (fn []
       (references* page-name))
       (references* page-name))
-    {:trigger-once? true
-     :debug-id (str page-name " references")})))
+    {:debug-id (str page-name " references")})))
 
 
 (rum/defcs unlinked-references-aux
 (rum/defcs unlinked-references-aux
   < rum/reactive db-mixins/query
   < rum/reactive db-mixins/query

+ 8 - 0
src/main/frontend/components/reference.css

@@ -10,3 +10,11 @@
   padding-left: 0.5rem;
   padding-left: 0.5rem;
   align-items: center;
   align-items: center;
 }
 }
+
+.references-blocks .breadcrumb {
+  margin-left: 1.5rem;
+}
+
+.ls-filters {
+    max-width: 704px;
+}

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

@@ -297,7 +297,7 @@
     [:div "Recent search:"]
     [:div "Recent search:"]
     (ui/with-shortcut :go/search-in-page "bottom"
     (ui/with-shortcut :go/search-in-page "bottom"
       [:div.flex-row.flex.align-items
       [:div.flex-row.flex.align-items
-       [:div.mr-2 "Search in page:"]
+       [:div.mr-2 "Search blocks in page:"]
        [:div {:style {:margin-top 3}}
        [:div {:style {:margin-top 3}}
         (ui/toggle in-page-search?
         (ui/toggle in-page-search?
                    (fn [_value]
                    (fn [_value]

+ 34 - 12
src/main/frontend/components/sidebar.cljs

@@ -12,6 +12,7 @@
             [frontend.components.svg :as svg]
             [frontend.components.svg :as svg]
             [frontend.components.theme :as theme]
             [frontend.components.theme :as theme]
             [frontend.components.widgets :as widgets]
             [frontend.components.widgets :as widgets]
+            [frontend.components.find-in-page :as find-in-page]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
             [frontend.db :as db]
@@ -72,7 +73,7 @@
         whiteboard-page? (db-model/whiteboard-page? name)]
         whiteboard-page? (db-model/whiteboard-page? name)]
     [:a {:on-click (fn [e]
     [:a {:on-click (fn [e]
                      (let [name (util/safe-page-name-sanity-lc name)
                      (let [name (util/safe-page-name-sanity-lc name)
-                           source-page (db-model/get-alias-source-page (state/get-current-repo) name) 
+                           source-page (db-model/get-alias-source-page (state/get-current-repo) name)
                            name (if (empty? source-page) name (:block/name source-page))]
                            name (if (empty? source-page) name (:block/name source-page))]
                        (if (and (gobj/get e "shiftKey") (not whiteboard-page?))
                        (if (and (gobj/get e "shiftKey") (not whiteboard-page?))
                          (when-let [page-entity (if (empty? source-page) (db/entity [:block/name name]) source-page)]
                          (when-let [page-entity (if (empty? source-page) (db/entity [:block/name name]) source-page)]
@@ -347,6 +348,9 @@
      [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row
      [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row
       {:data-is-margin-less-pages margin-less-pages?}
       {:data-is-margin-less-pages margin-less-pages?}
 
 
+      (when (util/electron?)
+        (find-in-page/search))
+
       (when show-action-bar?
       (when show-action-bar?
         (action-bar/action-bar))
         (action-bar/action-bar))
 
 
@@ -484,15 +488,41 @@
          :else
          :else
          [:div])])))
          [:div])])))
 
 
+(defn- hide-context-menu-and-clear-selection
+  [e]
+  (state/hide-custom-context-menu!)
+  (when-not (or (gobj/get e "shiftKey")
+                (util/meta-key? e)
+                (state/get-edit-input-id))
+    (editor-handler/clear-selection!)))
+
+(rum/defc render-custom-context-menu
+  [links position]
+  (let [ref (rum/use-ref nil)]
+    (rum/use-effect!
+     #(let [el (rum/deref ref)
+            {:keys [x y]} (util/calc-delta-rect-offset el js/document.documentElement)]
+        (set! (.. el -style -transform)
+              (str "translate3d(" (if (neg? x) x 0) "px," (if (neg? y) (- y 10) 0) "px" ",0)"))))
+    [:<>
+     [:div.menu-backdrop {:on-mouse-down (fn [e] (hide-context-menu-and-clear-selection e))}]
+     [:div#custom-context-menu
+      {:ref ref
+       :style {:left (str (first position) "px")
+               :top (str (second position) "px")}} links]]))
+
 (rum/defc custom-context-menu < rum/reactive
 (rum/defc custom-context-menu < rum/reactive
   []
   []
-  (when (state/sub :custom-context-menu/show?)
-    (when-let [links (state/sub :custom-context-menu/links)]
+  (let [show? (state/sub :custom-context-menu/show?)
+        links (state/sub :custom-context-menu/links)
+        position (state/sub :custom-context-menu/position)]
+    (when (and show? links position)
       (ui/css-transition
       (ui/css-transition
        {:class-names "fade"
        {:class-names "fade"
         :timeout {:enter 500
         :timeout {:enter 500
                   :exit 300}}
                   :exit 300}}
-       links))))
+       (render-custom-context-menu links position)))))
+
 
 
 (rum/defc new-block-mode < rum/reactive
 (rum/defc new-block-mode < rum/reactive
   []
   []
@@ -521,14 +551,6 @@
                    (state/sidebar-add-block! (state/get-current-repo) "help" :help))}
                    (state/sidebar-add-block! (state/get-current-repo) "help" :help))}
       "?"]]))
       "?"]]))
 
 
-(defn- hide-context-menu-and-clear-selection
-  [e]
-  (state/hide-custom-context-menu!)
-  (when-not (or (gobj/get e "shiftKey")
-                (util/meta-key? e)
-                (state/get-edit-input-id))
-    (editor-handler/clear-selection!)))
-
 (rum/defcs ^:large-vars/cleanup-todo sidebar <
 (rum/defcs ^:large-vars/cleanup-todo sidebar <
   (mixins/modal :modal/show?)
   (mixins/modal :modal/show?)
   rum/reactive
   rum/reactive

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

@@ -48,7 +48,7 @@
   get-files get-files-blocks get-files-full get-journals-length
   get-files get-files-blocks get-files-full get-journals-length
   get-latest-journals get-page get-page-alias get-page-alias-names get-paginated-blocks
   get-latest-journals get-page get-page-alias get-page-alias-names get-paginated-blocks
   get-page-blocks-count get-page-blocks-no-cache get-page-file get-page-format get-page-properties
   get-page-blocks-count get-page-blocks-no-cache get-page-file get-page-format get-page-properties
-  get-page-referenced-blocks get-page-referenced-blocks-full get-page-referenced-pages get-page-unlinked-references get-page-referenced-blocks-no-cache
+  get-page-referenced-blocks get-page-referenced-blocks-full get-page-referenced-pages get-page-unlinked-references
   get-all-pages get-pages get-pages-relation get-pages-that-mentioned-page get-public-pages get-tag-pages
   get-all-pages get-pages get-pages-relation get-pages-that-mentioned-page get-public-pages get-tag-pages
   journal-page? page-alias-set pull-block
   journal-page? page-alias-set pull-block
   set-file-last-modified-at! page-empty? page-exists? page-empty-or-dummy? get-alias-source-page
   set-file-last-modified-at! page-empty? page-exists? page-empty-or-dummy? get-alias-source-page

+ 3 - 41
src/main/frontend/db/model.cljs

@@ -1152,18 +1152,6 @@
                                db-utils/seq-flatten)]
                                db-utils/seq-flatten)]
       (mapv (fn [page] [page (get-page-alias repo page)]) mentioned-pages))))
       (mapv (fn [page] [page (get-page-alias repo page)]) mentioned-pages))))
 
 
-(defn get-page-referenced-blocks-no-cache
-  [page-id]
-  (when-let [repo (state/get-current-repo)]
-    (->>
-     (d/q '[:find (pull ?b [*])
-            :in $ ?page-id
-            :where
-            [?b :block/refs ?page-id]]
-          (conn/get-db repo)
-          page-id)
-     (flatten))))
-
 (defn get-page-referenced-blocks-full
 (defn get-page-referenced-blocks-full
   ([page]
   ([page]
    (get-page-referenced-blocks-full (state/get-current-repo) page nil))
    (get-page-referenced-blocks-full (state/get-current-repo) page nil))
@@ -1206,7 +1194,8 @@
          (->>
          (->>
           (react/q repo
           (react/q repo
             [:frontend.db.react/refs page-id]
             [:frontend.db.react/refs page-id]
-            {:query-fn (fn []
+            {:use-cache? false
+             :query-fn (fn []
                          (let [entities (mapcat (fn [id]
                          (let [entities (mapcat (fn [id]
                                                   (:block/_path-refs (db-utils/entity id))) pages)
                                                   (:block/_path-refs (db-utils/entity id))) pages)
                                blocks (map (fn [e] {:block/parent (:block/parent e)
                                blocks (map (fn [e] {:block/parent (:block/parent e)
@@ -1217,34 +1206,7 @@
             nil)
             nil)
           react
           react
           :entities
           :entities
-          (remove (fn [block] (= page-id (:db/id (:block/page block)))))
-          db-utils/group-by-page
-          (map (fn [[k blocks]]
-                 (let [k (if (contains? aliases (:db/id k))
-                           {:db/id (:db/id k)
-                            :block/alias? true
-                            :block/journal-day (:block/journal-day k)}
-                           k)]
-                   [k blocks])))))))))
-
-(defn get-linked-references-count
-  [id]
-  (when-let [block (db-utils/entity id)]
-    (let [repo (state/get-current-repo)
-          page? (:block/name block)
-          result (if page?
-                   (let [pages (page-alias-set repo (:block/name block))]
-                     @(react/q repo [:frontend.db.react/refs-count id] {}
-                        '[:find [?block ...]
-                          :in $ [?ref-page ...] ?id
-                          :where
-                          [?block :block/refs ?ref-page]
-                          [?block :block/page ?p]
-                          [(not= ?p ?id)]]
-                        pages
-                        id))
-                   (:block/_refs block))]
-      (count result))))
+          (remove (fn [block] (= page-id (:db/id (:block/page block)))))))))))
 
 
 (defn get-date-scheduled-or-deadlines
 (defn get-date-scheduled-or-deadlines
   [journal-title]
   [journal-title]

+ 10 - 3
src/main/frontend/db/query_dsl.cljs

@@ -6,7 +6,6 @@
             [clojure.set :as set]
             [clojure.set :as set]
             [clojure.string :as string]
             [clojure.string :as string]
             [clojure.walk :as walk]
             [clojure.walk :as walk]
-            [frontend.state :as state]
             [frontend.date :as date]
             [frontend.date :as date]
             [frontend.db.model :as model]
             [frontend.db.model :as model]
             [frontend.db.query-react :as query-react]
             [frontend.db.query-react :as query-react]
@@ -235,12 +234,20 @@
     (= 4 (count e))
     (= 4 (count e))
     (build-between-three-arg e)))
     (build-between-three-arg e)))
 
 
+
+(defn parse-property-value
+  "Parses non-string property values or any page-ref like values"
+  [v]
+  (if-some [res (text/parse-non-string-property-value v)]
+    res
+    (text/split-page-refs-without-brackets v)))
+
 (defn- build-property-two-arg
 (defn- build-property-two-arg
   [e]
   [e]
   (let [k (string/replace (name (nth e 1)) "_" "-")
   (let [k (string/replace (name (nth e 1)) "_" "-")
         v (nth e 2)
         v (nth e 2)
         v (if-not (nil? v)
         v (if-not (nil? v)
-            (text/parse-property k v (state/get-config))
+            (parse-property-value (str v))
             v)
             v)
         v (if (coll? v) (first v) v)]
         v (if (coll? v) (first v) v)]
     {:query (list 'property '?b (keyword k) v)
     {:query (list 'property '?b (keyword k) v)
@@ -285,7 +292,7 @@
   (let [[k v] (rest e)
   (let [[k v] (rest e)
         k (string/replace (name k) "_" "-")]
         k (string/replace (name k) "_" "-")]
     (if (some? v)
     (if (some? v)
-      (let [v' (text/parse-property k v (state/get-config))
+      (let [v' (parse-property-value (str v))
             val (if (coll? v') (first v') v')]
             val (if (coll? v') (first v') v')]
         {:query (list 'page-property '?p (keyword k) val)
         {:query (list 'page-property '?p (keyword k) val)
          :rules [:page-property]})
          :rules [:page-property]})

+ 8 - 13
src/main/frontend/db/react.cljs

@@ -33,10 +33,6 @@
 ;; ::refs
 ;; ::refs
 ;; get BLOCKS referencing PAGE or BLOCK
 ;; get BLOCKS referencing PAGE or BLOCK
 (s/def ::refs (s/tuple #(= ::refs %) int?))
 (s/def ::refs (s/tuple #(= ::refs %) int?))
-;; ::refs-count
-;; get refs count
-(s/def ::refs-count int?)
-
 ;; custom react-query
 ;; custom react-query
 (s/def ::custom any?)
 (s/def ::custom any?)
 
 
@@ -46,7 +42,6 @@
                                 :journals ::journals
                                 :journals ::journals
                                 :page<-pages ::page<-pages
                                 :page<-pages ::page<-pages
                                 :refs ::refs
                                 :refs ::refs
-                                :refs-count ::refs-count
                                 :custom ::custom))
                                 :custom ::custom))
 
 
 (s/def ::affected-keys (s/coll-of ::react-query-keys))
 (s/def ::affected-keys (s/coll-of ::react-query-keys))
@@ -240,7 +235,10 @@
   (let [blocks (->> (filter (fn [datom] (contains? #{:block/left :block/parent :block/page} (:a datom))) tx-data)
   (let [blocks (->> (filter (fn [datom] (contains? #{:block/left :block/parent :block/page} (:a datom))) tx-data)
                     (map :v)
                     (map :v)
                     (distinct))
                     (distinct))
-        refs (->> (filter (fn [datom] (contains? #{:block/refs :block/path-refs} (:a datom))) tx-data)
+        refs (->> (filter (fn [datom]
+                            (when (contains? #{:block/refs :block/path-refs} (:a datom))
+                              (not= (:v datom)
+                                    (:db/id (:block/page (db-utils/entity (:e datom))))))) tx-data)
                   (map :v)
                   (map :v)
                   (distinct))
                   (distinct))
         other-blocks (->> (filter (fn [datom] (= "block" (namespace (:a datom)))) tx-data)
         other-blocks (->> (filter (fn [datom] (= "block" (namespace (:a datom)))) tx-data)
@@ -259,10 +257,9 @@
                                          (:db/id (:block/page block)))
                                          (:db/id (:block/page block)))
                                 blocks [[::block (:db/id block)]]
                                 blocks [[::block (:db/id block)]]
                                 path-refs (:block/path-refs block)
                                 path-refs (:block/path-refs block)
-                                path-refs' (mapcat (fn [ref]
-                                                     [
-                                                      ;; [::refs-count (:db/id ref)]
-                                                      [::refs (:db/id ref)]]) path-refs)
+                                path-refs' (keep (fn [ref]
+                                                   (when-not (= (:db/id ref) page-id)
+                                                     [::refs (:db/id ref)])) path-refs)
                                 page-blocks (when page-id
                                 page-blocks (when page-id
                                               [[::page-blocks page-id]])]
                                               [[::page-blocks page-id]])]
                             (concat blocks page-blocks path-refs')))
                             (concat blocks page-blocks path-refs')))
@@ -270,9 +267,7 @@
 
 
                        (mapcat
                        (mapcat
                         (fn [ref]
                         (fn [ref]
-                          [
-                           ;; [::refs-count (:db/id entity)]
-                           [::refs ref]])
+                          [[::refs ref]])
                         refs)
                         refs)
 
 
                        (when-let [current-page-id (:db/id (get-current-page))]
                        (when-let [current-page-id (:db/id (get-current-page))]

+ 22 - 41
src/main/frontend/handler/block.cljs

@@ -13,9 +13,7 @@
    [frontend.state :as state]
    [frontend.state :as state]
    [frontend.util :as util]
    [frontend.util :as util]
    [goog.dom :as gdom]
    [goog.dom :as gdom]
-   [logseq.graph-parser.block :as gp-block]
-   [frontend.modules.instrumentation.posthog :as posthog]
-   [cljs-bean.core :as bean]))
+   [logseq.graph-parser.block :as gp-block]))
 
 
 ;;  Fns
 ;;  Fns
 
 
@@ -250,48 +248,31 @@
   (reset! *swipe nil))
   (reset! *swipe nil))
 
 
 (defn get-blocks-refed-pages
 (defn get-blocks-refed-pages
-  [repo page-entity]
-  (let [pages (db-model/page-alias-set repo (:block/name page-entity))
-        refs (->> pages
-                  (mapcat (fn [id] (:block/_path-refs (db/entity id))))
-                  (mapcat (fn [b] (conj (:block/path-refs b) (:block/page b))))
-                  (remove (fn [r] (= (:db/id page-entity) (:db/id r)))))]
+  [aliases ref-blocks]
+  (let [refs (->> (mapcat (fn [b] (conj (:block/path-refs b) (:block/page b))) ref-blocks)
+                  distinct
+                  (remove #(aliases (:db/id %))))]
     (keep (fn [ref]
     (keep (fn [ref]
             (when (:block/name ref)
             (when (:block/name ref)
               {:db/id (:db/id ref)
               {:db/id (:db/id ref)
                :block/name (:block/name ref)
                :block/name (:block/name ref)
                :block/original-name (:block/original-name ref)})) refs)))
                :block/original-name (:block/original-name ref)})) refs)))
 
 
-(defn- filter-blocks
-  [ref-blocks filters ref-pages]
-  (let [ref-pages (distinct ref-pages)]
-    (if (empty? filters)
-      ref-blocks
-      (let [ref-pages (zipmap (map :block/name ref-pages) (map :db/id ref-pages))
-            exclude-ids (->> (keep (fn [page] (get ref-pages page)) (get filters false))
-                             (set))
-            include-ids (->> (keep (fn [page] (get ref-pages page)) (get filters true))
-                             (set))]
-        (cond->> ref-blocks
-          (seq exclude-ids)
-          (remove (fn [block]
-                    (let [ids (set (map :db/id (:block/path-refs block)))]
-                      (seq (set/intersection exclude-ids ids)))))
+(defn filter-blocks
+  [ref-blocks filters]
+  (if (empty? filters)
+    ref-blocks
+    (let [exclude-ids (->> (keep (fn [page] (:db/id (db/entity [:block/name (util/page-name-sanity-lc page)]))) (get filters false))
+                           (set))
+          include-ids (->> (keep (fn [page] (:db/id (db/entity [:block/name (util/page-name-sanity-lc page)]))) (get filters true))
+                           (set))]
+      (cond->> ref-blocks
+        (seq exclude-ids)
+        (remove (fn [block]
+                  (let [ids (set (map :db/id (:block/path-refs block)))]
+                    (seq (set/intersection exclude-ids ids)))))
 
 
-          (seq include-ids)
-          (remove (fn [block]
-                    (let [ids (set (map :db/id (:block/path-refs block)))]
-                      (empty? (set/intersection include-ids ids))))))))))
-
-(defn get-filtered-ref-blocks
-  [ref-blocks filters ref-pages]
-  (try
-    (let [ref-blocks' (doall (mapcat second ref-blocks))
-          filtered-blocks (filter-blocks ref-blocks' filters ref-pages)]
-      (group-by :block/page filtered-blocks))
-    (catch :default e
-      (js/console.error e)
-      (posthog/capture :bad-ref-blocks (bean/->js
-                                        {:ref-blocks ref-blocks
-                                         :filters filters
-                                         :ref-pages ref-pages})))))
+        (seq include-ids)
+        (filter (fn [block]
+                  (let [ids (set (map :db/id (:block/path-refs block)))]
+                    (set/subset? include-ids ids))))))))

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

@@ -2,7 +2,6 @@
   (:require [cljs-bean.core :as bean]
   (:require [cljs-bean.core :as bean]
             [cljs.reader :as reader]
             [cljs.reader :as reader]
             [clojure.string :as string]
             [clojure.string :as string]
-            [dommy.core :as d]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.date :as date]
             [frontend.db :as db]
             [frontend.db :as db]
@@ -116,19 +115,8 @@
 
 
 (defn show-custom-context-menu! [e context-menu-content]
 (defn show-custom-context-menu! [e context-menu-content]
   (util/stop e)
   (util/stop e)
-  (let [client-x (gobj/get e "clientX")
-        client-y (gobj/get e "clientY")
-        scroll-y (util/cur-doc-top)]
-    (state/show-custom-context-menu! context-menu-content)
-
-    ;; FIXME: use setTimeout here because rum renders lazily.
-    (js/setTimeout
-     (fn []
-       (when-let [context-menu (d/by-id "custom-context-menu")]
-        (d/set-style! context-menu
-                      :left (str client-x "px")
-                      :top (str (+ scroll-y client-y) "px"))))
-     10)))
+  (let [position [(gobj/get e "clientX") (gobj/get e "clientY")]]
+    (state/show-custom-context-menu! context-menu-content position)))
 
 
 (defn parse-config
 (defn parse-config
   "Parse configuration from file `content` such as from config.edn."
   "Parse configuration from file `content` such as from config.edn."

+ 6 - 39
src/main/frontend/handler/editor.cljs

@@ -260,40 +260,6 @@
     (and (not= current-id id)
     (and (not= current-id id)
          (db/entity [:block/uuid id]))))
          (db/entity [:block/uuid id]))))
 
 
-(defn- attach-page-properties-if-exists!
-  [block]
-  (if (and (:block/pre-block? block)
-           (seq (:block/properties block)))
-    (let [page-properties (:block/properties block)
-          str->page (fn [n] (block/page-name->map n true))
-          refs (->> page-properties
-                    (filter (fn [[_ v]] (coll? v)))
-                    (vals)
-                    (apply concat)
-                    (set)
-                    (map str->page)
-                    (concat (:block/refs block))
-                    (util/distinct-by :block/name))
-          {:keys [tags alias]} page-properties
-          page-tx (let [id (:db/id (:block/page block))
-                        retract-attributes (when id
-                                             (mapv (fn [attribute]
-                                                     [:db/retract id attribute])
-                                                   [:block/properties :block/tags :block/alias]))
-                        tags (->> (map str->page tags) (remove nil?))
-                        alias (->> (map str->page alias) (remove nil?))
-                        tx (cond-> {:db/id id
-                                    :block/properties page-properties}
-                             (seq tags)
-                             (assoc :block/tags tags)
-                             (seq alias)
-                             (assoc :block/alias alias))]
-                    (conj retract-attributes tx))]
-      (assoc block
-             :block/refs refs
-             :db/other-tx page-tx))
-    block))
-
 (defn- remove-non-existed-refs!
 (defn- remove-non-existed-refs!
   [refs]
   [refs]
   (remove (fn [x] (or
   (remove (fn [x] (or
@@ -386,7 +352,6 @@
                 block
                 block
                 (dissoc block :block/pre-block?))
                 (dissoc block :block/pre-block?))
         block (update block :block/refs remove-non-existed-refs!)
         block (update block :block/refs remove-non-existed-refs!)
-        block (attach-page-properties-if-exists! block)
         new-properties (merge
         new-properties (merge
                         (select-keys properties (property/hidden-properties))
                         (select-keys properties (property/hidden-properties))
                         (:block/properties block))]
                         (:block/properties block))]
@@ -757,7 +722,8 @@
                    (remove nil?))]
                    (remove nil?))]
       (doseq [id ids]
       (doseq [id ids]
         (let [block (db/pull [:block/uuid id])]
         (let [block (db/pull [:block/uuid id])]
-          (set-marker block))))))
+          (when (not-empty (:block/content block))
+            (set-marker block)))))))
 
 
 (defn cycle-todo!
 (defn cycle-todo!
   []
   []
@@ -2711,7 +2677,7 @@
                        (surround-by? input "#" :end)
                        (surround-by? input "#" :end)
                        (= key "#"))]
                        (= key "#"))]
       (cond
       (cond
-        (and (contains? #{"ArrowLeft" "ArrowRight" "ArrowUp" "ArrowDown"} key)
+        (and (contains? #{"ArrowLeft" "ArrowRight"} key)
              (contains? #{:property-search :property-value-search} (state/get-editor-action)))
              (contains? #{:property-search :property-value-search} (state/get-editor-action)))
         (state/clear-editor-action!)
         (state/clear-editor-action!)
 
 
@@ -2956,8 +2922,9 @@
 
 
 (defn- cut-blocks-and-clear-selections!
 (defn- cut-blocks-and-clear-selections!
   [copy?]
   [copy?]
-  (cut-selection-blocks copy?)
-  (clear-selection!))
+  (when-not (get-in @state/state [:ui/find-in-page :active?])
+    (cut-selection-blocks copy?)
+    (clear-selection!)))
 
 
 (defn shortcut-copy-selection
 (defn shortcut-copy-selection
   [_e]
   [_e]

+ 1 - 0
src/main/frontend/handler/events.cljs

@@ -59,6 +59,7 @@
   (db/set-key-value repo :ast/version db-schema/ast-version)
   (db/set-key-value repo :ast/version db-schema/ast-version)
   (search-handler/rebuild-indices!)
   (search-handler/rebuild-indices!)
   (db/persist! repo)
   (db/persist! repo)
+  (plugin-handler/hook-plugin-app :graph-after-indexed {:repo repo :empty-graph? empty-graph?})
   (when (state/setups-picker?)
   (when (state/setups-picker?)
     (if empty-graph?
     (if empty-graph?
       (route-handler/redirect! {:to :import :query-params {:from "picker"}})
       (route-handler/redirect! {:to :import :query-params {:from "picker"}})

+ 8 - 5
src/main/frontend/handler/page.cljs

@@ -373,9 +373,9 @@
   ;; update all pages which have references to this page
   ;; update all pages which have references to this page
   (let [repo (state/get-current-repo)
   (let [repo (state/get-current-repo)
         to-page (db/entity [:block/name (util/page-name-sanity-lc new-name)])
         to-page (db/entity [:block/name (util/page-name-sanity-lc new-name)])
-        blocks   (db/get-page-referenced-blocks-no-cache (:db/id page))
-        page-ids (->> (map :block/page blocks)
-                      (remove nil?)
+        blocks (:block/_refs (db/entity (:db/id page)))
+        page-ids (->> (map (fn [b]
+                             {:db/id (:db/id (:block/page b))}) blocks)
                       (set))
                       (set))
         tx       (->> (map (fn [{:block/keys [uuid content properties] :as block}]
         tx       (->> (map (fn [{:block/keys [uuid content properties] :as block}]
                              (let [content    (let [content' (replace-old-page! content old-original-name new-name)]
                              (let [content    (let [content' (replace-old-page! content old-original-name new-name)]
@@ -389,8 +389,11 @@
                                   {:block/uuid       uuid
                                   {:block/uuid       uuid
                                    :block/content    content
                                    :block/content    content
                                    :block/properties properties
                                    :block/properties properties
-                                   :block/properties-order (map first properties)
-                                   :block/refs (rename-update-block-refs! (:block/refs block) (:db/id page) (:db/id to-page))})))) blocks)
+                                   :block/properties-order (when (seq properties)
+                                                             (map first properties))
+                                   :block/refs (->> (rename-update-block-refs! (:block/refs block) (:db/id page) (:db/id to-page))
+                                                    (map :db/id)
+                                                    (set))})))) blocks)
                       (remove nil?))]
                       (remove nil?))]
     (db/transact! repo tx)
     (db/transact! repo tx)
     (doseq [page-id page-ids]
     (doseq [page-id page-ids]

+ 54 - 1
src/main/frontend/handler/search.cljs

@@ -9,7 +9,10 @@
             [promesa.core :as p]
             [promesa.core :as p]
             [logseq.graph-parser.text :as text]
             [logseq.graph-parser.text :as text]
             [frontend.util.drawer :as drawer]
             [frontend.util.drawer :as drawer]
-            [frontend.util.property :as property]))
+            [frontend.util.property :as property]
+            [electron.ipc :as ipc]
+            [goog.functions :refer [debounce]]
+            [dommy.core :as dom]))
 
 
 (defn add-search-to-recent!
 (defn add-search-to-recent!
   [repo q]
   [repo q]
@@ -51,6 +54,56 @@
            (swap! state/state assoc search-key result)
            (swap! state/state assoc search-key result)
            result))))))
            result))))))
 
 
+(defn open-find-in-page!
+  []
+  (when (util/electron?)
+    (let [{:keys [active?]} (:ui/find-in-page @state/state)]
+      (when-not active? (state/set-state! [:ui/find-in-page :active?] true)))))
+
+(defn electron-find-in-page!
+  []
+  (when (util/electron?)
+    (let [{:keys [active? backward? match-case? q]} (:ui/find-in-page @state/state)
+          option (cond->
+                  {}
+
+                   (not active?)
+                   (assoc :findNext true)
+
+                   backward?
+                   (assoc :forward false)
+
+                   match-case?
+                   (assoc :matchCase true))]
+      (open-find-in-page!)
+      (when-not (string/blank? q)
+        (dom/set-style! (dom/by-id "search-in-page-input")
+                        :visibility "hidden")
+        (when (> (count q) 1)
+          (dom/set-html! (dom/by-id "search-in-page-placeholder")
+                         (util/format
+                          "<span><span>%s</span><span style=\"margin-left: -4px;\">%s</span></span>"
+                          (first q)
+                          (str " " (subs q 1)))))
+        (ipc/ipc "find-in-page" q option)))))
+
+(defonce debounced-search (debounce electron-find-in-page! 500))
+
+(defn loop-find-in-page!
+  [backward?]
+  (when (and (get-in @state/state [:ui/find-in-page :active?])
+             (not (state/editing?)))
+    (state/set-state! [:ui/find-in-page :backward?] backward?)
+    (debounced-search)))
+
+(defn electron-exit-find-in-page!
+  [& {:keys [clear-state?]
+      :or {clear-state? true}}]
+  (when (util/electron?)
+    (ipc/ipc "clear-find-in-page")
+    (when clear-state?
+      (state/set-state! :ui/find-in-page nil))))
+
 (defn clear-search!
 (defn clear-search!
   ([]
   ([]
    (clear-search! true))
    (clear-search! true))

+ 27 - 7
src/main/frontend/modules/outliner/core.cljs

@@ -158,12 +158,24 @@
                                       db-schema/retract-attributes)))))
                                       db-schema/retract-attributes)))))
 
 
         (when-let [e (:block/page block-entity)]
         (when-let [e (:block/page block-entity)]
-          (let [m {:db/id (:db/id e)
+          (let [m' {:db/id (:db/id e)
                    :block/updated-at (util/time-ms)}
                    :block/updated-at (util/time-ms)}
-                m (if (:block/created-at e)
-                    m
-                    (assoc m :block/created-at (util/time-ms)))]
-            (swap! txs-state conj m))
+                m' (if (:block/created-at e)
+                    m'
+                    (assoc m' :block/created-at (util/time-ms)))
+                m' (if (or (:block/pre-block? block-entity)
+                           (:block/pre-block? m))
+                     (let [properties (:block/properties m)
+                           alias (set (:alias properties))
+                           tags (set (:tags properties))
+                           alias (map (fn [p] {:block/name (util/page-name-sanity-lc p)}) alias)
+                           tags (map (fn [p] {:block/name (util/page-name-sanity-lc p)}) tags)]
+                       (assoc m'
+                              :block/alias alias
+                              :block/tags tags
+                              :block/properties properties))
+                     m')]
+            (swap! txs-state conj m'))
           (remove-orphaned-page-refs! (:db/id block-entity) txs-state old-refs new-refs)))
           (remove-orphaned-page-refs! (:db/id block-entity) txs-state old-refs new-refs)))
 
 
       (swap! txs-state conj (dissoc m :db/other-tx))
       (swap! txs-state conj (dissoc m :db/other-tx))
@@ -194,8 +206,16 @@
                                                  (assoc :block/left parent))))
                                                  (assoc :block/left parent))))
                                            immediate-children)))
                                            immediate-children)))
                     txs))
                     txs))
-                txs)]
-      (swap! txs-state concat txs)
+                txs)
+          page-tx (let [block (db/entity [:block/uuid block-id])]
+                    (when (:block/pre-block? block)
+                      (let [id (:db/id (:block/page block))]
+                        [[:db/retract id :block/properties]
+                         [:db/retract id :block/properties-order]
+                         [:db/retract id :block/alias]
+                         [:db/retract id :block/tags]])))]
+      (swap! txs-state concat txs page-tx)
+      (util/pprint @txs-state)
       block-id))
       block-id))
 
 
   (-get-children [this]
   (-get-children [this]

+ 10 - 11
src/main/frontend/modules/outliner/pipeline.cljs

@@ -21,12 +21,9 @@
 ;; 1. For each changed block, new-refs = its page + :block/refs + parents :block/refs
 ;; 1. For each changed block, new-refs = its page + :block/refs + parents :block/refs
 ;; 2. Its children' block/path-refs might need to be updated too.
 ;; 2. Its children' block/path-refs might need to be updated too.
 (defn compute-block-path-refs
 (defn compute-block-path-refs
-  [tx-meta blocks]
+  [{:keys [tx-meta]} blocks]
   (let [repo (state/get-current-repo)
   (let [repo (state/get-current-repo)
-        blocks (remove :block/name blocks)
-        blocks (if (= (:outliner-op tx-meta) :insert-blocks)
-                 (butlast blocks)
-                 blocks)]
+        blocks (remove :block/name blocks)]
     (when (:outliner-op tx-meta)
     (when (:outliner-op tx-meta)
       (when (react/path-refs-need-recalculated? tx-meta)
       (when (react/path-refs-need-recalculated? tx-meta)
         (let [*computed-ids (atom #{})]
         (let [*computed-ids (atom #{})]
@@ -44,13 +41,15 @@
                             refs-changed? (not= old-refs new-refs)
                             refs-changed? (not= old-refs new-refs)
                             children (db-model/get-block-children-ids repo (:block/uuid block))
                             children (db-model/get-block-children-ids repo (:block/uuid block))
                             children-refs (map (fn [id]
                             children-refs (map (fn [id]
-                                                 {:db/id (:db/id (db/entity [:block/uuid id]))
-                                                  :block/path-refs (concat
-                                                                    (map :db/id (:block/path-refs (db/entity id)))
-                                                                    new-refs)}) children)]
+                                                 (let [entity (db/entity [:block/uuid id])]
+                                                   {:db/id (:db/id entity)
+                                                    :block/path-refs (concat
+                                                                      (map :db/id (:block/path-refs entity))
+                                                                      new-refs)})) children)]
                         (swap! *computed-ids set/union (set (cons (:block/uuid block) children)))
                         (swap! *computed-ids set/union (set (cons (:block/uuid block) children)))
                         (util/concat-without-nil
                         (util/concat-without-nil
-                         [(when (and refs-changed? (seq new-refs))
+                         [(when (and (seq new-refs)
+                                     refs-changed?)
                             {:db/id (:db/id block)
                             {:db/id (:db/id block)
                              :block/path-refs new-refs})]
                              :block/path-refs new-refs})]
                          children-refs))))
                          children-refs))))
@@ -66,7 +65,7 @@
             repo (state/get-current-repo)
             repo (state/get-current-repo)
             refs-tx (util/profile
             refs-tx (util/profile
                      "Compute path refs: "
                      "Compute path refs: "
-                     (set (compute-block-path-refs (:tx-meta tx-report) blocks)))
+                     (set (compute-block-path-refs tx-report blocks)))
             truncate-refs-tx (map (fn [m] [:db/retract (:db/id m) :block/path-refs]) refs-tx)
             truncate-refs-tx (map (fn [m] [:db/retract (:db/id m) :block/path-refs]) refs-tx)
             tx (util/concat-without-nil truncate-refs-tx refs-tx)
             tx (util/concat-without-nil truncate-refs-tx refs-tx)
             tx-report' (if (seq tx)
             tx-report' (if (seq tx)

+ 20 - 11
src/main/frontend/modules/outliner/tree.cljs

@@ -84,20 +84,29 @@
       (assoc root' :block/children children)
       (assoc root' :block/children children)
       root')))
       root')))
 
 
+(defn block-entity->map
+  [e]
+  {:db/id (:db/id e)
+   :block/uuid (:block/uuid e)
+   :block/parent {:db/id (:db/id (:block/parent e))}
+   :block/left {:db/id (:db/id (:block/left e))}
+   :block/page (:block/page e)
+   :block/refs (:block/refs e)})
+
+(defn filter-top-level-blocks
+  [blocks]
+  (let [id->blocks (zipmap (map :db/id blocks) blocks)]
+    (filter #(nil?
+              (id->blocks
+               (:db/id (:block/parent (id->blocks (:db/id %)))))) blocks)))
+
 (defn non-consecutive-blocks->vec-tree
 (defn non-consecutive-blocks->vec-tree
   "`blocks` need to be in the same page."
   "`blocks` need to be in the same page."
   [blocks]
   [blocks]
-  (let [blocks (map (fn [e] {:db/id (:db/id e)
-                             :block/uuid (:block/uuid e)
-                             :block/parent {:db/id (:db/id (:block/parent e))}
-                             :block/left {:db/id (:db/id (:block/left e))}
-                             :block/page {:db/id (:db/id (:block/page e))}}) blocks)
-        parent->children (group-by :block/parent blocks)
-        id->blocks (zipmap (map :db/id blocks) blocks)
-        top-level-blocks (filter #(nil?
-                                   (id->blocks
-                                    (:db/id (:block/parent (id->blocks (:db/id %)))))) blocks)
-        top-level-blocks' (model/try-sort-by-left top-level-blocks (:block/parent (first top-level-blocks)))]
+  (let [blocks (map block-entity->map blocks)
+        top-level-blocks (filter-top-level-blocks blocks)
+        top-level-blocks' (model/try-sort-by-left top-level-blocks (:block/parent (first top-level-blocks)))
+        parent->children (group-by :block/parent blocks)]
     (map #(tree parent->children %) top-level-blocks')))
     (map #(tree parent->children %) top-level-blocks')))
 
 
 (defn- sort-blocks-aux
 (defn- sort-blocks-aux

+ 4 - 3
src/main/frontend/modules/shortcut/before.cljs

@@ -33,7 +33,8 @@
 (defn enable-when-not-component-editing!
 (defn enable-when-not-component-editing!
   [f]
   [f]
   (fn [e]
   (fn [e]
-    (when (or (contains? #{:srs} (state/get-modal-id))
-              (not (state/block-component-editing?))
-              (not (whiteboard/tldraw-idle?)))
+    (when (and (or (contains? #{:srs} (state/get-modal-id))
+                   (not (state/block-component-editing?)))
+               (not (and (whiteboard/tldraw-idle?)
+                         (not (state/editing?)))))
       (f e))))
       (f e))))

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

@@ -249,6 +249,20 @@
                                                 (editor-handler/escape-editing)
                                                 (editor-handler/escape-editing)
                                                 (route-handler/go-to-search! :global))}
                                                 (route-handler/go-to-search! :global))}
 
 
+   :go/electron-find-in-page       {:binding "mod+f"
+                                    :fn      #(when (util/electron?)
+                                                (search-handler/open-find-in-page!))}
+
+   :go/electron-jump-to-the-next {:binding ["enter" "mod+g"]
+                                    :fn      (fn [_state _e]
+                                               (when (util/electron?)
+                                                 (search-handler/loop-find-in-page! false)))}
+
+   :go/electron-jump-to-the-previous {:binding ["shift+enter" "mod+shift+g"]
+                                             :fn      (fn [_state _e]
+                                                        (when (util/electron?)
+                                                          (search-handler/loop-find-in-page! true)))}
+
    :go/journals                    {:binding "g j"
    :go/journals                    {:binding "g j"
                                     :fn      route-handler/go-to-journals!}
                                     :fn      route-handler/go-to-journals!}
 
 
@@ -280,7 +294,7 @@
    :graph/open                     {:fn      #(do
    :graph/open                     {:fn      #(do
                                                 (editor-handler/escape-editing)
                                                 (editor-handler/escape-editing)
                                                 (state/set-state! :ui/open-select :graph-open))
                                                 (state/set-state! :ui/open-select :graph-open))
-                                    :binding "mod+shift+g"}
+                                    :binding "alt+shift+g"}
 
 
    :graph/remove                   {:fn      #(do
    :graph/remove                   {:fn      #(do
                                                 (editor-handler/escape-editing)
                                                 (editor-handler/escape-editing)
@@ -501,6 +515,9 @@
                           :ui/toggle-brackets
                           :ui/toggle-brackets
                           :go/search-in-page
                           :go/search-in-page
                           :go/search
                           :go/search
+                          :go/electron-find-in-page
+                          :go/electron-jump-to-the-next
+                          :go/electron-jump-to-the-previous
                           :go/backward
                           :go/backward
                           :go/forward
                           :go/forward
                           :search/re-index
                           :search/re-index
@@ -553,6 +570,9 @@
     :editor/select-all-blocks
     :editor/select-all-blocks
     :go/search
     :go/search
     :go/search-in-page
     :go/search-in-page
+    :go/electron-find-in-page
+    :go/electron-jump-to-the-next
+    :go/electron-jump-to-the-previous
     :editor/undo
     :editor/undo
     :editor/redo
     :editor/redo
     :editor/copy
     :editor/copy

+ 10 - 8
src/main/frontend/modules/shortcut/data_helper.cljs

@@ -46,14 +46,16 @@
 
 
 (defn normalize-user-keyname
 (defn normalize-user-keyname
   [k]
   [k]
-  (some-> k
-          (util/safe-lower-case)
-          (str/replace #";+" "semicolon")
-          (str/replace #"=+" "equals")
-          (str/replace #"~+" "dash")
-          (str/replace "[" "open-square-bracket")
-          (str/replace "]" "close-square-bracket")
-          (str/replace "'" "single-quote")))
+  (let [keynames {";" "semicolon"
+                   "=" "equals"
+                   "-" "dash"
+                   "[" "open-square-bracket"
+                   "]" "close-square-bracket"
+                   "'" "single-quote"}]
+    (some-> k
+            (util/safe-lower-case)
+            (str/replace #"[;=-\[\]']" (fn [s]
+                                         (get keynames s))))))
 
 
 ;; returns a vector to preserve order
 ;; returns a vector to preserve order
 (defn binding-by-category [name]
 (defn binding-by-category [name]

+ 3 - 0
src/main/frontend/modules/shortcut/dicts.cljc

@@ -77,6 +77,9 @@
    :editor/zoom-out                "Zoom out editing block / Backwards otherwise"
    :editor/zoom-out                "Zoom out editing block / Backwards otherwise"
    :ui/toggle-brackets             "Toggle whether to display brackets"
    :ui/toggle-brackets             "Toggle whether to display brackets"
    :go/search-in-page              "Search in the current page"
    :go/search-in-page              "Search in the current page"
+   :go/electron-find-in-page       "Find in page"
+   :go/electron-jump-to-the-next   "Jump to the next match to your Find bar search"
+   :go/electron-jump-to-the-previous "Jump to the previous match to your Find bar search"
    :go/search                      "Full text search"
    :go/search                      "Full text search"
    :go/journals                    "Go to journals"
    :go/journals                    "Go to journals"
    :go/backward                    "Backwards"
    :go/backward                    "Backwards"

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

@@ -155,7 +155,7 @@
 
 
 (defn template-search
 (defn template-search
   ([q]
   ([q]
-   (template-search q 10))
+   (template-search q 100))
   ([q limit]
   ([q limit]
    (when q
    (when q
      (let [q (clean-str q)
      (let [q (clean-str q)
@@ -166,7 +166,7 @@
 
 
 (defn property-search
 (defn property-search
   ([q]
   ([q]
-   (property-search q 10))
+   (property-search q 100))
   ([q limit]
   ([q limit]
    (when q
    (when q
      (let [q (clean-str q)
      (let [q (clean-str q)
@@ -181,7 +181,7 @@
 
 
 (defn property-value-search
 (defn property-value-search
   ([property q]
   ([property q]
-   (property-value-search property q 10))
+   (property-value-search property q 100))
   ([property q limit]
   ([property q limit]
    (when q
    (when q
      (let [q (clean-str q)
      (let [q (clean-str q)

+ 12 - 6
src/main/frontend/state.cljs

@@ -133,6 +133,7 @@
      :selection/direction                   :down
      :selection/direction                   :down
      :custom-context-menu/show?             false
      :custom-context-menu/show?             false
      :custom-context-menu/links             nil
      :custom-context-menu/links             nil
+     :custom-context-menu/position          nil
 
 
      ;; pages or blocks in the right sidebar
      ;; pages or blocks in the right sidebar
      ;; It is a list of `[repo db-id block-type block-data]` 4-tuple
      ;; It is a list of `[repo db-id block-type block-data]` 4-tuple
@@ -231,6 +232,7 @@
 
 
      :encryption/graph-parsing?             false
      :encryption/graph-parsing?             false
 
 
+     :ui/find-in-page                     nil
      })))
      })))
 
 
 ;; block uuid -> {content(String) -> ast}
 ;; block uuid -> {content(String) -> ast}
@@ -369,9 +371,11 @@
                  (get (sub-config) (get-current-repo))))))
                  (get (sub-config) (get-current-repo))))))
 
 
 (defn enable-journals?
 (defn enable-journals?
-  [repo]
-  (not (false? (:feature/enable-journals?
-                (get (sub-config) repo)))))
+  ([]
+   (enable-journals? (get-current-repo)))
+  ([repo]
+   (not (false? (:feature/enable-journals?
+                 (get (sub-config) repo))))))
 
 
 (defn enable-flashcards?
 (defn enable-flashcards?
   ([]
   ([]
@@ -771,16 +775,18 @@
   (:selection/direction @state))
   (:selection/direction @state))
 
 
 (defn show-custom-context-menu!
 (defn show-custom-context-menu!
-  [links]
+  [links position]
   (swap! state assoc
   (swap! state assoc
          :custom-context-menu/show? true
          :custom-context-menu/show? true
-         :custom-context-menu/links links))
+         :custom-context-menu/links links
+         :custom-context-menu/position position))
 
 
 (defn hide-custom-context-menu!
 (defn hide-custom-context-menu!
   []
   []
   (swap! state assoc
   (swap! state assoc
          :custom-context-menu/show? false
          :custom-context-menu/show? false
-         :custom-context-menu/links nil))
+         :custom-context-menu/links nil
+         :custom-context-menu/position nil))
 
 
 (defn toggle-navigation-item-collapsed!
 (defn toggle-navigation-item-collapsed!
   [item]
   [item]

+ 41 - 40
src/main/frontend/ui.cljs

@@ -84,7 +84,7 @@
 
 
 (rum/defc dropdown-content-wrapper [state content class]
 (rum/defc dropdown-content-wrapper [state content class]
   (let [class (or class
   (let [class (or class
-                  (util/hiccup->class "origin-top-right.absolute.right-0.mt-2.rounded-md.shadow-lg"))]
+                  (util/hiccup->class "origin-top-right.absolute.right-0.mt-2"))]
     [:div.dropdown-wrapper
     [:div.dropdown-wrapper
      {:class (str class " "
      {:class (str class " "
                   (case state
                   (case state
@@ -109,18 +109,38 @@
         (when @open?
         (when @open?
           (dropdown-content-wrapper dropdown-state modal-content modal-class))))]))
           (dropdown-content-wrapper dropdown-state modal-content modal-class))))]))
 
 
+;; `sequence` can be a list of symbols, a list of strings, or a string
+(defn render-keyboard-shortcut [sequence]
+  (let [sequence (if (string? sequence)
+                   (-> sequence ;; turn string into sequence
+                       (string/trim)
+                       (string/lower-case)
+                       (string/split  #" |\+"))
+                   sequence)]
+    [:span.keyboard-shortcut
+     (map-indexed (fn [i key]
+                    [:code {:key i}
+                   ;; Display "cmd" rather than "meta" to the user to describe the Mac
+                   ;; mod key, because that's what the Mac keyboards actually say.
+                     (if (or (= :meta key) (= "meta" key))
+                       (util/meta-key-name)
+                       (name key))])
+                  sequence)]))
+
 (rum/defc menu-link
 (rum/defc menu-link
-  [options child]
-  [:a.block.px-4.py-2.text-sm.transition.ease-in-out.duration-150.cursor.menu-link
+  [options child shortcut]
+  [:a.flex.justify-between.px-4.py-2.text-sm.transition.ease-in-out.duration-150.cursor.menu-link
    options
    options
-   child])
+   [:span child]
+   (when shortcut
+     [:span.ml-1 (render-keyboard-shortcut shortcut)])])
 
 
 (rum/defc dropdown-with-links
 (rum/defc dropdown-with-links
   [content-fn links {:keys [links-header links-footer] :as opts}]
   [content-fn links {:keys [links-header links-footer] :as opts}]
   (dropdown
   (dropdown
    content-fn
    content-fn
    (fn [{:keys [close-fn]}]
    (fn [{:keys [close-fn]}]
-     [:div.py-1.rounded-md.shadow-xs
+     [:.menu-links-wrapper
       (when links-header links-header)
       (when links-header links-header)
       (for [{:keys [options title icon hr hover-detail item]} (if (fn? links) (links) links)]
       (for [{:keys [options title icon hr hover-detail item]} (if (fn? links) (links) links)]
         (let [new-options
         (let [new-options
@@ -138,16 +158,16 @@
                            [:div {:style {:margin-right "8px"
                            [:div {:style {:margin-right "8px"
                                           :margin-left  "4px"}} title]]))]
                                           :margin-left  "4px"}} title]]))]
           (if hr
           (if hr
-            [:hr.my-1 {:key "dropdown-hr"}]
+            [:hr.menu-separator {:key "dropdown-hr"}]
             (rum/with-key
             (rum/with-key
-              (menu-link new-options child)
+              (menu-link new-options child nil)
               title))))
               title))))
       (when links-footer links-footer)])
       (when links-footer links-footer)])
    opts))
    opts))
 
 
 (defn button
 (defn button
-  [text & {:keys [background href class intent on-click small? large?]
-           :or {small? false large? false}
+  [text & {:keys [background href class intent on-click small? large? title]
+           :or   {small? false large? false}
            :as   option}]
            :as   option}]
   (let [klass (when-not intent ".bg-indigo-600.hover:bg-indigo-700.focus:border-indigo-700.active:bg-indigo-700.text-center")
   (let [klass (when-not intent ".bg-indigo-600.hover:bg-indigo-700.focus:border-indigo-700.active:bg-indigo-700.text-center")
         klass (if background (string/replace klass "indigo" background) klass)
         klass (if background (string/replace klass "indigo" background) klass)
@@ -156,6 +176,7 @@
     [:button.ui__button
     [:button.ui__button
      (merge
      (merge
       {:type  "button"
       {:type  "button"
+       :title title
        :class (str (util/hiccup->class klass) " " class)}
        :class (str (util/hiccup->class klass) " " class)}
       (dissoc option :background :class :small? :large?)
       (dissoc option :background :class :small? :large?)
       (when href
       (when href
@@ -254,6 +275,11 @@
   []
   []
   (gdom/getElement "main-content-container"))
   (gdom/getElement "main-content-container"))
 
 
+(defn focus-element
+  [element]
+  (when-let [element ^js (gdom/getElement element)]
+    (.focus element)))
+
 (defn get-scroll-top []
 (defn get-scroll-top []
   (.-scrollTop (main-node)))
   (.-scrollTop (main-node)))
 
 
@@ -418,7 +444,7 @@
                                        (if (and (gobj/get e "shiftKey") on-shift-chosen)
                                        (if (and (gobj/get e "shiftKey") on-shift-chosen)
                                          (on-shift-chosen item)
                                          (on-shift-chosen item)
                                          (on-chosen item)))}
                                          (on-chosen item)))}
-                     (if item-render (item-render item chosen?) item)))]]
+                     (if item-render (item-render item chosen?) item) nil))]]
 
 
              (if get-group-name
              (if get-group-name
                (if-let [group-name (get-group-name item)]
                (if-let [group-name (get-group-name item)]
@@ -445,24 +471,6 @@
       {:class       (if on? (if small? "translate-x-4" "translate-x-5") "translate-x-0")
       {:class       (if on? (if small? "translate-x-4" "translate-x-5") "translate-x-0")
        :aria-hidden "true"}]]]))
        :aria-hidden "true"}]]]))
 
 
-;; `sequence` can be a list of symbols, a list of strings, or a string
-(defn render-keyboard-shortcut [sequence]
-  (let [sequence (if (string? sequence)
-                   (-> sequence ;; turn string into sequence
-                       (string/trim)
-                       (string/lower-case)
-                       (string/split  #" |\+"))
-                   sequence)]
-    [:span.keyboard-shortcut
-     (map-indexed (fn [i key]
-                    [:code {:key i}
-                   ;; Display "cmd" rather than "meta" to the user to describe the Mac
-                   ;; mod key, because that's what the Mac keyboards actually say.
-                     (if (or (= :meta key) (= "meta" key))
-                       (util/meta-key-name)
-                       (name key))])
-                  sequence)]))
-
 (defn keyboard-shortcut-from-config [shortcut-name]
 (defn keyboard-shortcut-from-config [shortcut-name]
   (let [default-binding (:binding (get shortcut-config/all-default-keyboard-shortcuts shortcut-name))
   (let [default-binding (:binding (get shortcut-config/all-default-keyboard-shortcuts shortcut-name))
         custom-binding  (when (state/shortcuts) (get (state/shortcuts) shortcut-name))
         custom-binding  (when (state/shortcuts) (get (state/shortcuts) shortcut-name))
@@ -947,24 +955,17 @@
   ([content-fn]
   ([content-fn]
    (lazy-visible content-fn nil))
    (lazy-visible content-fn nil))
   ([content-fn {:keys [trigger-once? _debug-id]
   ([content-fn {:keys [trigger-once? _debug-id]
-                :or {trigger-once? false}}]
+                :or {trigger-once? true}}]
    (if (or (util/mobile?) (mobile-util/native-platform?))
    (if (or (util/mobile?) (mobile-util/native-platform?))
      (content-fn)
      (content-fn)
      (let [[visible? set-visible!] (rum/use-state false)
      (let [[visible? set-visible!] (rum/use-state false)
-           [last-changed-time set-last-changed-time!] (rum/use-state nil)
            inViewState (useInView #js {:rootMargin "100px"
            inViewState (useInView #js {:rootMargin "100px"
                                        :triggerOnce trigger-once?
                                        :triggerOnce trigger-once?
                                        :onChange (fn [in-view? entry]
                                        :onChange (fn [in-view? entry]
-                                                   (let [self-top (.-top (.-boundingClientRect entry))
-                                                         time' (util/time-ms)]
-                                                     (when (and
-                                                            (or (and (not visible?) in-view?)
-                                                                ;; hide only the components below the current top for better ux
-                                                                (and visible? (not in-view?) (> self-top 0)))
-                                                            (or (nil? last-changed-time)
-                                                                (and (some? last-changed-time)
-                                                                     (> (- time' last-changed-time) 50))))
-                                                       (set-last-changed-time! time')
+                                                   (let [self-top (.-top (.-boundingClientRect entry))]
+                                                     (when (or (and (not visible?) in-view?)
+                                                               ;; hide only the components below the current top for better ux
+                                                               (and visible? (not in-view?) (> self-top 0)))
                                                        (set-visible! in-view?))))})
                                                        (set-visible! in-view?))))})
            ref (.-ref inViewState)]
            ref (.-ref inViewState)]
        (lazy-visible-inner visible? content-fn ref)))))
        (lazy-visible-inner visible? content-fn ref)))))

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

@@ -270,11 +270,6 @@ html.is-mobile {
   }
   }
 }
 }
 
 
-.dropdown-wrapper {
-  background-color: var(--ls-primary-background-color, #fff);
-  min-width: 12rem;
-}
-
 .dropdown-caret {
 .dropdown-caret {
   display: inline-block;
   display: inline-block;
   width: 0;
   width: 0;

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

@@ -79,8 +79,7 @@
    (defn electron?
    (defn electron?
      []
      []
      (when (and js/window (gobj/get js/window "navigator"))
      (when (and js/window (gobj/get js/window "navigator"))
-       (let [ua (string/lower-case js/navigator.userAgent)]
-         (string/includes? ua " electron")))))
+       (gstring/caseInsensitiveContains js/navigator.userAgent " electron"))))
 
 
 #?(:cljs
 #?(:cljs
    (defn mocked-open-dir-path
    (defn mocked-open-dir-path
@@ -338,10 +337,6 @@
    (defn stop-propagation [e]
    (defn stop-propagation [e]
      (when e (.stopPropagation e))))
      (when e (.stopPropagation e))))
 
 
-#?(:cljs
-   (defn cur-doc-top []
-     (.. js/document -documentElement -scrollTop)))
-
 #?(:cljs
 #?(:cljs
    (defn element-top [elem top]
    (defn element-top [elem top]
      (when elem
      (when elem
@@ -490,7 +485,10 @@
 
 
 #?(:cljs
 #?(:cljs
    (defn safe-path-join [prefix & paths]
    (defn safe-path-join [prefix & paths]
-     (apply node-path.join (cons prefix paths))))
+     (let [path (apply node-path.join (cons prefix paths))]
+       (if (and (electron?) (gstring/caseInsensitiveStartsWith path "file://"))
+         (js/decodeURIComponent (subs path 7))
+         path))))
 
 
 (defn trim-safe
 (defn trim-safe
   [s]
   [s]

+ 8 - 6
src/main/frontend/utils.js

@@ -315,12 +315,14 @@ export const nodePath = Object.assign({}, path, {
   join (input, ...paths) {
   join (input, ...paths) {
     let orURI = null
     let orURI = null
 
 
-    try {
-      orURI = new URL(input)
-      input = input.replace(orURI.protocol + '//', '')
-        .replace(orURI.protocol, '')
-        .replace(/^\/+/, '/')
-    } catch (_e) {}
+    if (input.startsWith("file://")) {
+      try {
+        orURI = new URL(input)
+        input = input.replace(orURI.protocol + '//', '')
+          .replace(orURI.protocol, '')
+          .replace(/^\/+/, '/')
+      } catch (_e) {}
+    }
 
 
     input = path.join(input, ...paths)
     input = path.join(input, ...paths)
 
 

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

@@ -1,3 +1,3 @@
 (ns frontend.version)
 (ns frontend.version)
 
 
-(defonce version "0.8.1")
+(defonce version "0.8.2")

+ 27 - 6
src/main/logseq/api.cljs

@@ -102,8 +102,16 @@
          :preferred-start-of-week (state/get-start-of-week)
          :preferred-start-of-week (state/get-start-of-week)
          :current-graph         (state/get-current-repo)
          :current-graph         (state/get-current-repo)
          :show-brackets         (state/show-brackets?)
          :show-brackets         (state/show-brackets?)
+         :enabled-journals      (state/enable-journals?)
+         :enabled-flashcards    (state/enable-flashcards?)
          :me                    (state/get-me)}))))
          :me                    (state/get-me)}))))
 
 
+(def ^:export get_current_graph_configs
+  (fn []
+    (some-> (get (:config @state/state) (state/get-current-repo))
+            (normalize-keyword-for-json)
+            (bean/->js))))
+
 (def ^:export get_current_graph
 (def ^:export get_current_graph
   (fn []
   (fn []
     (when-let [repo (state/get-current-repo)]
     (when-let [repo (state/get-current-repo)]
@@ -442,6 +450,9 @@
   [block-uuid]
   [block-uuid]
   (editor-handler/open-block-in-sidebar! (uuid block-uuid)))
   (editor-handler/open-block-in-sidebar! (uuid block-uuid)))
 
 
+(defn ^:export new_block_uuid []
+  (str (db/new-block-id)))
+
 (def ^:export select_block
 (def ^:export select_block
   (fn [block-uuid]
   (fn [block-uuid]
     (when-let [block (db-model/get-block-by-uuid block-uuid)]
     (when-let [block (db-model/get-block-by-uuid block-uuid)]
@@ -456,8 +467,16 @@
 
 
 (def ^:export insert_block
 (def ^:export insert_block
   (fn [block-uuid-or-page-name content ^js opts]
   (fn [block-uuid-or-page-name content ^js opts]
-    (let [{:keys [before sibling isPageBlock properties]} (bean/->clj opts)
+    (let [{:keys [before sibling isPageBlock customUUID properties]} (bean/->clj opts)
           page-name (and isPageBlock block-uuid-or-page-name)
           page-name (and isPageBlock block-uuid-or-page-name)
+          custom-uuid (or customUUID (:id properties))
+          _ (when (not (string/blank? custom-uuid))
+              (when-not (util/uuid-string? custom-uuid)
+                (throw (js/Error.
+                        (util/format "Illegal custom block UUID pattern (%s)." custom-uuid))))
+              (when (db-model/query-block-by-uuid custom-uuid)
+                (throw (js/Error.
+                        (util/format "Custom block UUID already exists (%s)." custom-uuid)))))
           block-uuid (if isPageBlock nil (uuid block-uuid-or-page-name))
           block-uuid (if isPageBlock nil (uuid block-uuid-or-page-name))
           block-uuid' (if (and (not sibling) before block-uuid)
           block-uuid' (if (and (not sibling) before block-uuid)
                         (let [block (db/entity [:block/uuid block-uuid])
                         (let [block (db/entity [:block/uuid block-uuid])
@@ -477,11 +496,13 @@
                     before?)
                     before?)
           new-block (editor-handler/api-insert-new-block!
           new-block (editor-handler/api-insert-new-block!
                       content
                       content
-                      {:block-uuid block-uuid'
-                       :sibling?   sibling?
-                       :before?    before?
-                       :page       page-name
-                       :properties properties})]
+                      {:block-uuid  block-uuid'
+                       :sibling?    sibling?
+                       :before?     before?
+                       :page        page-name
+                       :custom-uuid custom-uuid
+                       :properties  (merge properties
+                                           (when custom-uuid {:id custom-uuid}))})]
       (bean/->js (normalize-keyword-for-json new-block)))))
       (bean/->js (normalize-keyword-for-json new-block)))))
 
 
 (def ^:export insert_batch_block
 (def ^:export insert_batch_block

+ 42 - 9
src/test/frontend/db/query_dsl_test.cljs

@@ -3,7 +3,7 @@
             [clojure.string :as str]
             [clojure.string :as str]
             [frontend.db :as db]
             [frontend.db :as db]
             [frontend.db.query-dsl :as query-dsl]
             [frontend.db.query-dsl :as query-dsl]
-            [frontend.test.helper :as test-helper :refer [load-test-files]]))
+            [frontend.test.helper :as test-helper :include-macros true :refer [load-test-files]]))
 
 
 ;; TODO: quickcheck
 ;; TODO: quickcheck
 ;; 1. generate query filters
 ;; 1. generate query filters
@@ -45,7 +45,8 @@
 ;; Tests
 ;; Tests
 ;; =====
 ;; =====
 
 
-(deftest block-property-queries
+(defn- block-property-queries-test
+  []
   (load-test-files [{:file/path "journals/2022_02_28.md"
   (load-test-files [{:file/path "journals/2022_02_28.md"
                      :file/content "a:: b
                      :file/content "a:: b
 - b1
 - b1
@@ -64,7 +65,7 @@ prop-d:: nada"}])
   (testing "Blocks have given property value"
   (testing "Blocks have given property value"
     (is (= #{"b1" "b2"}
     (is (= #{"b1" "b2"}
            (set (map (comp first str/split-lines :block/content)
            (set (map (comp first str/split-lines :block/content)
-                 (dsl-query "(property prop-a val-a)")))))
+                     (dsl-query "(property prop-a val-a)")))))
 
 
     (is (= ["b2"]
     (is (= ["b2"]
            (map (comp first str/split-lines :block/content)
            (map (comp first str/split-lines :block/content)
@@ -112,15 +113,27 @@ prop-d:: nada"}])
               (dsl-query "(property prop-d)")))
               (dsl-query "(property prop-d)")))
       "Blocks that have a property"))
       "Blocks that have a property"))
 
 
-(deftest page-property-queries
+(deftest block-property-queries
+  (testing "block property tests with default config"
+    (test-helper/with-config {}
+      (block-property-queries-test)))
+
+  (test-helper/start-test-db!) ;; reset db
+
+  (testing "block property tests with rich-property-values? config"
+    (test-helper/with-config {:rich-property-values? true}
+      (block-property-queries-test))))
+
+(defn- page-property-queries-test
+  []
   (load-test-files [{:file/path "pages/page1.md"
   (load-test-files [{:file/path "pages/page1.md"
-                     :file/content "parent:: [[child page 1]], [[child-no-space]]"}
+                     :file/content "parent:: [[child page 1]], [[child-no-space]]\ninteresting:: true"}
                     {:file/path "pages/page2.md"
                     {:file/path "pages/page2.md"
-                     :file/content "foo:: bar"}
+                     :file/content "foo:: #bar\ninteresting:: false"}
                     {:file/path "pages/page3.md"
                     {:file/path "pages/page3.md"
-                     :file/content "parent:: [[child page 1]], child page 2\nfoo:: bar"}
+                     :file/content "parent:: [[child page 1]], [[child page 2]]\nfoo:: bar\ninteresting:: false"}
                     {:file/path "pages/page4.md"
                     {:file/path "pages/page4.md"
-                     :file/content "parent:: child page 2\nfoo:: baz"}])
+                     :file/content "parent:: [[child page 2]]\nfoo:: baz"}])
 
 
   (is (= ["page1" "page3" "page4"]
   (is (= ["page1" "page3" "page4"]
          (map :block/name (dsl-query "(page-property parent)")))
          (map :block/name (dsl-query "(page-property parent)")))
@@ -160,7 +173,27 @@ prop-d:: nada"}])
          (map
          (map
           :block/name
           :block/name
           (dsl-query "(and (not (page-property foo bar)) (page-property parent [[child page 2]]))")))
           (dsl-query "(and (not (page-property foo bar)) (page-property parent [[child page 2]]))")))
-      "Page property queries nested NOT in first clause"))
+      "Page property queries nested NOT in first clause")
+
+  (testing "boolean values"
+    (is (= ["page1"]
+           (map :block/name (dsl-query "(page-property interesting true)")))
+        "Boolean true")
+
+    (is (= ["page2" "page3"]
+           (map :block/name (dsl-query "(page-property interesting false)")))
+        "Boolean false")))
+
+(deftest page-property-queries
+  (testing "page property tests with default config"
+    (test-helper/with-config {}
+      (page-property-queries-test)))
+
+  (test-helper/start-test-db!) ;; reset db
+
+  (testing "page property tests with rich-property-values? config"
+    (test-helper/with-config {:rich-property-values? true}
+      (page-property-queries-test))))
 
 
 (deftest task-queries
 (deftest task-queries
   (load-test-files [{:file/path "pages/page1.md"
   (load-test-files [{:file/path "pages/page1.md"

+ 19 - 0
src/test/frontend/test/frontend_node_test_runner.cljs

@@ -0,0 +1,19 @@
+(ns frontend.test.frontend-node-test-runner
+  "This is a custom version of the node-test-runner for the frontend build"
+  {:dev/always true} ;; necessary for test-data freshness
+  (:require [frontend.test.node-test-runner :as node-test-runner]
+            [shadow.test.env :as env]
+            [lambdaisland.glogi.console :as glogi-console]
+            ;; activate humane test output for all tests
+            [pjstadig.humane-test-output]))
+
+;; Needed for new test runners
+(defn ^:dev/after-load reset-test-data! []
+  (-> (env/get-test-data)
+      (env/reset-test-data!)))
+
+(defn main [& args]
+  []
+  (glogi-console/install!) ;; see log messages
+  (reset-test-data!)
+  (node-test-runner/parse-and-run-tests args))

+ 8 - 0
src/test/frontend/test/helper.clj

@@ -0,0 +1,8 @@
+(ns frontend.test.helper)
+
+(defmacro with-config
+  [config & body]
+  `(let [repo# (frontend.state/get-current-repo)]
+     (frontend.state/set-config! repo# ~config)
+     ~@body
+     (frontend.state/set-config! repo# nil)))

+ 16 - 12
src/test/frontend/test/node_test_runner.cljs

@@ -1,6 +1,6 @@
 (ns frontend.test.node-test-runner
 (ns frontend.test.node-test-runner
-  "shadow-cljs test runner for :node-test that provides the same test selection
-  options as
+  "Application agnostic shadow-cljs test runner for :node-test that provides the
+  same test selection options as
   https://github.com/cognitect-labs/test-runner#invoke-with-clojure--m-clojuremain.
   https://github.com/cognitect-labs/test-runner#invoke-with-clojure--m-clojuremain.
   This gives the user a fair amount of control over which tests and namespaces
   This gives the user a fair amount of control over which tests and namespaces
   to call from the commandline. Once this test runner is stable enough we should
   to call from the commandline. Once this test runner is stable enough we should
@@ -12,9 +12,7 @@
             [clojure.set :as set]
             [clojure.set :as set]
             [shadow.test :as st]
             [shadow.test :as st]
             [cljs.test :as ct]
             [cljs.test :as ct]
-            ["util" :as util]
-            ;; activate humane test output for all tests
-            [pjstadig.humane-test-output]))
+            [goog.string :as gstring]))
 
 
 ;; Cljs.test customization
 ;; Cljs.test customization
 ;; Inherit behavior from default reporter
 ;; Inherit behavior from default reporter
@@ -47,10 +45,10 @@
 (defn- print-summary
 (defn- print-summary
   "Print help summary given args and opts strings"
   "Print help summary given args and opts strings"
   [options-summary additional-msg]
   [options-summary additional-msg]
-  (println (util/format "Usage: %s [OPTIONS]\nOptions:\n%s%s"
-                        "$0"
-                        options-summary
-                        additional-msg)))
+  (println (gstring/format "Usage: %s [OPTIONS]\nOptions:\n%s%s"
+                           "$0"
+                           options-summary
+                           additional-msg)))
 
 
 (defn- parse-options
 (defn- parse-options
   "Processes a command's functionality given a cli options definition, arguments
   "Processes a command's functionality given a cli options definition, arguments
@@ -172,9 +170,9 @@ returns selected tests and namespaces to run"
         (st/run-test-vars test-env test-vars))
         (st/run-test-vars test-env test-vars))
       (st/run-all-tests test-env nil))))
       (st/run-all-tests test-env nil))))
 
 
-(defn main [& args]
-  (reset-test-data!)
-
+(defn parse-and-run-tests
+  "Main entry point for custom test runners"
+  [args]
   (let [{:keys [options summary]} (parse-options args cli-options)]
   (let [{:keys [options summary]} (parse-options args cli-options)]
     (if (:help options)
     (if (:help options)
       (do
       (do
@@ -182,3 +180,9 @@ returns selected tests and namespaces to run"
                        "\n\nMultiple options are ANDed. Defaults to running all tests")
                        "\n\nMultiple options are ANDed. Defaults to running all tests")
         (js/process.exit 0))
         (js/process.exit 0))
       (run-tests (keys (env/get-tests)) (env/get-test-vars) options))))
       (run-tests (keys (env/get-tests)) (env/get-test-vars) options))))
+
+(defn main
+  "Main entry point if this ns is configured as a test runner"
+  [& args]
+  (reset-test-data!)
+  (parse-and-run-tests args))

+ 4 - 0
templates/config.edn

@@ -228,6 +228,10 @@
  ;; E.g.:property-pages/excludelist #{:duration :author}
  ;; E.g.:property-pages/excludelist #{:duration :author}
  ;; :property-pages/excludelist
  ;; :property-pages/excludelist
 
 
+ ;; Enables property values to contain a mix of tags, page-refs, special
+ ;; punctuation and free-form text
+ ;; :rich-property-values? true
+
  ;; logbook setup
  ;; logbook setup
  ;; :logbook/settings
  ;; :logbook/settings
  ;; {:with-second-support? false ;limit logbook to minutes, seconds will be eliminated
  ;; {:with-second-support? false ;limit logbook to minutes, seconds will be eliminated

+ 95 - 28
tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx

@@ -1,4 +1,4 @@
-import { isNonNullable, debounce, Decoration, TLLineShapeProps } from '@tldraw/core'
+import { debounce, Decoration, isNonNullable } from '@tldraw/core'
 import { useApp } from '@tldraw/react'
 import { useApp } from '@tldraw/react'
 import { observer } from 'mobx-react-lite'
 import { observer } from 'mobx-react-lite'
 import React from 'react'
 import React from 'react'
@@ -12,7 +12,6 @@ import {
   ToggleGroupMultipleInput,
   ToggleGroupMultipleInput,
 } from '~components/inputs/ToggleGroupInput'
 } from '~components/inputs/ToggleGroupInput'
 import { ToggleInput } from '~components/inputs/ToggleInput'
 import { ToggleInput } from '~components/inputs/ToggleInput'
-import { tint } from 'polished'
 import type {
 import type {
   BoxShape,
   BoxShape,
   EllipseShape,
   EllipseShape,
@@ -30,11 +29,12 @@ import { LogseqContext } from '~lib/logseq-context'
 export const contextBarActionTypes = [
 export const contextBarActionTypes = [
   // Order matters
   // Order matters
   'Edit',
   'Edit',
+  'AutoResizing',
   'Swatch',
   'Swatch',
   'NoFill',
   'NoFill',
-  'ResetBounds',
   'StrokeType',
   'StrokeType',
   'ScaleLevel',
   'ScaleLevel',
+  'TextStyle',
   'YoutubeLink',
   'YoutubeLink',
   'LogseqPortalViewMode',
   'LogseqPortalViewMode',
   'ArrowMode',
   'ArrowMode',
@@ -48,8 +48,8 @@ const contextBarActionMapping = new Map<ContextBarActionType, React.FC>()
 
 
 type ShapeType = Shape['props']['type']
 type ShapeType = Shape['props']['type']
 
 
-const shapeMapping: Partial<Record<ShapeType, ContextBarActionType[]>> = {
-  'logseq-portal': ['Edit', 'LogseqPortalViewMode', 'ScaleLevel', 'OpenPage', 'ResetBounds'],
+export const shapeMapping: Partial<Record<ShapeType, ContextBarActionType[]>> = {
+  'logseq-portal': ['Edit', 'LogseqPortalViewMode', 'ScaleLevel', 'OpenPage', 'AutoResizing'],
   youtube: ['YoutubeLink'],
   youtube: ['YoutubeLink'],
   box: ['Swatch', 'NoFill', 'StrokeType'],
   box: ['Swatch', 'NoFill', 'StrokeType'],
   ellipse: ['Swatch', 'NoFill', 'StrokeType'],
   ellipse: ['Swatch', 'NoFill', 'StrokeType'],
@@ -57,13 +57,13 @@ const shapeMapping: Partial<Record<ShapeType, ContextBarActionType[]>> = {
   line: ['Edit', 'Swatch', 'ArrowMode'],
   line: ['Edit', 'Swatch', 'ArrowMode'],
   pencil: ['Swatch'],
   pencil: ['Swatch'],
   highlighter: ['Swatch'],
   highlighter: ['Swatch'],
-  text: ['Edit', 'Swatch', 'ScaleLevel', 'ResetBounds'],
-  html: ['ScaleLevel', 'ResetBounds'],
+  text: ['Edit', 'Swatch', 'ScaleLevel', 'AutoResizing', 'TextStyle'],
+  html: ['ScaleLevel', 'AutoResizing'],
 }
 }
 
 
-const noStrokeShapes = Object.entries(shapeMapping)
+export const withFillShapes = Object.entries(shapeMapping)
   .filter(([key, types]) => {
   .filter(([key, types]) => {
-    return !types.includes('NoFill') && types.includes('Swatch')
+    return types.includes('NoFill') && types.includes('Swatch')
   })
   })
   .map(([key]) => key) as ShapeType[]
   .map(([key]) => key) as ShapeType[]
 
 
@@ -79,9 +79,9 @@ const EditAction = observer(() => {
     <button
     <button
       className="tl-contextbar-button"
       className="tl-contextbar-button"
       type="button"
       type="button"
+      title="Edit"
       onClick={() => {
       onClick={() => {
         app.api.editShape(shape)
         app.api.editShape(shape)
-        app.api.zoomToSelection()
         if (shape.props.type === 'logseq-portal') {
         if (shape.props.type === 'logseq-portal') {
           let uuid = shape.props.pageId
           let uuid = shape.props.pageId
           if (shape.props.blockType === 'P') {
           if (shape.props.blockType === 'P') {
@@ -99,26 +99,36 @@ const EditAction = observer(() => {
   )
   )
 })
 })
 
 
-const ResetBoundsAction = observer(() => {
+const AutoResizingAction = observer(() => {
   const app = useApp<Shape>()
   const app = useApp<Shape>()
   const shapes = filterShapeByAction<LogseqPortalShape | TextShape | HTMLShape>(
   const shapes = filterShapeByAction<LogseqPortalShape | TextShape | HTMLShape>(
     app.selectedShapesArray,
     app.selectedShapesArray,
-    'ResetBounds'
+    'AutoResizing'
   )
   )
 
 
+  const pressed = shapes.every(s => s.props.isAutoResizing)
+
   return (
   return (
-    <button
+    <ToggleInput
+      title="Auto Resize"
+      toggle={shapes.every(s => s.props.type === 'logseq-portal')}
       className="tl-contextbar-button"
       className="tl-contextbar-button"
-      type="button"
-      onClick={() => {
+      pressed={pressed}
+      onPressedChange={v => {
         shapes.forEach(s => {
         shapes.forEach(s => {
-          s.onResetBounds({ zoom: app.viewport.camera.zoom })
+          if (s.props.type === 'logseq-portal') {
+            s.update({
+              isAutoResizing: v,
+            })
+          } else {
+            s.onResetBounds({ zoom: app.viewport.camera.zoom })
+          }
         })
         })
         app.persist()
         app.persist()
       }}
       }}
     >
     >
       <TablerIcon name="dimensions" />
       <TablerIcon name="dimensions" />
-    </button>
+    </ToggleInput>
   )
   )
 })
 })
 
 
@@ -142,6 +152,7 @@ const LogseqPortalViewModeAction = observer(() => {
   ]
   ]
   return (
   return (
     <ToggleGroupInput
     <ToggleGroupInput
+      title="View Mode"
       options={ViewModeOptions}
       options={ViewModeOptions}
       value={collapsed ? '1' : '0'}
       value={collapsed ? '1' : '0'}
       onValueChange={v => {
       onValueChange={v => {
@@ -186,6 +197,7 @@ const ScaleLevelAction = observer(() => {
   ]
   ]
   return (
   return (
     <SelectInput
     <SelectInput
+      title="Scale Level"
       options={sizeOptions}
       options={sizeOptions}
       value={scaleLevel}
       value={scaleLevel}
       onValueChange={v => {
       onValueChange={v => {
@@ -208,6 +220,7 @@ const OpenPageAction = observer(() => {
   return (
   return (
     <span className="flex gap-1">
     <span className="flex gap-1">
       <button
       <button
+        title="Open Page in Right Sidebar"
         className="tl-contextbar-button"
         className="tl-contextbar-button"
         type="button"
         type="button"
         onClick={() => handlers?.sidebarAddBlock(pageId, blockType === 'B' ? 'block' : 'page')}
         onClick={() => handlers?.sidebarAddBlock(pageId, blockType === 'B' ? 'block' : 'page')}
@@ -215,6 +228,7 @@ const OpenPageAction = observer(() => {
         <TablerIcon name="layout-sidebar-right" />
         <TablerIcon name="layout-sidebar-right" />
       </button>
       </button>
       <button
       <button
+        title="Open Page"
         className="tl-contextbar-button"
         className="tl-contextbar-button"
         type="button"
         type="button"
         onClick={() => handlers?.redirectToPage(pageId)}
         onClick={() => handlers?.redirectToPage(pageId)}
@@ -235,8 +249,14 @@ const YoutubeLinkAction = observer(() => {
 
 
   return (
   return (
     <span className="flex gap-3">
     <span className="flex gap-3">
-      <TextInput className="tl-youtube-link" value={`${shape.props.url}`} onChange={handleChange} />
+      <TextInput
+        title="YouTube Link"
+        className="tl-youtube-link"
+        value={`${shape.props.url}`}
+        onChange={handleChange}
+      />
       <button
       <button
+        title="Open YouTube Link"
         className="tl-contextbar-button"
         className="tl-contextbar-button"
         type="button"
         type="button"
         onClick={() => window.logseq?.api?.open_external_link?.(shape.props.url)}
         onClick={() => window.logseq?.api?.open_external_link?.(shape.props.url)}
@@ -261,7 +281,12 @@ const NoFillAction = observer(() => {
   const noFill = shapes.every(s => s.props.noFill)
   const noFill = shapes.every(s => s.props.noFill)
 
 
   return (
   return (
-    <ToggleInput className="tl-contextbar-button" pressed={noFill} onPressedChange={handleChange}>
+    <ToggleInput
+      title="Fill Toggle"
+      className="tl-contextbar-button"
+      pressed={noFill}
+      onPressedChange={handleChange}
+    >
       {noFill ? <TablerIcon name="eye-off" /> : <TablerIcon name="eye" />}
       {noFill ? <TablerIcon name="eye-off" /> : <TablerIcon name="eye" />}
     </ToggleInput>
     </ToggleInput>
   )
   )
@@ -276,14 +301,8 @@ const SwatchAction = observer(() => {
   const handleChange = React.useMemo(() => {
   const handleChange = React.useMemo(() => {
     let latestValue = ''
     let latestValue = ''
     const handler: React.ChangeEventHandler<HTMLInputElement> = e => {
     const handler: React.ChangeEventHandler<HTMLInputElement> = e => {
-      const strokeColor = tint(0.4, latestValue)
       shapes.forEach(s => {
       shapes.forEach(s => {
-        const strokeOnly = noStrokeShapes.includes(s.props.type)
-        s.update(
-          strokeOnly
-            ? { stroke: latestValue, fill: latestValue }
-            : { fill: latestValue, stroke: strokeColor }
-        )
+        s.update({ fill: latestValue, stroke: latestValue })
       })
       })
       app.persist(true)
       app.persist(true)
     }
     }
@@ -293,7 +312,7 @@ const SwatchAction = observer(() => {
   }, [])
   }, [])
 
 
   const value = shapes[0].props.noFill ? shapes[0].props.stroke : shapes[0].props.fill
   const value = shapes[0].props.noFill ? shapes[0].props.stroke : shapes[0].props.fill
-  return <ColorInput value={value} onChange={handleChange} />
+  return <ColorInput title="Color Picker" value={value} onChange={handleChange} />
 })
 })
 
 
 const StrokeTypeAction = observer(() => {
 const StrokeTypeAction = observer(() => {
@@ -321,6 +340,7 @@ const StrokeTypeAction = observer(() => {
 
 
   return (
   return (
     <ToggleGroupInput
     <ToggleGroupInput
+      title="Stroke Type"
       options={StrokeTypeOptions}
       options={StrokeTypeOptions}
       value={value}
       value={value}
       onValueChange={v => {
       onValueChange={v => {
@@ -364,6 +384,7 @@ const ArrowModeAction = observer(() => {
 
 
   return (
   return (
     <ToggleGroupMultipleInput
     <ToggleGroupMultipleInput
+      title="Arrow Head"
       options={StrokeTypeOptions}
       options={StrokeTypeOptions}
       value={value}
       value={value}
       onValueChange={v => {
       onValueChange={v => {
@@ -378,8 +399,53 @@ const ArrowModeAction = observer(() => {
   )
   )
 })
 })
 
 
+const TextStyleAction = observer(() => {
+  const app = useApp<Shape>()
+  const shapes = filterShapeByAction<TextShape>(app.selectedShapesArray, 'TextStyle')
+
+  const bold = shapes.every(s => s.props.fontWeight > 500)
+  const italic = shapes.every(s => s.props.italic)
+
+  return (
+    <span className="flex gap-1">
+      <ToggleInput
+        title="Bold"
+        className="tl-contextbar-button"
+        pressed={bold}
+        onPressedChange={v => {
+          shapes.forEach(shape => {
+            shape.update({
+              fontWeight: v ? 700 : 400,
+            })
+            shape.onResetBounds()
+          })
+          app.persist()
+        }}
+      >
+        <TablerIcon name="bold" />
+      </ToggleInput>
+      <ToggleInput
+        title="Italic"
+        className="tl-contextbar-button"
+        pressed={italic}
+        onPressedChange={v => {
+          shapes.forEach(shape => {
+            shape.update({
+              italic: v,
+            })
+            shape.onResetBounds()
+          })
+          app.persist()
+        }}
+      >
+        <TablerIcon name="italic" />
+      </ToggleInput>
+    </span>
+  )
+})
+
 contextBarActionMapping.set('Edit', EditAction)
 contextBarActionMapping.set('Edit', EditAction)
-contextBarActionMapping.set('ResetBounds', ResetBoundsAction)
+contextBarActionMapping.set('AutoResizing', AutoResizingAction)
 contextBarActionMapping.set('LogseqPortalViewMode', LogseqPortalViewModeAction)
 contextBarActionMapping.set('LogseqPortalViewMode', LogseqPortalViewModeAction)
 contextBarActionMapping.set('ScaleLevel', ScaleLevelAction)
 contextBarActionMapping.set('ScaleLevel', ScaleLevelAction)
 contextBarActionMapping.set('OpenPage', OpenPageAction)
 contextBarActionMapping.set('OpenPage', OpenPageAction)
@@ -388,6 +454,7 @@ contextBarActionMapping.set('NoFill', NoFillAction)
 contextBarActionMapping.set('Swatch', SwatchAction)
 contextBarActionMapping.set('Swatch', SwatchAction)
 contextBarActionMapping.set('StrokeType', StrokeTypeAction)
 contextBarActionMapping.set('StrokeType', StrokeTypeAction)
 contextBarActionMapping.set('ArrowMode', ArrowModeAction)
 contextBarActionMapping.set('ArrowMode', ArrowModeAction)
+contextBarActionMapping.set('TextStyle', TextStyleAction)
 
 
 const getContextBarActionTypes = (type: ShapeType) => {
 const getContextBarActionTypes = (type: ShapeType) => {
   return (shapeMapping[type] ?? []).filter(isNonNullable)
   return (shapeMapping[type] ?? []).filter(isNonNullable)

+ 3 - 1
tldraw/apps/tldraw-logseq/src/components/inputs/ToggleInput.tsx

@@ -1,14 +1,16 @@
 import * as Toggle from '@radix-ui/react-toggle'
 import * as Toggle from '@radix-ui/react-toggle'
 
 
 interface ToggleInputProps extends React.HTMLAttributes<HTMLElement> {
 interface ToggleInputProps extends React.HTMLAttributes<HTMLElement> {
+  toggle?: boolean
   pressed: boolean
   pressed: boolean
   onPressedChange: (value: boolean) => void
   onPressedChange: (value: boolean) => void
 }
 }
 
 
-export function ToggleInput({ pressed, onPressedChange, className, ...rest }: ToggleInputProps) {
+export function ToggleInput({ toggle = true, pressed, onPressedChange, className, ...rest }: ToggleInputProps) {
   return (
   return (
     <Toggle.Root
     <Toggle.Root
       {...rest}
       {...rest}
+      data-toggle={toggle}
       className={'tl-toggle-input' + (className ? ' ' + className : '')}
       className={'tl-toggle-input' + (className ? ' ' + className : '')}
       pressed={pressed}
       pressed={pressed}
       onPressedChange={onPressedChange}
       onPressedChange={onPressedChange}

+ 25 - 0
tldraw/apps/tldraw-logseq/src/lib/color.ts

@@ -0,0 +1,25 @@
+let melm: any
+
+function getMeasurementDiv() {
+  // A div used for measurement
+  document.getElementById('__colorMeasure')?.remove()
+
+  const div = document.createElement('div')
+  div.id = '__colorMeasure'
+  div.tabIndex = -1
+
+  document.body.appendChild(div)
+  return div
+}
+
+export function getComputedColor(color: string) {
+  if (color?.toString().startsWith('var')) {
+    const varName = /var\((.*)\)/.exec(color.toString())?.[1]
+    if (varName) {
+      const [v, d] = varName.split(',').map(s => s.trim())
+      return getComputedStyle(getMeasurementDiv()).getPropertyValue(v).trim() ?? d ?? '#000'
+    }
+  }
+
+  return color
+}

+ 2 - 2
tldraw/apps/tldraw-logseq/src/lib/shapes/BoxShape.tsx

@@ -21,7 +21,7 @@ export class BoxShape extends TLBoxShape<BoxShapeProps> {
     size: [100, 100],
     size: [100, 100],
     borderRadius: 2,
     borderRadius: 2,
     stroke: '#000000',
     stroke: '#000000',
-    fill: '#ffffff',
+    fill: 'var(--ls-secondary-background-color)',
     noFill: false,
     noFill: false,
     strokeType: 'line',
     strokeType: 'line',
     strokeWidth: 2,
     strokeWidth: 2,
@@ -87,6 +87,6 @@ export class BoxShape extends TLBoxShape<BoxShapeProps> {
       props.size[1] = Math.max(props.size[1], 1)
       props.size[1] = Math.max(props.size[1], 1)
     }
     }
     if (props.borderRadius !== undefined) props.borderRadius = Math.max(0, props.borderRadius)
     if (props.borderRadius !== undefined) props.borderRadius = Math.max(0, props.borderRadius)
-    return withClampedStyles(props)
+    return withClampedStyles(this, props)
   }
   }
 }
 }

+ 2 - 2
tldraw/apps/tldraw-logseq/src/lib/shapes/DotShape.tsx

@@ -18,7 +18,7 @@ export class DotShape extends TLDotShape<DotShapeProps> {
     point: [0, 0],
     point: [0, 0],
     radius: 4,
     radius: 4,
     stroke: '#000000',
     stroke: '#000000',
-    fill: '#ffffff',
+    fill: 'var(--ls-secondary-background-color)',
     noFill: false,
     noFill: false,
     strokeType: 'line',
     strokeType: 'line',
     strokeWidth: 2,
     strokeWidth: 2,
@@ -50,6 +50,6 @@ export class DotShape extends TLDotShape<DotShapeProps> {
 
 
   validateProps = (props: Partial<DotShapeProps>) => {
   validateProps = (props: Partial<DotShapeProps>) => {
     if (props.radius !== undefined) props.radius = Math.max(props.radius, 1)
     if (props.radius !== undefined) props.radius = Math.max(props.radius, 1)
-    return withClampedStyles(props)
+    return withClampedStyles(this, props)
   }
   }
 }
 }

+ 2 - 2
tldraw/apps/tldraw-logseq/src/lib/shapes/EllipseShape.tsx

@@ -20,7 +20,7 @@ export class EllipseShape extends TLEllipseShape<EllipseShapeProps> {
     point: [0, 0],
     point: [0, 0],
     size: [100, 100],
     size: [100, 100],
     stroke: '#000000',
     stroke: '#000000',
-    fill: '#ffffff',
+    fill: 'var(--ls-secondary-background-color)',
     noFill: false,
     noFill: false,
     strokeType: 'line',
     strokeType: 'line',
     strokeWidth: 2,
     strokeWidth: 2,
@@ -74,7 +74,7 @@ export class EllipseShape extends TLEllipseShape<EllipseShapeProps> {
       props.size[0] = Math.max(props.size[0], 1)
       props.size[0] = Math.max(props.size[0], 1)
       props.size[1] = Math.max(props.size[1], 1)
       props.size[1] = Math.max(props.size[1], 1)
     }
     }
-    return withClampedStyles(props)
+    return withClampedStyles(this, props)
   }
   }
 
 
   /**
   /**

+ 2 - 2
tldraw/apps/tldraw-logseq/src/lib/shapes/HTMLShape.tsx

@@ -95,7 +95,7 @@ export class HTMLShape extends TLBoxShape<HTMLShapeProps> {
 
 
     React.useEffect(() => {
     React.useEffect(() => {
       if (this.props.size[1] === 0) {
       if (this.props.size[1] === 0) {
-        this.onResetBounds()
+        this.onResetBounds({ zoom: app.viewport.camera.zoom })
         app.persist(true)
         app.persist(true)
       }
       }
     }, [])
     }, [])
@@ -146,6 +146,6 @@ export class HTMLShape extends TLBoxShape<HTMLShapeProps> {
       props.size[0] = Math.max(props.size[0], 1)
       props.size[0] = Math.max(props.size[0], 1)
       props.size[1] = Math.max(props.size[1], 1)
       props.size[1] = Math.max(props.size[1], 1)
     }
     }
-    return withClampedStyles(props)
+    return withClampedStyles(this, props)
   }
   }
 }
 }

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/shapes/HighlighterShape.tsx

@@ -66,7 +66,7 @@ export class HighlighterShape extends TLDrawShape<HighlighterShapeProps> {
   })
   })
 
 
   validateProps = (props: Partial<HighlighterShapeProps>) => {
   validateProps = (props: Partial<HighlighterShapeProps>) => {
-    props = withClampedStyles(props)
+    props = withClampedStyles(this, props)
     if (props.strokeWidth !== undefined) props.strokeWidth = Math.max(props.strokeWidth, 1)
     if (props.strokeWidth !== undefined) props.strokeWidth = Math.max(props.strokeWidth, 1)
     return props
     return props
   }
   }

+ 2 - 2
tldraw/apps/tldraw-logseq/src/lib/shapes/LineShape.tsx

@@ -31,7 +31,7 @@ export class LineShape extends TLLineShape<LineShapeProps> {
       end: { id: 'end', canBind: true, point: [1, 1] },
       end: { id: 'end', canBind: true, point: [1, 1] },
     },
     },
     stroke: 'var(--ls-primary-text-color, #000)',
     stroke: 'var(--ls-primary-text-color, #000)',
-    fill: '#ffffff',
+    fill: 'var(--ls-secondary-background-color)',
     noFill: true,
     noFill: true,
     strokeType: 'line',
     strokeType: 'line',
     strokeWidth: 1,
     strokeWidth: 1,
@@ -145,7 +145,7 @@ export class LineShape extends TLLineShape<LineShapeProps> {
   })
   })
 
 
   validateProps = (props: Partial<LineShapeProps>) => {
   validateProps = (props: Partial<LineShapeProps>) => {
-    return withClampedStyles(props)
+    return withClampedStyles(this, props)
   }
   }
 
 
   getShapeSVGJsx({ preview }: any) {
   getShapeSVGJsx({ preview }: any) {

+ 17 - 17
tldraw/apps/tldraw-logseq/src/lib/shapes/LogseqPortalShape.tsx

@@ -144,6 +144,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
     collapsed: false,
     collapsed: false,
     compact: false,
     compact: false,
     scaleLevel: 'md',
     scaleLevel: 'md',
+    isAutoResizing: true,
   }
   }
 
 
   hideRotateHandle = true
   hideRotateHandle = true
@@ -159,7 +160,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
   constructor(props = {} as Partial<LogseqPortalShapeProps>) {
   constructor(props = {} as Partial<LogseqPortalShapeProps>) {
     super(props)
     super(props)
     makeObservable(this)
     makeObservable(this)
-    if (props.collapsed || props.compact) {
+    if (props.collapsed) {
       Object.assign(this.canResize, [true, false])
       Object.assign(this.canResize, [true, false])
     }
     }
     if (props.size?.[1] === 0) {
     if (props.size?.[1] === 0) {
@@ -194,7 +195,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
       this.canResize[1] = !collapsed
       this.canResize[1] = !collapsed
       this.update({
       this.update({
         collapsed: collapsed,
         collapsed: collapsed,
-        size: [this.props.size[0], collapsed ? HEADER_HEIGHT : this.props.collapsedHeight],
+        size: [this.props.size[0], collapsed ? this.getHeaderHeight() : this.props.collapsedHeight],
         collapsedHeight: collapsed ? originalHeight : this.props.collapsedHeight,
         collapsedHeight: collapsed ? originalHeight : this.props.collapsedHeight,
       })
       })
     }
     }
@@ -248,12 +249,9 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
     return size
     return size
   }
   }
 
 
-  shouldAutoResizeHeight() {
-    return this.props.blockType === 'B' && this.props.compact
-  }
-
   getHeaderHeight() {
   getHeaderHeight() {
-    return this.props.compact ? 0 : HEADER_HEIGHT
+    const scale = levelToScale[this.props.scaleLevel ?? 'md']
+    return this.props.compact ? 0 : HEADER_HEIGHT * scale
   }
   }
 
 
   getAutoResizeHeight() {
   getAutoResizeHeight() {
@@ -286,9 +284,10 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
 
 
     let height = bounds.height
     let height = bounds.height
 
 
-    if (this.shouldAutoResizeHeight()) {
+    if (this.props.isAutoResizing) {
       height = this.getAutoResizeHeight() ?? height
       height = this.getAutoResizeHeight() ?? height
     }
     }
+
     return this.update({
     return this.update({
       point: [bounds.minX, bounds.minY],
       point: [bounds.minX, bounds.minY],
       size: [Math.max(1, bounds.width), Math.max(1, height)],
       size: [Math.max(1, bounds.width), Math.max(1, height)],
@@ -373,8 +372,8 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
         ),
         ),
       })
       })
 
 
-      // New page option
-      if (searchResult?.pages?.length === 0 && q) {
+      // New page option when no exact match
+      if (!searchResult?.pages.some(p => p.toLowerCase() === q.toLowerCase()) && q) {
         options.push({
         options.push({
           actionIcon: 'circle-plus',
           actionIcon: 'circle-plus',
           onChosen: () => {
           onChosen: () => {
@@ -607,8 +606,9 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
     const { Page, Block } = renderers
     const { Page, Block } = renderers
 
 
     React.useEffect(() => {
     React.useEffect(() => {
-      if (this.shouldAutoResizeHeight()) {
-        const newHeight = innerHeight + this.getHeaderHeight()
+      if (this.props.isAutoResizing) {
+        const latestInnerHeight = this.getInnerHeight?.() ?? innerHeight
+        const newHeight = latestInnerHeight + this.getHeaderHeight()
         if (innerHeight && Math.abs(newHeight - this.props.size[1]) > AUTO_RESIZE_THRESHOLD) {
         if (innerHeight && Math.abs(newHeight - this.props.size[1]) > AUTO_RESIZE_THRESHOLD) {
           this.update({
           this.update({
             size: [this.props.size[0], newHeight],
             size: [this.props.size[0], newHeight],
@@ -616,7 +616,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
           app.persist(true)
           app.persist(true)
         }
         }
       }
       }
-    }, [innerHeight, this.props.compact])
+    }, [innerHeight, this.props.isAutoResizing])
 
 
     React.useEffect(() => {
     React.useEffect(() => {
       if (!this.initialHeightCalculated) {
       if (!this.initialHeightCalculated) {
@@ -632,7 +632,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
         ref={cpRefContainer}
         ref={cpRefContainer}
         className="tl-logseq-cp-container"
         className="tl-logseq-cp-container"
         style={{
         style={{
-          overflow: this.props.compact ? 'visible' : 'auto',
+          overflow: this.props.isAutoResizing ? 'visible' : 'auto',
         }}
         }}
       >
       >
         {this.props.blockType === 'B' && this.props.compact ? (
         {this.props.blockType === 'B' && this.props.compact ? (
@@ -692,7 +692,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
         })
         })
         return () => {
         return () => {
           this.update({
           this.update({
-            size: [this.props.size[0], HEADER_HEIGHT],
+            size: [this.props.size[0], this.getHeaderHeight()],
           })
           })
         }
         }
       }
       }
@@ -794,7 +794,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
 
 
   ReactIndicator = observer(() => {
   ReactIndicator = observer(() => {
     const bounds = this.getBounds()
     const bounds = this.getBounds()
-    return <rect width={bounds.width} height={bounds.height} fill="transparent" />
+    return <rect width={bounds.width} height={bounds.height} fill="transparent" stroke="none" />
   })
   })
 
 
   validateProps = (props: Partial<LogseqPortalShapeProps>) => {
   validateProps = (props: Partial<LogseqPortalShapeProps>) => {
@@ -803,7 +803,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
       props.size[0] = Math.max(props.size[0], 240 * scale)
       props.size[0] = Math.max(props.size[0], 240 * scale)
       props.size[1] = Math.max(props.size[1], HEADER_HEIGHT * scale)
       props.size[1] = Math.max(props.size[1], HEADER_HEIGHT * scale)
     }
     }
-    return withClampedStyles(props)
+    return withClampedStyles(this, props)
   }
   }
 
 
   getShapeSVGJsx({ preview }: any) {
   getShapeSVGJsx({ preview }: any) {

+ 1 - 2
tldraw/apps/tldraw-logseq/src/lib/shapes/PenShape.tsx

@@ -1,5 +1,4 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 /* eslint-disable @typescript-eslint/no-explicit-any */
-import * as React from 'react'
 import { getStroke } from 'perfect-freehand'
 import { getStroke } from 'perfect-freehand'
 import { SvgPathUtils, TLDrawShape, TLDrawShapeProps } from '@tldraw/core'
 import { SvgPathUtils, TLDrawShape, TLDrawShapeProps } from '@tldraw/core'
 import { SVGContainer, TLComponentProps } from '@tldraw/react'
 import { SVGContainer, TLComponentProps } from '@tldraw/react'
@@ -71,7 +70,7 @@ export class PenShape extends TLDrawShape<PenShapeProps> {
   })
   })
 
 
   validateProps = (props: Partial<PenShapeProps>) => {
   validateProps = (props: Partial<PenShapeProps>) => {
-    props = withClampedStyles(props)
+    props = withClampedStyles(this, props)
     if (props.strokeWidth !== undefined) props.strokeWidth = Math.max(props.strokeWidth, 1)
     if (props.strokeWidth !== undefined) props.strokeWidth = Math.max(props.strokeWidth, 1)
     return props
     return props
   }
   }

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/shapes/PencilShape.tsx

@@ -63,7 +63,7 @@ export class PencilShape extends TLDrawShape<PencilShapeProps> {
   })
   })
 
 
   validateProps = (props: Partial<PencilShapeProps>) => {
   validateProps = (props: Partial<PencilShapeProps>) => {
-    props = withClampedStyles(props)
+    props = withClampedStyles(this, props)
     if (props.strokeWidth !== undefined) props.strokeWidth = Math.max(props.strokeWidth, 1)
     if (props.strokeWidth !== undefined) props.strokeWidth = Math.max(props.strokeWidth, 1)
     return props
     return props
   }
   }

+ 2 - 2
tldraw/apps/tldraw-logseq/src/lib/shapes/PolygonShape.tsx

@@ -22,7 +22,7 @@ export class PolygonShape extends TLPolygonShape<PolygonShapeProps> {
     ratio: 1,
     ratio: 1,
     isFlippedY: false,
     isFlippedY: false,
     stroke: '#000000',
     stroke: '#000000',
-    fill: '#ffffff',
+    fill: 'var(--ls-secondary-background-color)',
     noFill: false,
     noFill: false,
     strokeType: 'line',
     strokeType: 'line',
     strokeWidth: 2,
     strokeWidth: 2,
@@ -72,7 +72,7 @@ export class PolygonShape extends TLPolygonShape<PolygonShapeProps> {
 
 
   validateProps = (props: Partial<PolygonShapeProps>) => {
   validateProps = (props: Partial<PolygonShapeProps>) => {
     if (props.sides !== undefined) props.sides = Math.max(props.sides, 3)
     if (props.sides !== undefined) props.sides = Math.max(props.sides, 3)
-    return withClampedStyles(props)
+    return withClampedStyles(this, props)
   }
   }
 
 
   /**
   /**

+ 45 - 34
tldraw/apps/tldraw-logseq/src/lib/shapes/TextShape.tsx

@@ -6,12 +6,14 @@ import { observer } from 'mobx-react-lite'
 import * as React from 'react'
 import * as React from 'react'
 import type { SizeLevel } from '~lib'
 import type { SizeLevel } from '~lib'
 import { CustomStyleProps, withClampedStyles } from './style-props'
 import { CustomStyleProps, withClampedStyles } from './style-props'
+import { TextAreaUtils } from './text/TextAreaUtils'
 
 
 export interface TextShapeProps extends TLTextShapeProps, CustomStyleProps {
 export interface TextShapeProps extends TLTextShapeProps, CustomStyleProps {
   borderRadius: number
   borderRadius: number
   fontFamily: string
   fontFamily: string
   fontSize: number
   fontSize: number
   fontWeight: number
   fontWeight: number
+  italic: boolean
   lineHeight: number
   lineHeight: number
   padding: number
   padding: number
   type: 'text'
   type: 'text'
@@ -41,6 +43,7 @@ export class TextShape extends TLTextShape<TextShapeProps> {
     lineHeight: 1.2,
     lineHeight: 1.2,
     fontSize: 20,
     fontSize: 20,
     fontWeight: 400,
     fontWeight: 400,
+    italic: false,
     padding: 4,
     padding: 4,
     fontFamily: "var(--ls-font-family), 'Helvetica Neue', Helvetica, Arial, sans-serif",
     fontFamily: "var(--ls-font-family), 'Helvetica Neue', Helvetica, Arial, sans-serif",
     borderRadius: 0,
     borderRadius: 0,
@@ -54,7 +57,17 @@ export class TextShape extends TLTextShape<TextShapeProps> {
 
 
   ReactComponent = observer(({ events, isErasing, isEditing, onEditingEnd }: TLComponentProps) => {
   ReactComponent = observer(({ events, isErasing, isEditing, onEditingEnd }: TLComponentProps) => {
     const {
     const {
-      props: { opacity, fontFamily, fontSize, fontWeight, lineHeight, text, stroke, padding },
+      props: {
+        opacity,
+        fontFamily,
+        fontSize,
+        fontWeight,
+        italic,
+        lineHeight,
+        text,
+        stroke,
+        padding,
+      },
     } = this
     } = this
     const rInput = React.useRef<HTMLTextAreaElement>(null)
     const rInput = React.useRef<HTMLTextAreaElement>(null)
 
 
@@ -75,39 +88,35 @@ export class TextShape extends TLTextShape<TextShapeProps> {
     }, [])
     }, [])
 
 
     const handleKeyDown = React.useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
     const handleKeyDown = React.useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
-      if (e.metaKey) e.stopPropagation()
-      switch (e.key) {
-        case 'Meta': {
-          e.stopPropagation()
-          break
-        }
-        case 'z': {
-          if (e.metaKey) {
-            if (e.shiftKey) {
-              document.execCommand('redo', false)
-            } else {
-              document.execCommand('undo', false)
-            }
-            e.preventDefault()
-          }
-          break
-        }
-        case 'Enter': {
-          if (e.ctrlKey || e.metaKey) {
-            e.currentTarget.blur()
-          }
-          break
+      if (e.key === 'Escape') return
+
+      if (e.key === 'Tab' && text.length === 0) {
+        e.preventDefault()
+        return
+      }
+
+      if (!(e.key === 'Meta' || e.metaKey)) {
+        e.stopPropagation()
+      } else if (e.key === 'z' && e.metaKey) {
+        if (e.shiftKey) {
+          document.execCommand('redo', false)
+        } else {
+          document.execCommand('undo', false)
         }
         }
-        case 'Tab': {
-          e.preventDefault()
-          if (e.shiftKey) {
-            TextUtils.unindent(e.currentTarget)
-          } else {
-            TextUtils.indent(e.currentTarget)
-          }
-          this.update({ text: TextUtils.normalizeText(e.currentTarget.value) })
-          break
+        e.stopPropagation()
+        e.preventDefault()
+        return
+      }
+
+      if (e.key === 'Tab') {
+        e.preventDefault()
+        if (e.shiftKey) {
+          TextAreaUtils.unindent(e.currentTarget)
+        } else {
+          TextAreaUtils.indent(e.currentTarget)
         }
         }
+
+        this.update({ text: TextUtils.normalizeText(e.currentTarget.value) })
       }
       }
     }, [])
     }, [])
 
 
@@ -171,6 +180,7 @@ export class TextShape extends TLTextShape<TextShapeProps> {
           data-isediting={isEditing}
           data-isediting={isEditing}
           style={{
           style={{
             fontFamily,
             fontFamily,
+            fontStyle: italic ? 'italic' : 'normal',
             fontSize,
             fontSize,
             fontWeight,
             fontWeight,
             padding,
             padding,
@@ -217,7 +227,7 @@ export class TextShape extends TLTextShape<TextShapeProps> {
   @action setScaleLevel = async (v?: SizeLevel) => {
   @action setScaleLevel = async (v?: SizeLevel) => {
     this.update({
     this.update({
       scaleLevel: v,
       scaleLevel: v,
-      fontSize: levelToScale[v ?? 'md']
+      fontSize: levelToScale[v ?? 'md'],
     })
     })
     this.onResetBounds()
     this.onResetBounds()
   }
   }
@@ -234,6 +244,7 @@ export class TextShape extends TLTextShape<TextShapeProps> {
         rx={borderRadius}
         rx={borderRadius}
         ry={borderRadius}
         ry={borderRadius}
         fill="transparent"
         fill="transparent"
+        stroke="none"
       />
       />
     )
     )
   })
   })
@@ -242,7 +253,7 @@ export class TextShape extends TLTextShape<TextShapeProps> {
     if (props.isSizeLocked || this.props.isSizeLocked) {
     if (props.isSizeLocked || this.props.isSizeLocked) {
       // props.size = this.getAutoSizedBoundingBox(props)
       // props.size = this.getAutoSizedBoundingBox(props)
     }
     }
-    return withClampedStyles(props)
+    return withClampedStyles(this, props)
   }
   }
 
 
   // Custom
   // Custom

+ 4 - 4
tldraw/apps/tldraw-logseq/src/lib/shapes/YouTubeShape.tsx

@@ -43,7 +43,7 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
     this.update({ url, size: YouTubeShape.defaultProps.size })
     this.update({ url, size: YouTubeShape.defaultProps.size })
   }
   }
 
 
-  ReactComponent = observer(({ events, isErasing, isEditing }: TLComponentProps) => {
+  ReactComponent = observer(({ events, isErasing, isEditing, isSelected }: TLComponentProps) => {
     return (
     return (
       <HTMLContainer
       <HTMLContainer
         style={{
         style={{
@@ -56,7 +56,7 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
         <div
         <div
           className="rounded-lg w-full h-full relative overflow-hidden shadow-xl"
           className="rounded-lg w-full h-full relative overflow-hidden shadow-xl"
           style={{
           style={{
-            pointerEvents: isEditing ? 'all' : 'none',
+            pointerEvents: (isEditing || isSelected) ? 'all' : 'none',
             userSelect: 'none',
             userSelect: 'none',
           }}
           }}
         >
         >
@@ -120,7 +120,7 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
         size: [w, h],
         size: [w, h],
       },
       },
     } = this
     } = this
-    return <rect width={w} height={h} fill="transparent" />
+    return <rect width={w} height={h} fill="transparent" stroke="none" />
   })
   })
 
 
   validateProps = (props: Partial<YouTubeShapeProps>) => {
   validateProps = (props: Partial<YouTubeShapeProps>) => {
@@ -128,6 +128,6 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
       props.size[0] = Math.max(props.size[0], 1)
       props.size[0] = Math.max(props.size[0], 1)
       props.size[1] = Math.max(props.size[0] * this.aspectRatio, 1)
       props.size[1] = Math.max(props.size[0] * this.aspectRatio, 1)
     }
     }
-    return withClampedStyles(props)
+    return withClampedStyles(this, props)
   }
   }
 }
 }

+ 2 - 0
tldraw/apps/tldraw-logseq/src/lib/shapes/index.ts

@@ -12,6 +12,7 @@ import { PencilShape } from './PencilShape'
 import { PolygonShape } from './PolygonShape'
 import { PolygonShape } from './PolygonShape'
 import { TextShape } from './TextShape'
 import { TextShape } from './TextShape'
 import { YouTubeShape } from './YouTubeShape'
 import { YouTubeShape } from './YouTubeShape'
+import type { PenShape } from './PenShape'
 
 
 export type Shape =
 export type Shape =
   | BoxShape
   | BoxShape
@@ -21,6 +22,7 @@ export type Shape =
   | ImageShape
   | ImageShape
   | VideoShape
   | VideoShape
   | LineShape
   | LineShape
+  | PenShape
   | PencilShape
   | PencilShape
   | PolygonShape
   | PolygonShape
   | TextShape
   | TextShape

+ 14 - 1
tldraw/apps/tldraw-logseq/src/lib/shapes/style-props.tsx

@@ -1,3 +1,8 @@
+import { darken } from 'polished'
+import { withFillShapes } from '~components/ContextBar/contextBarActionFactory'
+import type { Shape } from '~lib'
+import { getComputedColor } from '~lib/color'
+
 export interface CustomStyleProps {
 export interface CustomStyleProps {
   stroke: string
   stroke: string
   fill: string
   fill: string
@@ -7,8 +12,16 @@ export interface CustomStyleProps {
   opacity: number
   opacity: number
 }
 }
 
 
-export function withClampedStyles<P>(props: P & Partial<CustomStyleProps>) {
+export function withClampedStyles<P>(self: Shape, props: P & Partial<CustomStyleProps>) {
   if (props.strokeWidth !== undefined) props.strokeWidth = Math.max(props.strokeWidth, 1)
   if (props.strokeWidth !== undefined) props.strokeWidth = Math.max(props.strokeWidth, 1)
   if (props.opacity !== undefined) props.opacity = Math.min(1, Math.max(props.opacity, 0))
   if (props.opacity !== undefined) props.opacity = Math.min(1, Math.max(props.opacity, 0))
+
+  let fill = props.fill ?? (self.props as any).fill
+  if (fill !== undefined && !props.noFill && withFillShapes.includes(self.props.type)) {
+    fill = getComputedColor(fill)
+    const strokeColor = darken(0.3, fill)
+    props.stroke = strokeColor
+  }
+
   return props
   return props
 }
 }

+ 0 - 2
tldraw/apps/tldraw-logseq/src/lib/shapes/text/TextLabel.tsx

@@ -114,8 +114,6 @@ export const TextLabel = React.memo(function TextLabel({
           elm.select()
           elm.select()
         }
         }
       })
       })
-    } else {
-      onBlur?.()
     }
     }
   }, [isEditing, onBlur])
   }, [isEditing, onBlur])
 
 

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/tools/LogseqPortalTool/states/CreatingState.tsx

@@ -65,7 +65,7 @@ export class CreatingState extends TLToolState<
       this.app.setSelectedShapes([this.creatingShape.id])
       this.app.setSelectedShapes([this.creatingShape.id])
     } else {
     } else {
       this.app.deleteShapes([this.creatingShape.id])
       this.app.deleteShapes([this.creatingShape.id])
-      this.app.clearEditingShape()
+      this.app.setEditingShape()
     }
     }
   }
   }
 }
 }

+ 8 - 2
tldraw/apps/tldraw-logseq/src/styles.css

@@ -243,7 +243,7 @@
 button.tl-select-input-trigger {
 button.tl-select-input-trigger {
   @apply flex items-center py-1 px-3;
   @apply flex items-center py-1 px-3;
   box-shadow: 0 0 0 1px var(--ls-secondary-border-color);
   box-shadow: 0 0 0 1px var(--ls-secondary-border-color);
-  background-color: var(--ls-quaternary-background-color);
+  background-color: var(--ls-secondary-background-color);
   min-width: 160px;
   min-width: 160px;
   border-radius: 8px;
   border-radius: 8px;
   font-size: 16px;
   font-size: 16px;
@@ -264,6 +264,7 @@ button.tl-select-input-trigger {
 .tl-select-input-select-item {
 .tl-select-input-select-item {
   cursor: default;
   cursor: default;
   padding: 4px 12px;
   padding: 4px 12px;
+  outline-offset: -1px;
 
 
   color: var(--ls-secondary-text-color);
   color: var(--ls-secondary-text-color);
 
 
@@ -340,6 +341,7 @@ button.tl-select-input-trigger {
   font-family: inherit;
   font-family: inherit;
   font-size: inherit;
   font-size: inherit;
   font-variant: inherit;
   font-variant: inherit;
+  font-style: inherit;
   text-align: inherit;
   text-align: inherit;
   min-height: inherit;
   min-height: inherit;
   min-width: inherit;
   min-width: inherit;
@@ -630,7 +632,7 @@ button.tl-select-input-trigger {
 }
 }
 
 
 .tl-html-container {
 .tl-html-container {
-  @apply h-full w-full m-0 relative;
+  @apply h-full w-full m-0 relative flex flex-col;
   user-select: text;
   user-select: text;
   transform-origin: top left;
   transform-origin: top left;
 }
 }
@@ -740,6 +742,9 @@ html[data-theme='dark'] {
   &:hover {
   &:hover {
     background-color: var(--ls-tertiary-background-color);
     background-color: var(--ls-tertiary-background-color);
   }
   }
+  &[data-toggle='false'] {
+    opacity: 1;
+  }
   &[data-state='on'] {
   &[data-state='on'] {
     background-color: var(--ls-tertiary-background-color);
     background-color: var(--ls-tertiary-background-color);
     color: var(--ls-primary-text-color);
     color: var(--ls-primary-text-color);
@@ -751,6 +756,7 @@ html[data-theme='dark'] {
   @apply rounded inline-flex items-center justify-center;
   @apply rounded inline-flex items-center justify-center;
   height: 32px;
   height: 32px;
   width: 32px;
   width: 32px;
+  color: var(--ls-primary-text-color);
 
 
   &:hover {
   &:hover {
     background-color: var(--ls-tertiary-background-color);
     background-color: var(--ls-tertiary-background-color);

+ 7 - 1
tldraw/demo/postcss.config.js

@@ -3,7 +3,13 @@ module.exports = {
     'postcss-import': {},
     'postcss-import': {},
     'postcss-nested': {},
     'postcss-nested': {},
     'postcss-import-ext-glob': {},
     'postcss-import-ext-glob': {},
-    tailwindcss: {},
+    'tailwindcss/nesting': {},
+    tailwindcss: {
+      content: [
+        './**/*.jsx',
+        '../apps/**/*.{js,jsx,ts,tsx}',
+      ]
+    },
     autoprefixer: {},
     autoprefixer: {},
   },
   },
 }
 }

+ 14 - 1
tldraw/demo/src/App.jsx

@@ -139,6 +139,19 @@ const searchHandler = q => {
 export default function App() {
 export default function App() {
   const [theme, setTheme] = React.useState('light')
   const [theme, setTheme] = React.useState('light')
 
 
+  const [model, setModel] = React.useState(documentModel)
+
+  // Mimic external reload event
+  React.useEffect(() => {
+    const interval = setInterval(() => {
+      setModel(onLoad())
+    }, 5000)
+
+    return () => {
+      clearInterval(interval)
+    }
+  }, [])
+
   return (
   return (
     <div className={`h-screen w-screen`}>
     <div className={`h-screen w-screen`}>
       <ThemeSwitcher theme={theme} setTheme={setTheme} />
       <ThemeSwitcher theme={theme} setTheme={setTheme} />
@@ -157,7 +170,7 @@ export default function App() {
           saveAsset: fileToBase64,
           saveAsset: fileToBase64,
           makeAssetUrl: a => a,
           makeAssetUrl: a => a,
         }}
         }}
-        model={documentModel}
+        model={model}
         onPersist={onPersist}
         onPersist={onPersist}
       />
       />
     </div>
     </div>

+ 8 - 0
tldraw/demo/src/logseq-styles.css

@@ -0,0 +1,8 @@
+@import '../../../resources/css/inter.css';
+@import '../../../resources/css/fonts.css';
+@import '../../../resources/css/animation.css';
+@import '../../../resources/css/table.css';
+@import '../../../resources/css/tooltip.css';
+@import '../../../resources/css/common.css';
+@import '../../../resources/css/tabler-extension.css';
+@import '../../apps/tldraw-logseq/src/styles.css';

+ 1 - 1
tldraw/demo/src/main.jsx

@@ -1,9 +1,9 @@
-import '../../../tailwind.all.css'
 import React from 'react'
 import React from 'react'
 import ReactDOM from 'react-dom'
 import ReactDOM from 'react-dom'
 
 
 import App from './App'
 import App from './App'
 
 
+import './logseq-styles.css'
 import './index.css'
 import './index.css'
 
 
 // Not using strict mode because it may cause side effect problems
 // Not using strict mode because it may cause side effect problems

+ 0 - 8
tldraw/demo/tailwind.config.js

@@ -1,8 +0,0 @@
-module.exports = {
-  // just import everything for ease of dev
-  safelist: [{ pattern: /.*/ }],
-  theme: {
-    extend: {},
-  },
-  plugins: [],
-}

+ 0 - 10
tldraw/packages/core/src/lib/TLApi/TLApi.ts

@@ -10,16 +10,6 @@ export class TLApi<S extends TLShape = TLShape, K extends TLEventMap = TLEventMa
     this.app = app
     this.app = app
   }
   }
 
 
-  /**
-   * Set the current page.
-   *
-   * @param page The new current page or page id.
-   */
-  changePage = (page: string | TLPage<S, K>): this => {
-    this.app.setCurrentPage(page)
-    return this
-  }
-
   editShape = (shape: string | S | undefined): this => {
   editShape = (shape: string | S | undefined): this => {
     this.app.transition('select').selectedTool.transition('editingShape', { shape })
     this.app.transition('select').selectedTool.transition('editingShape', { shape })
     return this
     return this

+ 10 - 13
tldraw/packages/core/src/lib/TLApp/TLApp.ts

@@ -283,7 +283,9 @@ export class TLApp<
     ['page', new TLPage(this, { id: 'page', name: 'page', shapes: [], bindings: {} })],
     ['page', new TLPage(this, { id: 'page', name: 'page', shapes: [], bindings: {} })],
   ])
   ])
 
 
-  @observable currentPageId = 'page'
+  @computed get currentPageId() {
+    return this.pages.keys().next().value
+  }
 
 
   @computed get currentPage(): TLPage<S, K> {
   @computed get currentPage(): TLPage<S, K> {
     return this.getPageById(this.currentPageId)
     return this.getPageById(this.currentPageId)
@@ -295,11 +297,6 @@ export class TLApp<
     return page
     return page
   }
   }
 
 
-  @action setCurrentPage(page: string | TLPage<S, K>): this {
-    this.currentPageId = typeof page === 'string' ? page : page.id
-    return this
-  }
-
   @action addPages(pages: TLPage<S, K>[]): this {
   @action addPages(pages: TLPage<S, K>[]): this {
     pages.forEach(page => this.pages.set(page.id, page))
     pages.forEach(page => this.pages.set(page.id, page))
     this.persist()
     this.persist()
@@ -484,7 +481,8 @@ export class TLApp<
     return this
     return this
   }
   }
 
 
-  readonly clearEditingShape = (): this => {
+  readonly clearEditingState = (): this => {
+    this.selectedTool.transition('idle')
     return this.setEditingShape()
     return this.setEditingShape()
   }
   }
 
 
@@ -534,7 +532,7 @@ export class TLApp<
       this.selectionRotation = 0
       this.selectionRotation = 0
     }
     }
     if (shapes.length === 0) {
     if (shapes.length === 0) {
-      this.clearEditingShape()
+      this.setEditingShape()
     }
     }
     return this
     return this
   }
   }
@@ -642,11 +640,10 @@ export class TLApp<
     } = this
     } = this
     return currentPage.shapes.filter(shape => {
     return currentPage.shapes.filter(shape => {
       return (
       return (
-        shape.props.parentId === currentPage.id &&
-        (!shape.canUnmount ||
-          selectedShapes.has(shape) ||
-          BoundsUtils.boundsContain(currentView, shape.rotatedBounds) ||
-          BoundsUtils.boundsCollide(currentView, shape.rotatedBounds))
+        !shape.canUnmount ||
+        selectedShapes.has(shape) ||
+        BoundsUtils.boundsContain(currentView, shape.rotatedBounds) ||
+        BoundsUtils.boundsCollide(currentView, shape.rotatedBounds)
       )
       )
     })
     })
   }
   }

部分文件因为文件数量过多而无法显示