浏览代码

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!
 ;; Repl fn
 frontend.util.property/add-page-properties
-;; Test runner used by shadow
+;; Test runners used by shadow
 frontend.test.node-test-runner/main
+frontend.test.frontend-node-test-runner/main
 ;; Test runner for nbb
 logseq.graph-parser.nbb-test-runner/run-tests

+ 2 - 2
android/app/build.gradle

@@ -6,8 +6,8 @@ android {
         applicationId "com.logseq.app"
         minSdkVersion rootProject.ext.minSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
-        versionCode 35
-        versionName "0.8.1"
+        versionCode 36
+        versionName "0.8.2"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         aaptOptions {
              // 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
 ;; API
 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)
     []))
 
+;; TODO: Use text/parse-property to determine refs rather than maintain this similar
+;; implementation to parse-property
 (defn- get-page-ref-names-from-properties
   [format properties user-config]
   (let [page-refs (->>
@@ -174,7 +176,11 @@
                             (and (string? v)
                                  (not (gp-mldoc/link? format 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)
                                 (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
   [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]))]
     (mapv
      (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
   "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]))
 
 (def app-name
@@ -12,7 +11,8 @@
 
 (defn local-asset?
   [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")
 ;; TODO read configurable value?

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

@@ -154,7 +154,8 @@
                           (remove string/blank?)))
           tags (:tags properties)
           tags (->> (->vec-concat tags filetags)
-                    (remove string/blank?))
+                    (remove string/blank?)
+                    vec)
           properties (assoc properties :tags tags :alias alias)
           properties (-> properties
                          (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" "::")
 
+(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?
   [block]
   (and
@@ -60,7 +73,7 @@
   [content]
   (when content
     (and (string/includes? content properties-start)
-         (gp-util/safe-re-find properties-end-pattern content))))
+         (re-find properties-end-pattern content))))
 
 (defn ->new-properties
   "New syntax: key:: value"

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

@@ -5,6 +5,7 @@
             [clojure.set :as set]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [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]]))
 
 (defn get-file-basename
@@ -109,8 +110,8 @@
 
      (and (string? s)
             ;; 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)
                        (mapcat
                         (fn [s]
@@ -199,7 +200,35 @@
 (defonce non-parsing-properties
   (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
+  "Property value parsing that takes into account built-in properties, format
+  and user config"
   ([k v config-state]
    (parse-property :markdown k v config-state))
   ([format k v config-state]
@@ -212,14 +241,6 @@
                    (get config-state :ignored-page-references-keywords)) k)
        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 ""
        v
 
@@ -229,5 +250,12 @@
        (gp-mldoc/link? format v)
        v
 
+       (contains? gp-property/editable-linkable-built-in-properties (keyword k))
+       (split-page-refs-without-brackets v)
+
        :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]
             [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
   "Normalize file path (for reading paths from FS, not required by writting)"
   [s]
@@ -40,7 +34,7 @@
 (defn tag-valid?
   [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
   ([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]
   (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
   "Extracts page-name from page-ref string"
   [s]

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

@@ -1,5 +1,5 @@
 (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]))
 
 (deftest test->new-properties
@@ -24,3 +24,16 @@
 
     "hello\n:PROPERTIES:\n:foo: bar\n:nice\n:END:\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
-  (:require [cljs.test :refer [are deftest testing]]
+  (:require [cljs.test :refer [are deftest testing is]]
             [logseq.graph-parser.text :as text]))
 
 (deftest test-get-page-name
@@ -109,4 +109,9 @@
       :tags "\"[[foo]], [[bar]]\"" "\"[[foo]], [[bar]]\""
       :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)

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

@@ -1,9 +1,10 @@
 (ns logseq.graph-parser-test
-  (:require [cljs.test :refer [deftest testing is]]
+  (:require [cljs.test :refer [deftest testing is are]]
             [clojure.string :as string]
             [logseq.graph-parser :as graph-parser]
             [logseq.db :as ldb]
             [logseq.graph-parser.block :as gp-block]
+            [logseq.graph-parser.property :as gp-property]
             [datascript.core :as d]))
 
 (def foo-edn
@@ -97,3 +98,139 @@
     (test-property-order 4))
   (testing "Sort order and persistence of 10 properties"
     (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) {
     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.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
   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)
 
   // 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_2, and
   //   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.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
 
     // test search results

+ 1 - 1
libs/package.json

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

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

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

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

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

+ 10 - 0
libs/src/LSPlugin.ts

@@ -140,6 +140,8 @@ export interface AppUserConfigs {
 
   currentGraph: string
   showBracket: boolean
+  enabledFlashcards: boolean
+  enabledJournals: boolean
 
   [key: string]: any
 }
@@ -410,6 +412,7 @@ export interface IAppProxy {
 
   // hook events
   onCurrentGraphChanged: IUserHook
+  onGraphAfterIndexed: IUserHook<{repo: string}>
   onThemeModeChanged: IUserHook<{ mode: 'dark' | 'light' }>
   onThemeChanged: IUserHook<Partial<{name: string, mode: string, pid: string, url: string}>>
   onBlockRendererSlotted: IUserSlotHook<{ uuid: BlockUUID }>
@@ -551,6 +554,12 @@ export interface IEditorProxy extends Record<string, any> {
     namespace: BlockPageName
   ) => 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
    *
@@ -565,6 +574,7 @@ export interface IEditorProxy extends Record<string, any> {
       before: boolean
       sibling: boolean
       isPageBlock: boolean
+      customUUID: string
       properties: {}
     }>
   ) => Promise<BlockEntity | null>

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

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

+ 25 - 9
resources/css/common.css

@@ -843,8 +843,14 @@ i.ti {
 
 .heading-bg {
   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 **/
@@ -901,20 +907,30 @@ button.menu:focus {
   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 {
   background-color: var(--ls-primary-background-color, #fff);
   color: var(--ls-primary-text-color);
   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 {

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


+ 1 - 1
resources/js/preload.js

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

+ 1 - 1
resources/package.json

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

+ 1 - 1
shadow-cljs.edn

@@ -72,7 +72,7 @@
          :devtools        {:enabled false}
          ;; disable :static-fns to allow for with-redefs and repl development
          :compiler-options {:static-fns false}
-         :main            frontend.test.node-test-runner/main}
+         :main            frontend.test.frontend-node-test-runner/main}
 
   :publishing {:target        :browser
                :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.window :as win]
             [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))))
 
@@ -536,6 +537,12 @@
     (f)
     (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]
   (let [main-channel "main"]
     (.handle ipcMain main-channel

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

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

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

@@ -17,7 +17,8 @@
             [frontend.ui :as ui]
             [frontend.handler.notification :as notification]
             [frontend.handler.repo :as repo-handler]
-            [frontend.handler.user :as user]))
+            [frontend.handler.user :as user]
+            [dommy.core :as dom]))
 
 (defn persist-dbs!
   []
@@ -124,6 +125,15 @@
                                        :on-error   error-f}]
                          (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"
                      (fn [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.mldoc :as gp-mldoc]
             [logseq.graph-parser.text :as text]
+            [logseq.graph-parser.property :as gp-property]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util.block-ref :as block-ref]
             [logseq.graph-parser.util.page-ref :as page-ref]
@@ -1821,9 +1822,15 @@
   [:span ", "])
 
 (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
      (if property-pages-enabled?
        (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}})])]])
 
 (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?]
-  (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-id (str "editor-" edit-input-id)
         slide? (:slide? config)
@@ -2222,7 +2230,7 @@
 
              (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)]
              (refs-cp uuid)))]))))
 
@@ -2246,6 +2254,7 @@
         block-el-id (str "ls-block-" blocks-container-id "-" uuid)
         config {:id (str uuid)
                 :db/id (:db/id block-entity)
+                :block/uuid uuid
                 :block? true
                 :editor-box (state/get-component :editor/box)}
         edit-input-id (str "edit-block-" blocks-container-id "-" uuid)
@@ -2468,9 +2477,7 @@
         *navigating-block (get state ::navigating-block)
         navigating-block (rum/react *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])
                       blocks (db/get-paginated-blocks repo (:db/id block)
                                                       {:scoped-block-id (:db/id block)})
@@ -2613,7 +2620,7 @@
   (let [repo (state/get-current-repo)
         ref? (:ref? 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
        (fn [] (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/lazy-visible
     (fn [] (custom-query* config q))
-    {:debug-id q})))
+    {:debug-id q
+     :trigger-once? false})))
 
 (defn admonition
   [config type result]
@@ -3344,7 +3352,7 @@
              (assoc state
                     ::initial-block    first-block
                     ::navigating-block (atom (:block/uuid first-block)))))}
-  [state blocks config]
+  [state block config]
   (let [repo (state/get-current-repo)
         *navigating-block (::navigating-block state)
         navigating-block (rum/react *navigating-block)
@@ -3357,10 +3365,10 @@
                  (let [block navigating-block-entity]
                    (db/get-paginated-blocks repo (:db/id block)
                                             {:scoped-block-id (:db/id block)}))
-                 blocks)]
+                 [block])]
     [:div
      (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
                     :navigating-block *navigating-block}))
      (blocks-container blocks (assoc config
@@ -3375,8 +3383,7 @@
    (cond-> option
      (:document/mode? config) (assoc :class "doc-mode"))
    (cond
-     (and (or (:ref? config) (:custom-query? config))
-          (:group-by-page? config))
+     (and (:custom-query? config) (:group-by-page? config))
      [:div.flex.flex-col
       (let [blocks (sort-by (comp :block/journal-day first) > blocks)]
         (for [[page blocks] blocks]
@@ -3384,8 +3391,7 @@
            (fn []
              (let [alias? (:block/alias? 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))}
                             (:ref? config)
                             (assoc :class "color-level px-2 sm:px-7 py-2 rounded"))
@@ -3393,11 +3399,34 @@
                  [:div
                   (page-cp config page)
                   (when alias? [:span.text-sm.font-medium.opacity-50 " Alias"])]
-                 (for [[parent blocks] parent-blocks]
+                 (for [block blocks']
                    (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)
           (vector? (first blocks)))

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

@@ -55,35 +55,43 @@
 
 (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
 (def block-background-colors
@@ -145,192 +153,199 @@
                                           (editor-handler/set-block-property! block-id :template-including-parent false))
                                         (state/hide-custom-context-menu!)))))))])
       (ui/menu-link
-       {:key "Make template"
+       {:key "Make a Template"
         :on-click (fn [e]
                     (util/stop e)
                     (reset! edit? true))}
-       "Make template"))))
+       "Make a Template"
+       nil))))
 
 (rum/defc ^:large-vars/cleanup-todo block-context-menu-content
   [_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])]
       (let [properties (:block/properties block)
             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)
-             :style    {:margin-right 14
-                        :margin-top   4}
              :on-click (fn [_e]
                          (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
-          (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
   [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
   [page]
   (when-not (string/blank? 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
 ;; Also, keyboard bindings should only be activated after

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

@@ -12,10 +12,8 @@
 }
 
 #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);
 }

+ 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)
         (ui/lazy-visible
          (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))
 
        ;; referenced blocks
-       (when-not whiteboard?
+       (when-not (or block? whiteboard?)
          [:div {:key "page-references"}
           (rum/with-key
             (reference/references route-page-name)

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

@@ -10,7 +10,8 @@
             [frontend.util.property :as property]
             [frontend.format.block :as block]
             [medley.core :as medley]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [frontend.modules.outliner.tree :as tree]))
 
 ;; TODO: extract to table utils
 (defn- sort-result-by
@@ -82,7 +83,8 @@
   (rum/local false ::select?)
   [state config current-block result {:keys [page?]} map-inline page-cp ->elem inline-text]
   (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])
           select? (get state ::select?)
           *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.ui :as ui]
             [frontend.util :as util]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [frontend.modules.outliner.tree :as tree]))
 
 (defn- frequencies-sort
   [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)
-  [state filters-atom _close-fn references page-name]
+  [state filters-atom *references page-name]
   (let [filter-search (get state ::filterSearch)
+        references (rum/react *references)
         filtered-references  (frequencies-sort
                               (if (= @filter-search "")
                                 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.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
@@ -34,6 +64,16 @@
        [:h3#modal-headline.text-lg.leading-6.font-medium "Filter"]
        [:span.text-xs
         "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
       (ui/icon "search")
       [:input.cp__filters-input.w-full
@@ -41,30 +81,17 @@
         :auto-focus true
         :on-change (fn [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
-  [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
   [block-id]
@@ -80,16 +107,10 @@
      (content/content block-id
                       {: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
-   (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
                                      :ref? true
                                      :breadcrumb-show? true
@@ -97,24 +118,21 @@
                                      :editor-box editor/box
                                      :filters filters}
                                     {})]
-     (reset! *filtered-ref-blocks filtered-ref-blocks)
      (content/content page-name {:hiccup ref-hiccup}))])
 
 (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)
-        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
      [: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
        {:title "Filter"
         :on-mouse-over (fn [_e]
@@ -124,10 +142,8 @@
         :on-mouse-down (fn [e]
                          (util/stop-propagation e))
         :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
                                    (empty? filter-state)
                                    ""
@@ -140,36 +156,81 @@
                           :style {:fontSize 24}})]]
 
      (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?
       :title-trigger? true
       :init-collapsed (fn [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/local nil ::ref-pages)
   {:init (fn [state]
            (let [page-name (first (:rum/args state))
                  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)))}
   [state 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)
           filters-atom (get state ::filters)
           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.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
   [page-name]
@@ -178,8 +239,7 @@
    (ui/lazy-visible
     (fn []
       (references* page-name))
-    {:trigger-once? true
-     :debug-id (str page-name " references")})))
+    {:debug-id (str page-name " references")})))
 
 (rum/defcs unlinked-references-aux
   < rum/reactive db-mixins/query

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

@@ -10,3 +10,11 @@
   padding-left: 0.5rem;
   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:"]
     (ui/with-shortcut :go/search-in-page "bottom"
       [:div.flex-row.flex.align-items
-       [:div.mr-2 "Search in page:"]
+       [:div.mr-2 "Search blocks in page:"]
        [:div {:style {:margin-top 3}}
         (ui/toggle in-page-search?
                    (fn [_value]

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

@@ -12,6 +12,7 @@
             [frontend.components.svg :as svg]
             [frontend.components.theme :as theme]
             [frontend.components.widgets :as widgets]
+            [frontend.components.find-in-page :as find-in-page]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
@@ -72,7 +73,7 @@
         whiteboard-page? (db-model/whiteboard-page? name)]
     [:a {:on-click (fn [e]
                      (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))]
                        (if (and (gobj/get e "shiftKey") (not whiteboard-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
       {:data-is-margin-less-pages margin-less-pages?}
 
+      (when (util/electron?)
+        (find-in-page/search))
+
       (when show-action-bar?
         (action-bar/action-bar))
 
@@ -484,15 +488,41 @@
          :else
          [: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
   []
-  (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
        {:class-names "fade"
         :timeout {:enter 500
                   :exit 300}}
-       links))))
+       (render-custom-context-menu links position)))))
+
 
 (rum/defc new-block-mode < rum/reactive
   []
@@ -521,14 +551,6 @@
                    (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 <
   (mixins/modal :modal/show?)
   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-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-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
   journal-page? page-alias-set pull-block
   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)]
       (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
   ([page]
    (get-page-referenced-blocks-full (state/get-current-repo) page nil))
@@ -1206,7 +1194,8 @@
          (->>
           (react/q repo
             [:frontend.db.react/refs page-id]
-            {:query-fn (fn []
+            {:use-cache? false
+             :query-fn (fn []
                          (let [entities (mapcat (fn [id]
                                                   (:block/_path-refs (db-utils/entity id))) pages)
                                blocks (map (fn [e] {:block/parent (:block/parent e)
@@ -1217,34 +1206,7 @@
             nil)
           react
           :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
   [journal-title]

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

@@ -6,7 +6,6 @@
             [clojure.set :as set]
             [clojure.string :as string]
             [clojure.walk :as walk]
-            [frontend.state :as state]
             [frontend.date :as date]
             [frontend.db.model :as model]
             [frontend.db.query-react :as query-react]
@@ -235,12 +234,20 @@
     (= 4 (count 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
   [e]
   (let [k (string/replace (name (nth e 1)) "_" "-")
         v (nth e 2)
         v (if-not (nil? v)
-            (text/parse-property k v (state/get-config))
+            (parse-property-value (str v))
             v)
         v (if (coll? v) (first v) v)]
     {:query (list 'property '?b (keyword k) v)
@@ -285,7 +292,7 @@
   (let [[k v] (rest e)
         k (string/replace (name k) "_" "-")]
     (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')]
         {:query (list 'page-property '?p (keyword k) val)
          :rules [:page-property]})

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

@@ -33,10 +33,6 @@
 ;; ::refs
 ;; get BLOCKS referencing PAGE or BLOCK
 (s/def ::refs (s/tuple #(= ::refs %) int?))
-;; ::refs-count
-;; get refs count
-(s/def ::refs-count int?)
-
 ;; custom react-query
 (s/def ::custom any?)
 
@@ -46,7 +42,6 @@
                                 :journals ::journals
                                 :page<-pages ::page<-pages
                                 :refs ::refs
-                                :refs-count ::refs-count
                                 :custom ::custom))
 
 (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)
                     (map :v)
                     (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)
                   (distinct))
         other-blocks (->> (filter (fn [datom] (= "block" (namespace (:a datom)))) tx-data)
@@ -259,10 +257,9 @@
                                          (:db/id (:block/page block)))
                                 blocks [[::block (:db/id 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 page-id]])]
                             (concat blocks page-blocks path-refs')))
@@ -270,9 +267,7 @@
 
                        (mapcat
                         (fn [ref]
-                          [
-                           ;; [::refs-count (:db/id entity)]
-                           [::refs ref]])
+                          [[::refs ref]])
                         refs)
 
                        (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.util :as util]
    [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
 
@@ -250,48 +248,31 @@
   (reset! *swipe nil))
 
 (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]
             (when (:block/name ref)
               {:db/id (:db/id ref)
                :block/name (:block/name ref)
                :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]
             [cljs.reader :as reader]
             [clojure.string :as string]
-            [dommy.core :as d]
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.db :as db]
@@ -116,19 +115,8 @@
 
 (defn show-custom-context-menu! [e context-menu-content]
   (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
   "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)
          (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!
   [refs]
   (remove (fn [x] (or
@@ -386,7 +352,6 @@
                 block
                 (dissoc block :block/pre-block?))
         block (update block :block/refs remove-non-existed-refs!)
-        block (attach-page-properties-if-exists! block)
         new-properties (merge
                         (select-keys properties (property/hidden-properties))
                         (:block/properties block))]
@@ -757,7 +722,8 @@
                    (remove nil?))]
       (doseq [id ids]
         (let [block (db/pull [:block/uuid id])]
-          (set-marker block))))))
+          (when (not-empty (:block/content block))
+            (set-marker block)))))))
 
 (defn cycle-todo!
   []
@@ -2711,7 +2677,7 @@
                        (surround-by? input "#" :end)
                        (= key "#"))]
       (cond
-        (and (contains? #{"ArrowLeft" "ArrowRight" "ArrowUp" "ArrowDown"} key)
+        (and (contains? #{"ArrowLeft" "ArrowRight"} key)
              (contains? #{:property-search :property-value-search} (state/get-editor-action)))
         (state/clear-editor-action!)
 
@@ -2956,8 +2922,9 @@
 
 (defn- cut-blocks-and-clear-selections!
   [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
   [_e]

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

@@ -59,6 +59,7 @@
   (db/set-key-value repo :ast/version db-schema/ast-version)
   (search-handler/rebuild-indices!)
   (db/persist! repo)
+  (plugin-handler/hook-plugin-app :graph-after-indexed {:repo repo :empty-graph? empty-graph?})
   (when (state/setups-picker?)
     (if empty-graph?
       (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
   (let [repo (state/get-current-repo)
         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))
         tx       (->> (map (fn [{:block/keys [uuid content properties] :as block}]
                              (let [content    (let [content' (replace-old-page! content old-original-name new-name)]
@@ -389,8 +389,11 @@
                                   {:block/uuid       uuid
                                    :block/content    content
                                    :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?))]
     (db/transact! repo tx)
     (doseq [page-id page-ids]

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

@@ -9,7 +9,10 @@
             [promesa.core :as p]
             [logseq.graph-parser.text :as text]
             [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!
   [repo q]
@@ -51,6 +54,56 @@
            (swap! state/state assoc search-key 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!
   ([]
    (clear-search! true))

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

@@ -158,12 +158,24 @@
                                       db-schema/retract-attributes)))))
 
         (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)}
-                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)))
 
       (swap! txs-state conj (dissoc m :db/other-tx))
@@ -194,8 +206,16 @@
                                                  (assoc :block/left parent))))
                                            immediate-children)))
                     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))
 
   (-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
 ;; 2. Its children' block/path-refs might need to be updated too.
 (defn compute-block-path-refs
-  [tx-meta blocks]
+  [{:keys [tx-meta]} blocks]
   (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 (react/path-refs-need-recalculated? tx-meta)
         (let [*computed-ids (atom #{})]
@@ -44,13 +41,15 @@
                             refs-changed? (not= old-refs new-refs)
                             children (db-model/get-block-children-ids repo (:block/uuid block))
                             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)))
                         (util/concat-without-nil
-                         [(when (and refs-changed? (seq new-refs))
+                         [(when (and (seq new-refs)
+                                     refs-changed?)
                             {:db/id (:db/id block)
                              :block/path-refs new-refs})]
                          children-refs))))
@@ -66,7 +65,7 @@
             repo (state/get-current-repo)
             refs-tx (util/profile
                      "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)
             tx (util/concat-without-nil truncate-refs-tx refs-tx)
             tx-report' (if (seq tx)

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

@@ -84,20 +84,29 @@
       (assoc root' :block/children children)
       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
   "`blocks` need to be in the same page."
   [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')))
 
 (defn- sort-blocks-aux

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

@@ -33,7 +33,8 @@
 (defn enable-when-not-component-editing!
   [f]
   (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))))

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

@@ -249,6 +249,20 @@
                                                 (editor-handler/escape-editing)
                                                 (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"
                                     :fn      route-handler/go-to-journals!}
 
@@ -280,7 +294,7 @@
    :graph/open                     {:fn      #(do
                                                 (editor-handler/escape-editing)
                                                 (state/set-state! :ui/open-select :graph-open))
-                                    :binding "mod+shift+g"}
+                                    :binding "alt+shift+g"}
 
    :graph/remove                   {:fn      #(do
                                                 (editor-handler/escape-editing)
@@ -501,6 +515,9 @@
                           :ui/toggle-brackets
                           :go/search-in-page
                           :go/search
+                          :go/electron-find-in-page
+                          :go/electron-jump-to-the-next
+                          :go/electron-jump-to-the-previous
                           :go/backward
                           :go/forward
                           :search/re-index
@@ -553,6 +570,9 @@
     :editor/select-all-blocks
     :go/search
     :go/search-in-page
+    :go/electron-find-in-page
+    :go/electron-jump-to-the-next
+    :go/electron-jump-to-the-previous
     :editor/undo
     :editor/redo
     :editor/copy

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

@@ -46,14 +46,16 @@
 
 (defn normalize-user-keyname
   [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
 (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"
    :ui/toggle-brackets             "Toggle whether to display brackets"
    :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/journals                    "Go to journals"
    :go/backward                    "Backwards"

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

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

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

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

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

@@ -84,7 +84,7 @@
 
 (rum/defc dropdown-content-wrapper [state content 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
      {:class (str class " "
                   (case state
@@ -109,18 +109,38 @@
         (when @open?
           (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
-  [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
-   child])
+   [:span child]
+   (when shortcut
+     [:span.ml-1 (render-keyboard-shortcut shortcut)])])
 
 (rum/defc dropdown-with-links
   [content-fn links {:keys [links-header links-footer] :as opts}]
   (dropdown
    content-fn
    (fn [{:keys [close-fn]}]
-     [:div.py-1.rounded-md.shadow-xs
+     [:.menu-links-wrapper
       (when links-header links-header)
       (for [{:keys [options title icon hr hover-detail item]} (if (fn? links) (links) links)]
         (let [new-options
@@ -138,16 +158,16 @@
                            [:div {:style {:margin-right "8px"
                                           :margin-left  "4px"}} title]]))]
           (if hr
-            [:hr.my-1 {:key "dropdown-hr"}]
+            [:hr.menu-separator {:key "dropdown-hr"}]
             (rum/with-key
-              (menu-link new-options child)
+              (menu-link new-options child nil)
               title))))
       (when links-footer links-footer)])
    opts))
 
 (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}]
   (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)
@@ -156,6 +176,7 @@
     [:button.ui__button
      (merge
       {:type  "button"
+       :title title
        :class (str (util/hiccup->class klass) " " class)}
       (dissoc option :background :class :small? :large?)
       (when href
@@ -254,6 +275,11 @@
   []
   (gdom/getElement "main-content-container"))
 
+(defn focus-element
+  [element]
+  (when-let [element ^js (gdom/getElement element)]
+    (.focus element)))
+
 (defn get-scroll-top []
   (.-scrollTop (main-node)))
 
@@ -418,7 +444,7 @@
                                        (if (and (gobj/get e "shiftKey") on-shift-chosen)
                                          (on-shift-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-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")
        :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]
   (let [default-binding (:binding (get shortcut-config/all-default-keyboard-shortcuts shortcut-name))
         custom-binding  (when (state/shortcuts) (get (state/shortcuts) shortcut-name))
@@ -947,24 +955,17 @@
   ([content-fn]
    (lazy-visible content-fn nil))
   ([content-fn {:keys [trigger-once? _debug-id]
-                :or {trigger-once? false}}]
+                :or {trigger-once? true}}]
    (if (or (util/mobile?) (mobile-util/native-platform?))
      (content-fn)
      (let [[visible? set-visible!] (rum/use-state false)
-           [last-changed-time set-last-changed-time!] (rum/use-state nil)
            inViewState (useInView #js {:rootMargin "100px"
                                        :triggerOnce trigger-once?
                                        :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?))))})
            ref (.-ref inViewState)]
        (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 {
   display: inline-block;
   width: 0;

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

@@ -79,8 +79,7 @@
    (defn electron?
      []
      (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
    (defn mocked-open-dir-path
@@ -338,10 +337,6 @@
    (defn stop-propagation [e]
      (when e (.stopPropagation e))))
 
-#?(:cljs
-   (defn cur-doc-top []
-     (.. js/document -documentElement -scrollTop)))
-
 #?(:cljs
    (defn element-top [elem top]
      (when elem
@@ -490,7 +485,10 @@
 
 #?(:cljs
    (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
   [s]

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

@@ -315,12 +315,14 @@ export const nodePath = Object.assign({}, path, {
   join (input, ...paths) {
     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)
 

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

@@ -1,3 +1,3 @@
 (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)
          :current-graph         (state/get-current-repo)
          :show-brackets         (state/show-brackets?)
+         :enabled-journals      (state/enable-journals?)
+         :enabled-flashcards    (state/enable-flashcards?)
          :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
   (fn []
     (when-let [repo (state/get-current-repo)]
@@ -442,6 +450,9 @@
   [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
   (fn [block-uuid]
     (when-let [block (db-model/get-block-by-uuid block-uuid)]
@@ -456,8 +467,16 @@
 
 (def ^:export insert_block
   (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)
+          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 (and (not sibling) before block-uuid)
                         (let [block (db/entity [:block/uuid block-uuid])
@@ -477,11 +496,13 @@
                     before?)
           new-block (editor-handler/api-insert-new-block!
                       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)))))
 
 (def ^:export insert_batch_block

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

@@ -3,7 +3,7 @@
             [clojure.string :as str]
             [frontend.db :as db]
             [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
 ;; 1. generate query filters
@@ -45,7 +45,8 @@
 ;; Tests
 ;; =====
 
-(deftest block-property-queries
+(defn- block-property-queries-test
+  []
   (load-test-files [{:file/path "journals/2022_02_28.md"
                      :file/content "a:: b
 - b1
@@ -64,7 +65,7 @@ prop-d:: nada"}])
   (testing "Blocks have given property value"
     (is (= #{"b1" "b2"}
            (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"]
            (map (comp first str/split-lines :block/content)
@@ -112,15 +113,27 @@ prop-d:: nada"}])
               (dsl-query "(property prop-d)")))
       "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"
-                     :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/content "foo:: bar"}
+                     :file/content "foo:: #bar\ninteresting:: false"}
                     {: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/content "parent:: child page 2\nfoo:: baz"}])
+                     :file/content "parent:: [[child page 2]]\nfoo:: baz"}])
 
   (is (= ["page1" "page3" "page4"]
          (map :block/name (dsl-query "(page-property parent)")))
@@ -160,7 +173,27 @@ prop-d:: nada"}])
          (map
           :block/name
           (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
   (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
-  "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.
   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
@@ -12,9 +12,7 @@
             [clojure.set :as set]
             [shadow.test :as st]
             [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
 ;; Inherit behavior from default reporter
@@ -47,10 +45,10 @@
 (defn- print-summary
   "Print help summary given args and opts strings"
   [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
   "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-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)]
     (if (:help options)
       (do
@@ -182,3 +180,9 @@ returns selected tests and namespaces to run"
                        "\n\nMultiple options are ANDed. Defaults to running all tests")
         (js/process.exit 0))
       (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}
  ;; :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/settings
  ;; {: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 { observer } from 'mobx-react-lite'
 import React from 'react'
@@ -12,7 +12,6 @@ import {
   ToggleGroupMultipleInput,
 } from '~components/inputs/ToggleGroupInput'
 import { ToggleInput } from '~components/inputs/ToggleInput'
-import { tint } from 'polished'
 import type {
   BoxShape,
   EllipseShape,
@@ -30,11 +29,12 @@ import { LogseqContext } from '~lib/logseq-context'
 export const contextBarActionTypes = [
   // Order matters
   'Edit',
+  'AutoResizing',
   'Swatch',
   'NoFill',
-  'ResetBounds',
   'StrokeType',
   'ScaleLevel',
+  'TextStyle',
   'YoutubeLink',
   'LogseqPortalViewMode',
   'ArrowMode',
@@ -48,8 +48,8 @@ const contextBarActionMapping = new Map<ContextBarActionType, React.FC>()
 
 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'],
   box: ['Swatch', 'NoFill', 'StrokeType'],
   ellipse: ['Swatch', 'NoFill', 'StrokeType'],
@@ -57,13 +57,13 @@ const shapeMapping: Partial<Record<ShapeType, ContextBarActionType[]>> = {
   line: ['Edit', 'Swatch', 'ArrowMode'],
   pencil: ['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]) => {
-    return !types.includes('NoFill') && types.includes('Swatch')
+    return types.includes('NoFill') && types.includes('Swatch')
   })
   .map(([key]) => key) as ShapeType[]
 
@@ -79,9 +79,9 @@ const EditAction = observer(() => {
     <button
       className="tl-contextbar-button"
       type="button"
+      title="Edit"
       onClick={() => {
         app.api.editShape(shape)
-        app.api.zoomToSelection()
         if (shape.props.type === 'logseq-portal') {
           let uuid = shape.props.pageId
           if (shape.props.blockType === 'P') {
@@ -99,26 +99,36 @@ const EditAction = observer(() => {
   )
 })
 
-const ResetBoundsAction = observer(() => {
+const AutoResizingAction = observer(() => {
   const app = useApp<Shape>()
   const shapes = filterShapeByAction<LogseqPortalShape | TextShape | HTMLShape>(
     app.selectedShapesArray,
-    'ResetBounds'
+    'AutoResizing'
   )
 
+  const pressed = shapes.every(s => s.props.isAutoResizing)
+
   return (
-    <button
+    <ToggleInput
+      title="Auto Resize"
+      toggle={shapes.every(s => s.props.type === 'logseq-portal')}
       className="tl-contextbar-button"
-      type="button"
-      onClick={() => {
+      pressed={pressed}
+      onPressedChange={v => {
         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()
       }}
     >
       <TablerIcon name="dimensions" />
-    </button>
+    </ToggleInput>
   )
 })
 
@@ -142,6 +152,7 @@ const LogseqPortalViewModeAction = observer(() => {
   ]
   return (
     <ToggleGroupInput
+      title="View Mode"
       options={ViewModeOptions}
       value={collapsed ? '1' : '0'}
       onValueChange={v => {
@@ -186,6 +197,7 @@ const ScaleLevelAction = observer(() => {
   ]
   return (
     <SelectInput
+      title="Scale Level"
       options={sizeOptions}
       value={scaleLevel}
       onValueChange={v => {
@@ -208,6 +220,7 @@ const OpenPageAction = observer(() => {
   return (
     <span className="flex gap-1">
       <button
+        title="Open Page in Right Sidebar"
         className="tl-contextbar-button"
         type="button"
         onClick={() => handlers?.sidebarAddBlock(pageId, blockType === 'B' ? 'block' : 'page')}
@@ -215,6 +228,7 @@ const OpenPageAction = observer(() => {
         <TablerIcon name="layout-sidebar-right" />
       </button>
       <button
+        title="Open Page"
         className="tl-contextbar-button"
         type="button"
         onClick={() => handlers?.redirectToPage(pageId)}
@@ -235,8 +249,14 @@ const YoutubeLinkAction = observer(() => {
 
   return (
     <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
+        title="Open YouTube Link"
         className="tl-contextbar-button"
         type="button"
         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)
 
   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" />}
     </ToggleInput>
   )
@@ -276,14 +301,8 @@ const SwatchAction = observer(() => {
   const handleChange = React.useMemo(() => {
     let latestValue = ''
     const handler: React.ChangeEventHandler<HTMLInputElement> = e => {
-      const strokeColor = tint(0.4, latestValue)
       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)
     }
@@ -293,7 +312,7 @@ const SwatchAction = observer(() => {
   }, [])
 
   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(() => {
@@ -321,6 +340,7 @@ const StrokeTypeAction = observer(() => {
 
   return (
     <ToggleGroupInput
+      title="Stroke Type"
       options={StrokeTypeOptions}
       value={value}
       onValueChange={v => {
@@ -364,6 +384,7 @@ const ArrowModeAction = observer(() => {
 
   return (
     <ToggleGroupMultipleInput
+      title="Arrow Head"
       options={StrokeTypeOptions}
       value={value}
       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('ResetBounds', ResetBoundsAction)
+contextBarActionMapping.set('AutoResizing', AutoResizingAction)
 contextBarActionMapping.set('LogseqPortalViewMode', LogseqPortalViewModeAction)
 contextBarActionMapping.set('ScaleLevel', ScaleLevelAction)
 contextBarActionMapping.set('OpenPage', OpenPageAction)
@@ -388,6 +454,7 @@ contextBarActionMapping.set('NoFill', NoFillAction)
 contextBarActionMapping.set('Swatch', SwatchAction)
 contextBarActionMapping.set('StrokeType', StrokeTypeAction)
 contextBarActionMapping.set('ArrowMode', ArrowModeAction)
+contextBarActionMapping.set('TextStyle', TextStyleAction)
 
 const getContextBarActionTypes = (type: ShapeType) => {
   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'
 
 interface ToggleInputProps extends React.HTMLAttributes<HTMLElement> {
+  toggle?: boolean
   pressed: boolean
   onPressedChange: (value: boolean) => void
 }
 
-export function ToggleInput({ pressed, onPressedChange, className, ...rest }: ToggleInputProps) {
+export function ToggleInput({ toggle = true, pressed, onPressedChange, className, ...rest }: ToggleInputProps) {
   return (
     <Toggle.Root
       {...rest}
+      data-toggle={toggle}
       className={'tl-toggle-input' + (className ? ' ' + className : '')}
       pressed={pressed}
       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],
     borderRadius: 2,
     stroke: '#000000',
-    fill: '#ffffff',
+    fill: 'var(--ls-secondary-background-color)',
     noFill: false,
     strokeType: 'line',
     strokeWidth: 2,
@@ -87,6 +87,6 @@ export class BoxShape extends TLBoxShape<BoxShapeProps> {
       props.size[1] = Math.max(props.size[1], 1)
     }
     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],
     radius: 4,
     stroke: '#000000',
-    fill: '#ffffff',
+    fill: 'var(--ls-secondary-background-color)',
     noFill: false,
     strokeType: 'line',
     strokeWidth: 2,
@@ -50,6 +50,6 @@ export class DotShape extends TLDotShape<DotShapeProps> {
 
   validateProps = (props: Partial<DotShapeProps>) => {
     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],
     size: [100, 100],
     stroke: '#000000',
-    fill: '#ffffff',
+    fill: 'var(--ls-secondary-background-color)',
     noFill: false,
     strokeType: 'line',
     strokeWidth: 2,
@@ -74,7 +74,7 @@ export class EllipseShape extends TLEllipseShape<EllipseShapeProps> {
       props.size[0] = Math.max(props.size[0], 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(() => {
       if (this.props.size[1] === 0) {
-        this.onResetBounds()
+        this.onResetBounds({ zoom: app.viewport.camera.zoom })
         app.persist(true)
       }
     }, [])
@@ -146,6 +146,6 @@ export class HTMLShape extends TLBoxShape<HTMLShapeProps> {
       props.size[0] = Math.max(props.size[0], 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>) => {
-    props = withClampedStyles(props)
+    props = withClampedStyles(this, props)
     if (props.strokeWidth !== undefined) props.strokeWidth = Math.max(props.strokeWidth, 1)
     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] },
     },
     stroke: 'var(--ls-primary-text-color, #000)',
-    fill: '#ffffff',
+    fill: 'var(--ls-secondary-background-color)',
     noFill: true,
     strokeType: 'line',
     strokeWidth: 1,
@@ -145,7 +145,7 @@ export class LineShape extends TLLineShape<LineShapeProps> {
   })
 
   validateProps = (props: Partial<LineShapeProps>) => {
-    return withClampedStyles(props)
+    return withClampedStyles(this, props)
   }
 
   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,
     compact: false,
     scaleLevel: 'md',
+    isAutoResizing: true,
   }
 
   hideRotateHandle = true
@@ -159,7 +160,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
   constructor(props = {} as Partial<LogseqPortalShapeProps>) {
     super(props)
     makeObservable(this)
-    if (props.collapsed || props.compact) {
+    if (props.collapsed) {
       Object.assign(this.canResize, [true, false])
     }
     if (props.size?.[1] === 0) {
@@ -194,7 +195,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
       this.canResize[1] = !collapsed
       this.update({
         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,
       })
     }
@@ -248,12 +249,9 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
     return size
   }
 
-  shouldAutoResizeHeight() {
-    return this.props.blockType === 'B' && this.props.compact
-  }
-
   getHeaderHeight() {
-    return this.props.compact ? 0 : HEADER_HEIGHT
+    const scale = levelToScale[this.props.scaleLevel ?? 'md']
+    return this.props.compact ? 0 : HEADER_HEIGHT * scale
   }
 
   getAutoResizeHeight() {
@@ -286,9 +284,10 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
 
     let height = bounds.height
 
-    if (this.shouldAutoResizeHeight()) {
+    if (this.props.isAutoResizing) {
       height = this.getAutoResizeHeight() ?? height
     }
+
     return this.update({
       point: [bounds.minX, bounds.minY],
       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({
           actionIcon: 'circle-plus',
           onChosen: () => {
@@ -607,8 +606,9 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
     const { Page, Block } = renderers
 
     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) {
           this.update({
             size: [this.props.size[0], newHeight],
@@ -616,7 +616,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
           app.persist(true)
         }
       }
-    }, [innerHeight, this.props.compact])
+    }, [innerHeight, this.props.isAutoResizing])
 
     React.useEffect(() => {
       if (!this.initialHeightCalculated) {
@@ -632,7 +632,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
         ref={cpRefContainer}
         className="tl-logseq-cp-container"
         style={{
-          overflow: this.props.compact ? 'visible' : 'auto',
+          overflow: this.props.isAutoResizing ? 'visible' : 'auto',
         }}
       >
         {this.props.blockType === 'B' && this.props.compact ? (
@@ -692,7 +692,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
         })
         return () => {
           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(() => {
     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>) => {
@@ -803,7 +803,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
       props.size[0] = Math.max(props.size[0], 240 * scale)
       props.size[1] = Math.max(props.size[1], HEADER_HEIGHT * scale)
     }
-    return withClampedStyles(props)
+    return withClampedStyles(this, props)
   }
 
   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 */
-import * as React from 'react'
 import { getStroke } from 'perfect-freehand'
 import { SvgPathUtils, TLDrawShape, TLDrawShapeProps } from '@tldraw/core'
 import { SVGContainer, TLComponentProps } from '@tldraw/react'
@@ -71,7 +70,7 @@ export class PenShape extends TLDrawShape<PenShapeProps> {
   })
 
   validateProps = (props: Partial<PenShapeProps>) => {
-    props = withClampedStyles(props)
+    props = withClampedStyles(this, props)
     if (props.strokeWidth !== undefined) props.strokeWidth = Math.max(props.strokeWidth, 1)
     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>) => {
-    props = withClampedStyles(props)
+    props = withClampedStyles(this, props)
     if (props.strokeWidth !== undefined) props.strokeWidth = Math.max(props.strokeWidth, 1)
     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,
     isFlippedY: false,
     stroke: '#000000',
-    fill: '#ffffff',
+    fill: 'var(--ls-secondary-background-color)',
     noFill: false,
     strokeType: 'line',
     strokeWidth: 2,
@@ -72,7 +72,7 @@ export class PolygonShape extends TLPolygonShape<PolygonShapeProps> {
 
   validateProps = (props: Partial<PolygonShapeProps>) => {
     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 type { SizeLevel } from '~lib'
 import { CustomStyleProps, withClampedStyles } from './style-props'
+import { TextAreaUtils } from './text/TextAreaUtils'
 
 export interface TextShapeProps extends TLTextShapeProps, CustomStyleProps {
   borderRadius: number
   fontFamily: string
   fontSize: number
   fontWeight: number
+  italic: boolean
   lineHeight: number
   padding: number
   type: 'text'
@@ -41,6 +43,7 @@ export class TextShape extends TLTextShape<TextShapeProps> {
     lineHeight: 1.2,
     fontSize: 20,
     fontWeight: 400,
+    italic: false,
     padding: 4,
     fontFamily: "var(--ls-font-family), 'Helvetica Neue', Helvetica, Arial, sans-serif",
     borderRadius: 0,
@@ -54,7 +57,17 @@ export class TextShape extends TLTextShape<TextShapeProps> {
 
   ReactComponent = observer(({ events, isErasing, isEditing, onEditingEnd }: TLComponentProps) => {
     const {
-      props: { opacity, fontFamily, fontSize, fontWeight, lineHeight, text, stroke, padding },
+      props: {
+        opacity,
+        fontFamily,
+        fontSize,
+        fontWeight,
+        italic,
+        lineHeight,
+        text,
+        stroke,
+        padding,
+      },
     } = this
     const rInput = React.useRef<HTMLTextAreaElement>(null)
 
@@ -75,39 +88,35 @@ export class TextShape extends TLTextShape<TextShapeProps> {
     }, [])
 
     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}
           style={{
             fontFamily,
+            fontStyle: italic ? 'italic' : 'normal',
             fontSize,
             fontWeight,
             padding,
@@ -217,7 +227,7 @@ export class TextShape extends TLTextShape<TextShapeProps> {
   @action setScaleLevel = async (v?: SizeLevel) => {
     this.update({
       scaleLevel: v,
-      fontSize: levelToScale[v ?? 'md']
+      fontSize: levelToScale[v ?? 'md'],
     })
     this.onResetBounds()
   }
@@ -234,6 +244,7 @@ export class TextShape extends TLTextShape<TextShapeProps> {
         rx={borderRadius}
         ry={borderRadius}
         fill="transparent"
+        stroke="none"
       />
     )
   })
@@ -242,7 +253,7 @@ export class TextShape extends TLTextShape<TextShapeProps> {
     if (props.isSizeLocked || this.props.isSizeLocked) {
       // props.size = this.getAutoSizedBoundingBox(props)
     }
-    return withClampedStyles(props)
+    return withClampedStyles(this, props)
   }
 
   // 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 })
   }
 
-  ReactComponent = observer(({ events, isErasing, isEditing }: TLComponentProps) => {
+  ReactComponent = observer(({ events, isErasing, isEditing, isSelected }: TLComponentProps) => {
     return (
       <HTMLContainer
         style={{
@@ -56,7 +56,7 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
         <div
           className="rounded-lg w-full h-full relative overflow-hidden shadow-xl"
           style={{
-            pointerEvents: isEditing ? 'all' : 'none',
+            pointerEvents: (isEditing || isSelected) ? 'all' : 'none',
             userSelect: 'none',
           }}
         >
@@ -120,7 +120,7 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
         size: [w, h],
       },
     } = this
-    return <rect width={w} height={h} fill="transparent" />
+    return <rect width={w} height={h} fill="transparent" stroke="none" />
   })
 
   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[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 { TextShape } from './TextShape'
 import { YouTubeShape } from './YouTubeShape'
+import type { PenShape } from './PenShape'
 
 export type Shape =
   | BoxShape
@@ -21,6 +22,7 @@ export type Shape =
   | ImageShape
   | VideoShape
   | LineShape
+  | PenShape
   | PencilShape
   | PolygonShape
   | 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 {
   stroke: string
   fill: string
@@ -7,8 +12,16 @@ export interface CustomStyleProps {
   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.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
 }

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

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

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

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

@@ -139,6 +139,19 @@ const searchHandler = q => {
 export default function App() {
   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 (
     <div className={`h-screen w-screen`}>
       <ThemeSwitcher theme={theme} setTheme={setTheme} />
@@ -157,7 +170,7 @@ export default function App() {
           saveAsset: fileToBase64,
           makeAssetUrl: a => a,
         }}
-        model={documentModel}
+        model={model}
         onPersist={onPersist}
       />
     </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 ReactDOM from 'react-dom'
 
 import App from './App'
 
+import './logseq-styles.css'
 import './index.css'
 
 // 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
   }
 
-  /**
-   * 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 => {
     this.app.transition('select').selectedTool.transition('editingShape', { shape })
     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: {} })],
   ])
 
-  @observable currentPageId = 'page'
+  @computed get currentPageId() {
+    return this.pages.keys().next().value
+  }
 
   @computed get currentPage(): TLPage<S, K> {
     return this.getPageById(this.currentPageId)
@@ -295,11 +297,6 @@ export class TLApp<
     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 {
     pages.forEach(page => this.pages.set(page.id, page))
     this.persist()
@@ -484,7 +481,8 @@ export class TLApp<
     return this
   }
 
-  readonly clearEditingShape = (): this => {
+  readonly clearEditingState = (): this => {
+    this.selectedTool.transition('idle')
     return this.setEditingShape()
   }
 
@@ -534,7 +532,7 @@ export class TLApp<
       this.selectionRotation = 0
     }
     if (shapes.length === 0) {
-      this.clearEditingShape()
+      this.setEditingShape()
     }
     return this
   }
@@ -642,11 +640,10 @@ export class TLApp<
     } = this
     return currentPage.shapes.filter(shape => {
       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)
       )
     })
   }

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