Browse Source

Merge remote-tracking branch 'upstream/master' into whiteboards

Peng Xiao 3 years ago
parent
commit
d3dae5fddc
50 changed files with 962 additions and 700 deletions
  1. 1 1
      .github/workflows/build.yml
  2. 1 1
      CODE_OF_CONDUCT.md
  3. 3 1
      README.md
  4. 2 2
      android/app/build.gradle
  5. 63 45
      deps/graph-parser/src/logseq/graph_parser/block.cljs
  6. 1 1
      deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs
  7. 1 1
      deps/graph-parser/src/logseq/graph_parser/util.cljs
  8. 7 7
      deps/graph-parser/test/logseq/graph_parser/block_test.cljs
  9. 23 5
      docs/develop-logseq.md
  10. 2 0
      e2e-tests/basic.spec.ts
  11. 1 1
      e2e-tests/page-search.spec.ts
  12. 4 4
      ios/App/App.xcodeproj/project.pbxproj
  13. 7 2
      ios/App/App/FileContainer.swift
  14. 2 1
      libs/src/LSPlugin.ts
  15. 1 1
      package.json
  16. 1 1
      resources/package.json
  17. 46 41
      src/main/frontend/commands.cljs
  18. 16 15
      src/main/frontend/components/block.cljs
  19. 6 1
      src/main/frontend/components/block.css
  20. 3 4
      src/main/frontend/components/datetime.cljs
  21. 243 192
      src/main/frontend/components/editor.cljs
  22. 3 3
      src/main/frontend/components/header.cljs
  23. 1 4
      src/main/frontend/components/journal.cljs
  24. 4 4
      src/main/frontend/components/onboarding.cljs
  25. 13 8
      src/main/frontend/components/plugins.cljs
  26. 1 3
      src/main/frontend/components/reference.cljs
  27. 1 1
      src/main/frontend/components/search.cljs
  28. 2 2
      src/main/frontend/components/widgets.cljs
  29. 1 1
      src/main/frontend/core.cljs
  30. 34 0
      src/main/frontend/db/model.cljs
  31. 14 12
      src/main/frontend/dicts.cljc
  32. 30 13
      src/main/frontend/extensions/html_parser.cljs
  33. 50 40
      src/main/frontend/extensions/srs.cljs
  34. 5 3
      src/main/frontend/extensions/srs/handler.cljs
  35. 1 1
      src/main/frontend/extensions/zotero/handler.cljs
  36. 1 1
      src/main/frontend/fs/watcher_handler.cljs
  37. 201 156
      src/main/frontend/handler/editor.cljs
  38. 2 1
      src/main/frontend/handler/events.cljs
  39. 29 9
      src/main/frontend/handler/page.cljs
  40. 33 5
      src/main/frontend/search.cljs
  41. 38 46
      src/main/frontend/state.cljs
  42. 19 35
      src/main/frontend/ui.cljs
  43. 5 0
      src/main/frontend/util.cljc
  44. 4 3
      src/main/frontend/util/cursor.cljs
  45. 10 8
      src/main/frontend/util/text.cljs
  46. 1 1
      src/main/frontend/version.cljs
  47. 1 1
      src/test/frontend/db/query_dsl_test.cljs
  48. 8 1
      src/test/frontend/extensions/calc_test.cljc
  49. 3 3
      src/test/frontend/util/text_test.cljs
  50. 13 8
      yarn.lock

+ 1 - 1
.github/workflows/build.yml

@@ -179,7 +179,7 @@ jobs:
         run: xvfb-run -- yarn e2e-test
         env:
           CI: true
-          DEBUG: "pw:test"
+          DEBUG: "pw:api"
 
       - name: Save test artifacts
         if: ${{ failure() }}

+ 1 - 1
CODE_OF_CONDUCT.md

@@ -13,4 +13,4 @@ This Code of Conduct applies within all community spaces, and also applies when
 - Please respect each other. Do not dismiss, abuse, harass, attack, insult, or discriminate against others.
 - Likewise, any spamming, trolling, flaming, baiting or other attention-stealing behavior is not welcome.
 - Respect that people have differences of opinion and that every design or implementation choice carries a trade-off and numerous costs. There is seldom a right answer.
-- If you feel being harassed or made uncomfortable by a community member, please report the incident(s) either by contacting the moderators on the [Discord](https://discord.gg/KpN4eHY) channel, Tienson Qin (@tiensonqin) on GitHub, or the official [Twitter](https://twitter.com/logseq). We will work with you to resolve the issue promptly.
+- If you feel being harassed or made uncomfortable by a community member, please report the incident(s) either by contacting the moderators on the [Forum](https://discuss.logseq.com), [Discord](https://discord.gg/KpN4eHY) channel, Tienson Qin (@tiensonqin) on GitHub, or the official [Twitter](https://twitter.com/logseq). We will work with you to resolve the issue promptly.

+ 3 - 1
README.md

@@ -3,6 +3,7 @@
 [![latest release version](https://img.shields.io/github/v/release/logseq/logseq)](https://github.com/logseq/logseq/releases)
 [![License](https://img.shields.io/github/license/logseq/logseq?color=blue)](https://github.com/logseq/logseq/blob/master/LICENSE.md)
 [![Twitter follow](https://img.shields.io/badge/follow-%40logseq-blue.svg?style=flat&logo=twitter)](https://twitter.com/logseq)
+[![forum](https://img.shields.io/badge/forum-Logseq-blue.svg?style=flat&logo=discourse)](https://discuss.logseq.com)
 [![discord](https://img.shields.io/discord/725182569297215569?label=discord&logo=Discord&color=blue)](https://discord.gg/KpN4eHY)
 [![total](https://opencollective.com/logseq/tiers/badge.svg?color=blue)](https://opencollective.com/logseq)
 
@@ -68,7 +69,8 @@ Logseq is also made possible by the following projects:
 
 - Our blog: https://logseq.com/blog - Please be sure to visit our [About page](https://logseq.com/blog/about) for the latest updates of the app
 - Twitter: https://twitter.com/logseq
-- Discord: https://discord.gg/KpN4eHY - Where we answer questions, discuss workflows and share tips
+- Forum: https://discuss.logseq.com - Where we answer questions, discuss workflows and share tips
+- Discord: https://discord.gg/KpN4eHY
 - 中文 Discord:https://discord.gg/xYqcrXWymg
 - Github: https://github.com/logseq/logseq - everyone is encouraged to report issues!
 

+ 2 - 2
android/app/build.gradle

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

+ 63 - 45
deps/graph-parser/src/logseq/graph_parser/block.cljs

@@ -139,52 +139,78 @@
    (vector? block)
    (= "Timestamp" (first block))))
 
-;; TODO: we should move this to mldoc
-(defn extract-properties
-  [format properties user-config]
-  (when (seq properties)
-    (let [properties (seq properties)
-          page-refs (->>
+(defn- get-page-ref-names-from-properties
+  [format properties]
+  (let [page-refs (->>
                      properties
                      (remove (fn [[k _]]
                                (contains? #{:background-color :background_color} (keyword k))))
                      (map last)
                      (map (fn [v]
-                            (when (and (string? v)
-                                       (not (gp-mldoc/link? format v)))
+                            (cond
+                              (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})]
                                 (if (coll? result)
                                   (map text/page-ref-un-brackets! result)
-                                  [])))))
-                     (apply concat)
-                     (remove string/blank?))
+                                  []))
+
+                              (coll? v)
+                              (map (fn [s]
+                                     (when-not (and (string? v)
+                                                    (gp-mldoc/link? format v))
+                                       (text/page-ref-un-brackets! s))) v)
+
+                              :else
+                              nil)))
+                     (apply concat))
+        property-keys-page-refs (some->> properties
+                                         (map (comp name first))
+                                         (remove string/blank?)
+                                         (distinct))]
+    (->> (concat page-refs property-keys-page-refs)
+         (remove string/blank?)
+         distinct)))
+
+(defn- invalid-property-key?
+  [s]
+  (string/includes? s "`"))
+
+(defn extract-properties
+  [format properties user-config]
+  (when (seq properties)
+    (let [properties (seq properties)
+          properties (into {} properties)
+          page-refs (get-page-ref-names-from-properties format properties)
           properties (->> properties
                           (map (fn [[k v]]
                                  (let [k (-> (string/lower-case (name k))
                                              (string/replace " " "-")
-                                             (string/replace "_" "-"))
-                                       k (if (contains? #{"custom_id" "custom-id"} k)
-                                           "id"
-                                           k)
-                                       v (if (coll? v)
-                                           (remove string/blank? v)
-                                           (cond
-                                             (string/blank? v)
-                                             nil
-                                             (and (= (keyword k) :file-path)
-                                                  (string/starts-with? v "file:"))
-                                             v
-                                             :else
-                                             (text/parse-property format k v user-config)))
-                                       k (keyword k)
-                                       v (if (and
-                                              (string? v)
-                                              (contains? #{:alias :aliases :tags} k))
-                                           (set [v])
-                                           v)
-                                       v (if (coll? v) (set v) v)]
-                                   [k v])))
+                                             (string/replace "_" "-")
+                                             (string/replace #"[\"|^|(|)|{|}]+" ""))]
+                                   (when-not (invalid-property-key? k)
+                                     (let [k (if (contains? #{"custom_id" "custom-id"} k)
+                                               "id"
+                                               k)
+                                           v (if (coll? v)
+                                               (remove string/blank? v)
+                                               (cond
+                                                 (string/blank? v)
+                                                 nil
+                                                 (and (= (keyword k) :file-path)
+                                                      (string/starts-with? v "file:"))
+                                                 v
+                                                 :else
+                                                 (text/parse-property format k v user-config)))
+                                           k (keyword k)
+                                           v (if (and
+                                                  (string? v)
+                                                  (contains? #{:alias :aliases :tags} k))
+                                               (set [v])
+                                               v)
+                                           v (if (coll? v) (set v) v)]
+                                       [k v])))))
                           (remove #(nil? (second %))))]
       {:properties (into {} properties)
        :properties-order (map first properties)
@@ -437,17 +463,8 @@
       (d/squuid)))
 
 (defn get-page-refs-from-properties
-  [properties db date-formatter]
-  (let [page-refs (mapcat (fn [v] (cond
-                                   (coll? v)
-                                   v
-
-                                   (text/page-ref? v)
-                                   [(text/page-ref-un-brackets! v)]
-
-                                   :else
-                                   nil)) (vals properties))
-        page-refs (remove string/blank? page-refs)]
+  [format properties db date-formatter]
+  (let [page-refs (get-page-ref-names-from-properties format properties)]
     (map (fn [page] (page-name->map page true db true date-formatter)) page-refs)))
 
 (defn- with-page-block-refs
@@ -461,6 +478,7 @@
 (defn- with-pre-block-if-exists
   [blocks body pre-block-properties encoded-content {:keys [supported-formats db date-formatter]}]
   (let [first-block (first blocks)
+        format (or (:block/format first-block) :markdown)
         first-block-start-pos (get-in first-block [:block/meta :start_pos])
 
         ;; Add pre-block
@@ -471,7 +489,7 @@
                    (let [content (utf8/substring encoded-content 0 first-block-start-pos)
                          {:keys [properties properties-order]} pre-block-properties
                          id (get-custom-id-or-new-id {:properties properties})
-                         property-refs (->> (get-page-refs-from-properties properties db date-formatter)
+                         property-refs (->> (get-page-refs-from-properties format properties db date-formatter)
                                             (map :block/original-name))
                          block {:uuid id
                                 :content content

+ 1 - 1
deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs

@@ -142,7 +142,7 @@
   ;; only increase over time as the docs graph rarely has deletions
   (testing "Counts"
     (is (= 211 (count files)) "Correct file count")
-    (is (= 40945 (count (d/datoms db :eavt))) "Correct datoms count")
+    (is (= 44212 (count (d/datoms db :eavt))) "Correct datoms count")
 
     (is (= 3600
            (ffirst

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

@@ -116,7 +116,7 @@
                       (remove-boundary-slashes)
                       (path-normalize))]
      (if replace-slash?
-       (string/replace page #"/" "%2A")
+       (string/replace page #"/" "%2F")
        page))))
 
 (defn page-name-sanity-lc

+ 7 - 7
deps/graph-parser/test/logseq/graph_parser/block_test.cljs

@@ -25,12 +25,12 @@
     [["file-path" "file:///home/x, y.pdf"]] {:file-path "file:///home/x, y.pdf"})
 
   (are [x y] (= (vec (:page-refs (gp-block/extract-properties :markdown x {}))) y)
-    [["year" "1000"]] []
-    [["year" "\"1000\""]] []
-    [["foo" "[[bar]] test"]] ["bar" "test"]
-    [["foo" "[[bar]] test [[baz]]"]] ["bar" "test" "baz"]
-    [["foo" "[[bar]] test [[baz]] [[nested [[baz]]]]"]] ["bar" "test" "baz" "nested [[baz]]"]
-    [["foo" "#bar, #baz"]] ["bar" "baz"]
-    [["foo" "[[nested [[page]]]], test"]] ["nested [[page]]" "test"]))
+    [["year" "1000"]] ["year"]
+    [["year" "\"1000\""]] ["year"]
+    [["foo" "[[bar]] test"]] ["bar" "test" "foo"]
+    [["foo" "[[bar]] test [[baz]]"]] ["bar" "test" "baz" "foo"]
+    [["foo" "[[bar]] test [[baz]] [[nested [[baz]]]]"]] ["bar" "test" "baz" "nested [[baz]]" "foo"]
+    [["foo" "#bar, #baz"]] ["bar" "baz" "foo"]
+    [["foo" "[[nested [[page]]]], test"]] ["nested [[page]]" "test" "foo"]))
 
 #_(cljs.test/run-tests)

+ 23 - 5
docs/develop-logseq.md

@@ -24,12 +24,14 @@ yarn watch
 
 Then open the browser <http://localhost:3001>.
 
-### Production
+### Production Build
 
 ```bash
 yarn release
 ```
 
+The released files will be at `resources/` directory.
+
 ## Desktop app development
 
 ### Development
@@ -37,23 +39,39 @@ yarn release
 1. Install npm packages for building the desktop app
 
 ``` bash
-yarn install && cd static && yarn install && cd ..
+yarn install
 ```
+
 2. Compile to JavaScript and open the dev app
 
 ```bash
 yarn watch
-# Wait until watch is finished building and then in a different shell
-# If you have opened desktop logseq, you should close it. Otherwise, the following command will fail.
+# Wait until watch reports `Build Completed.` for `:electron` and `:app`.
+# Then, run the following command in a different shell.
+# If you have opened desktop logseq, you should close it. Otherwise, this command will fail.
 yarn dev-electron-app
 ```
 
 Alternatively, run `bb dev:electron-start` to do this step with one command. To
 download bb, see https://github.com/babashka/babashka#installation.
 
-### Production
+3. (Optional) Update dependencies if your are updating from an old branch
+
+```bash
+# pull new changes
+git pull
+
+cd static && yarn install && cd ..
+```
+
+Here `static/` is generated by `yarn watch` command.
+
+### Production Build
+
 Build a release:
 
 ```bash
 yarn release-electron
 ```
+
+The final released binaries or installers will be at `static/out/`.

+ 2 - 0
e2e-tests/basic.spec.ts

@@ -203,6 +203,8 @@ test('auto completion and auto pair', async ({ page, block }) => {
   await block.mustType('type (', { toBe: 'type ()' })
   await block.mustType('(', { toBe: 'type (())' })
 
+  await block.escapeEditing() // escape any popup from `(())`
+
   // [[  #3251
   await block.clickNext()
 

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

@@ -157,6 +157,6 @@ async function alias_test(page: Page, page_name: string, search_kws: string[]) {
   // TODO: search clicking (alias property)
 }
 
-test('page diacritic alias', async ({ page }) => {
+test.skip('page diacritic alias', async ({ page }) => {
   await alias_test(page, "ü", ["ü", "ü", "Ü"])
 })

+ 4 - 4
ios/App/App.xcodeproj/project.pbxproj

@@ -542,7 +542,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.7.5;
+				MARKETING_VERSION = 0.7.6;
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -568,7 +568,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.7.5;
+				MARKETING_VERSION = 0.7.6;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -593,7 +593,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.7.5;
+				MARKETING_VERSION = 0.7.6;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
@@ -620,7 +620,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.7.5;
+				MARKETING_VERSION = 0.7.6;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 7 - 2
ios/App/App/FileContainer.swift

@@ -21,8 +21,13 @@ public class FileContainer: CAPPlugin, UIDocumentPickerDelegate {
 
     @objc func ensureDocuments(_ call: CAPPluginCall) {
         
-        validateDocuments(at: self.iCloudContainerUrl!)
-        validateDocuments(at: self.localContainerUrl!)
+        if self.iCloudContainerUrl != nil {
+            validateDocuments(at: self.iCloudContainerUrl!)
+        }
+        
+        if self.localContainerUrl != nil {
+            validateDocuments(at: self.localContainerUrl!)
+        }
         
         call.resolve(["path": [self.iCloudContainerUrl?.path as Any,
                                self.localContainerUrl?.path as Any]])

+ 2 - 1
libs/src/LSPlugin.ts

@@ -196,6 +196,7 @@ export interface PageEntity {
   children?: Array<PageEntity>
   format?: 'markdown' | 'org'
   journalDay?: number
+  updatedAt?: number
 }
 
 export type BlockIdentity = BlockUUID | Pick<BlockEntity, 'uuid'>
@@ -622,7 +623,7 @@ export interface IEditorProxy extends Record<string, any> {
 
   renamePage: (oldName: string, newName: string) => Promise<void>
 
-  getAllPages: (repo?: string) => Promise<any>
+  getAllPages: (repo?: string) => Promise<PageEntity[] | null>
 
   prependBlockInPage: (
     page: PageIdentity,

+ 1 - 1
package.json

@@ -115,11 +115,11 @@
         "react-grid-layout": "0.16.6",
         "react-icon-base": "^2.1.2",
         "react-icons": "2.2.7",
+        "react-intersection-observer": "^9.3.5",
         "react-resize-context": "3.0.0",
         "react-textarea-autosize": "8.3.3",
         "react-tippy": "1.4.0",
         "react-transition-group": "4.3.0",
-        "react-visibility-sensor": "^5.1.1",
         "reakit": "0.11.1",
         "remove-accents": "0.4.2",
         "send-intent": "3.0.11",

+ 1 - 1
resources/package.json

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

+ 46 - 41
src/main/frontend/commands.cljs

@@ -22,11 +22,8 @@
 
 ;; TODO: move to frontend.handler.editor.commands
 
-(defonce *show-commands (atom false))
-(defonce *slash-caret-pos (atom nil))
-(defonce *show-block-commands (atom false))
 (defonce angle-bracket "<")
-(defonce *angle-bracket-caret-pos (atom nil))
+(defonce colon ":")
 (defonce *current-command (atom nil))
 
 (def query-doc
@@ -302,16 +299,20 @@
 
 (defonce *matched-block-commands (atom (block-commands-map)))
 
-(defn restore-state
-  [restore-slash-caret-pos?]
-  (when restore-slash-caret-pos?
-    (reset! *slash-caret-pos nil))
-  (reset! *show-commands false)
-  (reset! *angle-bracket-caret-pos nil)
-  (reset! *show-block-commands false)
-  (reset! *matched-commands @*initial-commands)
+(defn reinit-matched-commands!
+  []
+  (reset! *matched-commands @*initial-commands))
+
+(defn reinit-matched-block-commands!
+  []
   (reset! *matched-block-commands (block-commands-map)))
 
+(defn restore-state
+  []
+  (state/clear-editor-action!)
+  (reinit-matched-commands!)
+  (reinit-matched-block-commands!))
+
 (defn insert!
   [id value
    {:keys [last-pattern postfix-fn backward-pos forward-pos end-pattern backward-truncate-number]
@@ -327,19 +328,22 @@
                            (+ current-pos i)))
                        current-pos)
           orig-prefix (subs edit-content 0 current-pos)
-          space? (when (and last-pattern orig-prefix)
-                   (let [s (when-let [last-index (string/last-index-of orig-prefix last-pattern)]
-                             (gp-util/safe-subs orig-prefix 0 last-index))]
-                     (not
-                      (or
-                       (and s
-                            (string/ends-with? s "(")
-                            (or (string/starts-with? last-pattern "((")
-                                (string/starts-with? last-pattern "[[")))
-                       (and s (string/starts-with? s "{{embed"))))))
-          space? (if (and space? (string/starts-with? last-pattern "#[["))
-                   false
-                   space?)
+          space? (let [space? (when (and last-pattern orig-prefix)
+                                (let [s (when-let [last-index (string/last-index-of orig-prefix last-pattern)]
+                                          (gp-util/safe-subs orig-prefix 0 last-index))]
+                                  (not
+                                   (or
+                                    (and s
+                                         (string/ends-with? s "(")
+                                         (or (string/starts-with? last-pattern "((")
+                                             (string/starts-with? last-pattern "[[")))
+                                    (and s (string/starts-with? s "{{embed"))
+                                    (and last-pattern
+                                         (or (string/ends-with? last-pattern "::")
+                                             (string/starts-with? last-pattern "::")))))))]
+                   (if (and space? (string/starts-with? last-pattern "#[["))
+                     false
+                     space?))
           prefix (cond
                    (and backward-truncate-number (integer? backward-truncate-number))
                    (str (gp-util/safe-subs orig-prefix 0 (- (count orig-prefix) backward-truncate-number))
@@ -366,12 +370,13 @@
                       (str prefix postfix))
           new-pos (- (count prefix)
                      (or backward-pos 0))]
-      (state/set-block-content-and-last-pos! id new-value new-pos)
-      (cursor/move-cursor-to input
-                             (if (and (or backward-pos forward-pos)
-                                      (not= end-pattern "]]"))
-                               new-pos
-                               (inc new-pos))))))
+      (when-not (string/blank? new-value)
+        (state/set-block-content-and-last-pos! id new-value new-pos)
+        (cursor/move-cursor-to input
+                               (if (and (or backward-pos forward-pos)
+                                        (not= end-pattern "]]"))
+                                 new-pos
+                                 (inc new-pos)))))))
 
 (defn simple-insert!
   [id value
@@ -464,13 +469,13 @@
     (let [type (:type option)
           input (gdom/getElement input-id)
           beginning-of-line? (or (cursor/beginning-of-line? input)
-                                 (= 1 (:pos @*angle-bracket-caret-pos)))
+                                 (= 1 (:pos (:pos (state/get-editor-action-data)))))
           value (if (and (contains? #{"block" "properties"} type)
                          (not beginning-of-line?))
                   (str "\n" value)
                   value)]
       (insert! input-id value option)
-      (reset! *show-commands false))))
+      (state/clear-editor-action!))))
 
 (defmethod handle-step :editor/cursor-back [[_ n]]
   (when-let [input-id (state/get-edit-input-id)]
@@ -537,7 +542,7 @@
   (when-let [input-id (state/get-edit-input-id)]
     (when-let [current-input (gdom/getElement input-id)]
       (let [edit-content (gobj/get current-input "value")
-            slash-pos (:pos @*slash-caret-pos)
+            slash-pos (:pos (:pos (state/get-editor-action-data)))
             [re-pattern new-line-re-pattern] (if (= :org format)
                                                [#"\*+\s" #"\n\*+\s"]
                                                [#"#+\s" #"\n#+\s"])
@@ -595,22 +600,22 @@
         (state/set-edit-content! input-id new-value)))))
 
 (defmethod handle-step :editor/search-page [[_]]
-  (state/set-editor-show-page-search! true))
+  (state/set-editor-action! :page-search))
 
 (defmethod handle-step :editor/search-page-hashtag [[_]]
-  (state/set-editor-show-page-search-hashtag! true))
+  (state/set-editor-action! :page-search-hashtag))
 
 (defmethod handle-step :editor/search-block [[_ _type]]
-  (state/set-editor-show-block-search! true))
+  (state/set-editor-action! :block-search))
 
 (defmethod handle-step :editor/search-template [[_]]
-  (state/set-editor-show-template-search! true))
+  (state/set-editor-action! :template-search))
 
 (defmethod handle-step :editor/show-input [[_ option]]
   (state/set-editor-show-input! option))
 
 (defmethod handle-step :editor/show-zotero [[_]]
-  (state/set-editor-show-zotero! true))
+  (state/set-editor-action! :zotero))
 
 (defn insert-youtube-timestamp
   []
@@ -632,8 +637,8 @@
          (string/blank? value)))
     (do
       (notification/show! [:div "Please add some content first."] :warning)
-      (restore-state false))
-    (state/set-editor-show-date-picker! true)))
+      (restore-state))
+    (state/set-editor-action! :datepicker)))
 
 (defmethod handle-step :editor/click-hidden-file-input [[_ _input-id]]
   (when-let [input-file (gdom/getElement "upload-file")]

+ 16 - 15
src/main/frontend/components/block.cljs

@@ -421,7 +421,9 @@
   [config page-name-in-block page-name redirect-page-name page-entity contents-page? children html-export? label]
   (let [tag? (:tag? config)]
     [:a
-     {:class (if tag? "tag" "page-ref")
+     {:class (cond-> (if tag? "tag" "page-ref")
+               (:property? config)
+               (str " page-property-key"))
       :data-ref page-name
       :on-mouse-down
       (fn [e]
@@ -1065,7 +1067,7 @@
 ;;;; Macro component render functions
 (defn- macro-query-cp
   [config arguments]
-  [:div.dsl-query
+  [:div.dsl-query.overflow-x-hidden.pr-3.sm:pr-0
    (let [query (->> (string/join ", " arguments)
                     (string/trim))]
      (when-not (string/blank? query)
@@ -1805,7 +1807,7 @@
   [config block k v]
   (let [date (and (= k :date) (date/get-locale-string (str v)))]
     [:div
-     [:span.page-property-key.font-medium (name k)]
+     (page-cp (assoc config :property? true) {:block/name (subs (str k) 1)})
      [:span.mr-1 ":"]
      (cond
        (int? v)
@@ -1894,12 +1896,12 @@
                            (do
                              (reset! show? false)
                              (reset! commands/*current-command nil)
-                             (state/set-editor-show-date-picker! false)
+                             (state/clear-editor-action!)
                              (state/set-timestamp-block! nil))
                            (do
                              (reset! show? true)
                              (reset! commands/*current-command typ)
-                             (state/set-editor-show-date-picker! true)
+                             (state/set-editor-action! :datepicker)
                              (state/set-timestamp-block! {:block block
                                                           :typ typ
                                                           :show? show?}))))}
@@ -1923,7 +1925,8 @@
    (util/details-or-summary? target)
    (and (util/sup? target)
         (dom/has-class? target "fn"))
-   (dom/has-class? target "image-resize")))
+   (dom/has-class? target "image-resize")
+   (dom/closest target "a")))
 
 (defn- block-content-on-mouse-down
   [e block block-id _content edit-input-id]
@@ -2057,7 +2060,7 @@
 
      [:<>
       [:div.flex.flex-row.justify-between.block-content-inner
-       [:div.flex-1
+       [:div.flex-1.w-full
         (cond
           (seq title)
           (build-block-title config block)
@@ -2168,7 +2171,10 @@
             (ui/block-error "Block Render Error:"
                             {:content (:block/content block)
                              :section-attrs
-                             {:on-click #(state/set-editing! edit-input-id (:block/content block) block "")}})
+                             {:on-click #(do
+                                           (editor-handler/clear-selection!)
+                                           (editor-handler/unhighlight-blocks!)
+                                           (state/set-editing! edit-input-id (:block/content block) block ""))}})
             (block-content config block edit-input-id block-id slide?))]
           [:div.flex.flex-row.items-center
            (when (and (:embed? config)
@@ -2551,10 +2557,7 @@
         custom-query? (boolean (:custom-query? config))]
     (if (and ref? (not custom-query?) (not (:ref-query-child? config)))
       (ui/lazy-visible
-       (fn []
-         (block-container-inner state repo config block))
-       nil
-       {})
+       (fn [] (block-container-inner state repo config block)))
       (block-container-inner state repo config block))))
 
 (defn divide-lists
@@ -2892,9 +2895,7 @@
   (ui/catch-error
    (ui/block-error "Query Error:" {:content (:query q)})
    (ui/lazy-visible
-    (fn [] (custom-query* config q))
-    nil
-    {})))
+    (fn [] (custom-query* config q)))))
 (defn admonition
   [config type result]
   (when-let [icon (case (string/lower-case (name type))

+ 6 - 1
src/main/frontend/components/block.css

@@ -188,7 +188,7 @@
 .block-left-menu {
     background-color: var(--ls-secondary-background-color);
     background: linear-gradient(90deg, var(--ls-primary-background-color) 0%, var(--ls-secondary-background-color) 100%);
-    
+
     .commands-button {
         overflow: hidden;
         max-width: 40px;
@@ -613,9 +613,14 @@ a.cloze-revealed {
 }
 
 .page-property-key {
+  @apply font-medium;
   color: var(--ls-secondary-text-color);
 }
 
+.page-property-key:hover {
+    background-color: var(--ls-selection-background-color);
+}
+
 .block-parents a {
   color: var(--ls-primary-text-color);
 }

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

@@ -105,8 +105,7 @@
     (when show?
       (reset! show? false)))
   (clear-timestamp!)
-  (state/set-editor-show-date-picker! false)
-  (commands/restore-state false))
+  (commands/restore-state))
 
 (rum/defc time-repeater < rum/reactive
   (mixins/event-mixin
@@ -147,7 +146,7 @@
                                    (contains? #{"deadline" "scheduled"}
                                               (string/lower-case current-command)))
         date (state/sub :date-picker/date)]
-    (when (state/sub :editor/show-date-picker?)
+    (when (= :datepicker (state/sub :editor/action))
       [:div#date-time-picker.flex.flex-row {:on-click (fn [e] (util/stop e))
                                             :on-mouse-down (fn [e] (.stopPropagation e))}
        (ui/datepicker
@@ -164,7 +163,7 @@
                                                (util/format "[[%s]]" journal)
                                                format
                                                nil)
-               (state/set-editor-show-date-picker! false)
+               (state/clear-editor-action!)
                (reset! commands/*current-command nil))))})
        (when deadline-or-schedule?
          (time-repeater))])))

+ 243 - 192
src/main/frontend/components/editor.cljs

@@ -2,11 +2,12 @@
   (:require [clojure.string :as string]
             [goog.string :as gstring]
             [frontend.commands :as commands
-             :refer [*angle-bracket-caret-pos *first-command-group *matched-block-commands *matched-commands *show-block-commands *show-commands *slash-caret-pos]]
+             :refer [*first-command-group *matched-block-commands *matched-commands]]
             [frontend.components.block :as block]
             [frontend.components.datetime :as datetime-comp]
             [frontend.components.search :as search]
             [frontend.components.svg :as svg]
+            [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
             [frontend.db.model :as db-model]
             [frontend.extensions.zotero :as zotero]
@@ -29,8 +30,8 @@
 
 (rum/defc commands < rum/reactive
   [id format]
-  (let [matched (util/react *matched-commands)]
-    (when (util/react *show-commands)
+  (when (= :commands (state/sub :editor/action))
+    (let [matched (util/react *matched-commands)]
       (ui/auto-complete
        matched
        {:get-group-name
@@ -82,7 +83,7 @@
 
 (rum/defc block-commands < rum/reactive
   [id format]
-  (when (util/react *show-block-commands)
+  (when (= :block-commands (state/get-editor-action))
     (let [matched (util/react *matched-block-commands)]
       (ui/auto-complete
        (map first matched)
@@ -99,59 +100,63 @@
   {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
   "Embedded page searching popup"
   [id format]
-  (when (state/sub :editor/show-page-search?)
-    (let [pos (state/get-editor-last-pos)
-          input (gdom/getElement id)]
-      (when input
-        (let [current-pos (cursor/pos input)
-              edit-content (or (state/sub [:editor/content id]) "")
-              sidebar? (in-sidebar? input)
-              q (or
-                 @editor-handler/*selected-text
-                 (when (state/sub :editor/show-page-search-hashtag?)
-                   (gp-util/safe-subs edit-content pos current-pos))
-                 (when (> (count edit-content) current-pos)
-                   (gp-util/safe-subs edit-content pos current-pos))
-                 "")
-              matched-pages (when-not (string/blank? q)
-                              (editor-handler/get-matched-pages q))
-              matched-pages (cond
-                              (contains? (set (map util/page-name-sanity-lc matched-pages)) (util/page-name-sanity-lc (string/trim q)))  ;; if there's a page name fully matched
-                              matched-pages
-
-                              (string/blank? q)
-                              nil
-
-                              (empty? matched-pages)
-                              (cons (str "New page: " q) matched-pages)
-
-                              ;; reorder, shortest and starts-with first.
-                              :else
-                              (let [matched-pages (remove nil? matched-pages)
-                                    matched-pages (sort-by
-                                                   (fn [m]
-                                                     [(not (gstring/caseInsensitiveStartsWith m q)) (count m)])
-                                                   matched-pages)]
-                                (if (gstring/caseInsensitiveStartsWith (first matched-pages) q)
-                                  (cons (first matched-pages)
-                                        (cons  (str "New page: " q) (rest matched-pages)))
-                                  (cons (str "New page: " q) matched-pages))))]
-          (ui/auto-complete
-           matched-pages
-           {:on-chosen   (page-handler/on-chosen-handler input id q pos format)
-            :on-enter    #(page-handler/page-not-exists-handler input id q current-pos)
-            :item-render (fn [page-name chosen?]
-                           [:div.preview-trigger-wrapper
-                            (block/page-preview-trigger
-                             {:children        [:div (search/highlight-exact-query page-name q)]
-                              :open?           chosen?
-                              :manual?         true
-                              :fixed-position? true
-                              :tippy-distance  24
-                              :tippy-position  (if sidebar? "left" "right")}
-                             page-name)])
-            :empty-placeholder [:div.text-gray-500.text-sm.px-4.py-2 "Search for a page"]
-            :class       "black"}))))))
+  (let [action (state/sub :editor/action)]
+    (when (contains? #{:page-search :page-search-hashtag} action)
+      (let [pos (state/get-editor-last-pos)
+            input (gdom/getElement id)]
+        (when input
+          (let [current-pos (cursor/pos input)
+                edit-content (or (state/sub [:editor/content id]) "")
+                sidebar? (in-sidebar? input)
+                q (or
+                   @editor-handler/*selected-text
+                   (when (= action :page-search-hashtag)
+                     (gp-util/safe-subs edit-content pos current-pos))
+                   (when (> (count edit-content) current-pos)
+                     (gp-util/safe-subs edit-content pos current-pos))
+                   "")
+                matched-pages (when-not (string/blank? q)
+                                (editor-handler/get-matched-pages q))
+                matched-pages (cond
+                                (contains? (set (map util/page-name-sanity-lc matched-pages))
+                                           (util/page-name-sanity-lc (string/trim q)))  ;; if there's a page name fully matched
+                                (sort-by (fn [m]
+                                           [(count m) m])
+                                         matched-pages)
+
+                                (string/blank? q)
+                                nil
+
+                                (empty? matched-pages)
+                                (cons (str (t :new-page) ": " q) matched-pages)
+
+                               ;; reorder, shortest and starts-with first.
+                                :else
+                                (let [matched-pages (remove nil? matched-pages)
+                                      matched-pages (sort-by
+                                                     (fn [m]
+                                                       [(not (gstring/caseInsensitiveStartsWith m q)) (count m) m])
+                                                     matched-pages)]
+                                  (if (gstring/caseInsensitiveStartsWith (first matched-pages) q)
+                                    (cons (first matched-pages)
+                                          (cons  (str (t :new-page) ": " q) (rest matched-pages)))
+                                    (cons (str (t :new-page) ": " q) matched-pages))))]
+            (ui/auto-complete
+             matched-pages
+             {:on-chosen   (page-handler/on-chosen-handler input id q pos format)
+              :on-enter    #(page-handler/page-not-exists-handler input id q current-pos)
+              :item-render (fn [page-name chosen?]
+                             [:div.preview-trigger-wrapper
+                              (block/page-preview-trigger
+                               {:children        [:div (search/highlight-exact-query page-name q)]
+                                :open?           chosen?
+                                :manual?         true
+                                :fixed-position? true
+                                :tippy-distance  24
+                                :tippy-position  (if sidebar? "left" "right")}
+                               page-name)])
+              :empty-placeholder [:div.text-gray-500.text-sm.px-4.py-2 "Search for a page"]
+              :class       "black"})))))))
 
 (rum/defcs block-search-auto-complete < rum/reactive
   {:init (fn [state]
@@ -164,24 +169,25 @@
                      (reset! result matched-blocks)))
                  state)}
   [state _edit-block input id q format]
-  (let [result (rum/react (get state ::result))
+  (let [result (->> (rum/react (get state ::result))
+                    (remove (fn [b] (string/blank? (:block/content (db-model/query-block-by-uuid (:block/uuid b)))))))
         chosen-handler (editor-handler/block-on-chosen-handler input id q format)
         non-exist-block-handler (editor-handler/block-non-exist-handler input)]
-    (when result
-      (ui/auto-complete
-       result
-       {:on-chosen   chosen-handler
-        :on-enter    non-exist-block-handler
-        :empty-placeholder   [:div.text-gray-500.pl-4.pr-4 "Search for a block"]
-        :item-render (fn [{:block/keys [page uuid]}]  ;; content returned from search engine is normalized
-                       (let [page (or (:block/original-name page)
-                                      (:block/name page))
-                             repo (state/sub :git/current-repo)
-                             format (db/get-page-format page)
-                             block (db-model/query-block-by-uuid uuid)
-                             content (:block/content block)]
-                         [:.py-2 (search/block-search-result-item repo uuid format content q :block)]))
-        :class       "black"}))))
+    (ui/auto-complete
+     result
+     {:on-chosen   chosen-handler
+      :on-enter    non-exist-block-handler
+      :empty-placeholder   [:div.text-gray-500.pl-4.pr-4 "Search for a block"]
+      :item-render (fn [{:block/keys [page uuid]}]  ;; content returned from search engine is normalized
+                     (let [page (or (:block/original-name page)
+                                    (:block/name page))
+                           repo (state/sub :git/current-repo)
+                           format (db/get-page-format page)
+                           block (db-model/query-block-by-uuid uuid)
+                           content (:block/content block)]
+                       (when-not (string/blank? content)
+                         [:.py-2 (search/block-search-result-item repo uuid format content q :block)])))
+      :class       "black"})))
 
 (rum/defcs block-search < rum/reactive
   {:will-unmount (fn [state]
@@ -189,7 +195,7 @@
                    (state/clear-search-result!)
                    state)}
   [state id _format]
-  (when (state/sub :editor/show-block-search?)
+  (when (= :block-search (state/sub :editor/action))
     (let [pos (state/get-editor-last-pos)
           input (gdom/getElement id)
           [id format] (:rum/args state)
@@ -206,27 +212,73 @@
 (rum/defc template-search < rum/reactive
   {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
   [id _format]
-  (when (state/sub :editor/show-template-search?)
-    (let [pos (state/get-editor-last-pos)
-          input (gdom/getElement id)]
-      (when input
-        (let [current-pos (cursor/pos input)
-              edit-content (state/sub [:editor/content id])
-              q (or
-                 (when (>= (count edit-content) current-pos)
-                   (subs edit-content pos current-pos))
-                 "")
-              matched-templates (editor-handler/get-matched-templates q)
-              non-exist-handler (fn [_state]
-                                  (state/set-editor-show-template-search! false))]
-          (ui/auto-complete
-           matched-templates
-           {:on-chosen   (editor-handler/template-on-chosen-handler id)
-            :on-enter    non-exist-handler
-            :empty-placeholder [:div.text-gray-500.px-4.py-2.text-sm "Search for a template"]
-            :item-render (fn [[template _block-db-id]]
-                           template)
-            :class       "black"}))))))
+  (let [pos (state/get-editor-last-pos)
+        input (gdom/getElement id)]
+    (when input
+      (let [current-pos (cursor/pos input)
+            edit-content (state/sub [:editor/content id])
+            q (or
+               (when (>= (count edit-content) current-pos)
+                 (subs edit-content pos current-pos))
+               "")
+            matched-templates (editor-handler/get-matched-templates q)
+            non-exist-handler (fn [_state]
+                                (state/clear-editor-action!))]
+        (ui/auto-complete
+         matched-templates
+         {:on-chosen   (editor-handler/template-on-chosen-handler id)
+          :on-enter    non-exist-handler
+          :empty-placeholder [:div.text-gray-500.px-4.py-2.text-sm "Search for a template"]
+          :item-render (fn [[template _block-db-id]]
+                         template)
+          :class       "black"})))))
+
+(rum/defc property-search < rum/reactive
+  {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
+  [id]
+  (let [input (gdom/getElement id)]
+    (when input
+      (let [q (or (:searching-property (editor-handler/get-searching-property input))
+                  "")
+            matched-properties (editor-handler/get-matched-properties q)
+            q-property (string/replace (string/lower-case q) #"\s+" "-")
+            non-exist-handler (fn [_state]
+                                ((editor-handler/property-on-chosen-handler id q-property) nil))]
+        (ui/auto-complete
+         matched-properties
+         {:on-chosen (editor-handler/property-on-chosen-handler id q-property)
+          :on-enter non-exist-handler
+          :empty-placeholder [:div.px-4.py-2.text-sm (str "Create a new property: " q-property)]
+          :header [:div.px-4.py-2.text-sm.font-medium "Matched properties: "]
+          :item-render (fn [property] property)
+          :class       "black"})))))
+
+(rum/defc property-value-search < rum/reactive
+  {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
+  [id]
+  (let [property (:property (state/get-editor-action-data))
+        input (gdom/getElement id)]
+    (when (and input
+               (not (string/blank? property)))
+      (let [current-pos (cursor/pos input)
+            edit-content (state/sub [:editor/content id])
+            start-idx (string/last-index-of (subs edit-content 0 current-pos) "::")
+            q (or
+               (when (>= current-pos (+ start-idx 2))
+                 (subs edit-content (+ start-idx 2) current-pos))
+               "")
+            q (string/triml q)
+            matched-values (editor-handler/get-matched-property-values property q)
+            non-exist-handler (fn [_state]
+                                ((editor-handler/property-value-on-chosen-handler id q) nil))]
+        (ui/auto-complete
+         matched-values
+         {:on-chosen (editor-handler/property-value-on-chosen-handler id q)
+          :on-enter non-exist-handler
+          :empty-placeholder [:div.px-4.py-2.text-sm (str "Create a new property value: " q)]
+          :header [:div.px-4.py-2.text-sm.font-medium "Matched property values: "]
+          :item-render (fn [property-value] property-value)
+          :class       "black"})))))
 
 (rum/defcs input < rum/reactive
   (rum/local {} ::input-value)
@@ -237,44 +289,44 @@
       {;; enter
        13 (fn [state e]
             (let [input-value (get state ::input-value)
-                  input-option (get @state/state :editor/show-input)]
+                  input-option (:options (state/get-editor-show-input))]
               (when (seq @input-value)
                 ;; no new line input
                 (util/stop e)
                 (let [[_id on-submit] (:rum/args state)
-                      {:keys [pos]} @*slash-caret-pos
                       command (:command (first input-option))]
-                  (on-submit command @input-value pos))
+                  (on-submit command @input-value))
                 (reset! input-value nil))))})))
   [state _id on-submit]
-  (when-let [input-option (state/sub :editor/show-input)]
-    (let [{:keys [pos]} (util/react *slash-caret-pos)
-          input-value (get state ::input-value)]
-      (when (seq input-option)
-        (let [command (:command (first input-option))]
-          [:div.p-2.rounded-md.shadow-lg
-           (for [{:keys [id placeholder type autoFocus] :as input-item} input-option]
-             [:div.my-3 {:key id}
-              [:input.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5
-               (merge
-                (cond->
-                 {:key           (str "modal-input-" (name id))
-                  :id            (str "modal-input-" (name id))
-                  :type          (or type "text")
-                  :on-change     (fn [e]
-                                   (swap! input-value assoc id (util/evalue e)))
-                  :auto-complete (if (util/chrome?) "chrome-off" "off")}
-                  placeholder
-                  (assoc :placeholder placeholder)
-                  autoFocus
-                  (assoc :auto-focus true))
-                (dissoc input-item :id))]])
-           (ui/button
-            "Submit"
-            :on-click
-            (fn [e]
-              (util/stop e)
-              (on-submit command @input-value pos)))])))))
+  (when (= :input (state/sub :editor/action))
+    (when-let [action-data (state/sub :editor/action-data)]
+      (let [{:keys [pos options]} action-data
+            input-value (get state ::input-value)]
+        (when (seq options)
+          (let [command (:command (first options))]
+            [:div.p-2.rounded-md.shadow-lg
+             (for [{:keys [id placeholder type autoFocus] :as input-item} options]
+               [:div.my-3 {:key id}
+                [:input.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5
+                 (merge
+                  (cond->
+                    {:key           (str "modal-input-" (name id))
+                     :id            (str "modal-input-" (name id))
+                     :type          (or type "text")
+                     :on-change     (fn [e]
+                                      (swap! input-value assoc id (util/evalue e)))
+                     :auto-complete (if (util/chrome?) "chrome-off" "off")}
+                    placeholder
+                    (assoc :placeholder placeholder)
+                    autoFocus
+                    (assoc :auto-focus true))
+                  (dissoc input-item :id))]])
+             (ui/button
+               "Submit"
+               :on-click
+               (fn [e]
+                 (util/stop e)
+                 (on-submit command @input-value pos)))]))))))
 
 (rum/defc absolute-modal < rum/static
   [cp set-default-width? {:keys [top left rect]}]
@@ -305,37 +357,37 @@
         to-max-height (if y-overflow-vh? max-height to-max-height)
         pos-rect (when (and (seq rect) editing-key)
                    (:rect (cursor/get-caret-pos (state/get-input))))
-        y-diff (when pos-rect (- (:height pos-rect) (:height rect)))]
+        y-diff (when pos-rect (- (:height pos-rect) (:height rect)))
+        style (merge
+               {:top        (+ top offset-top (if (int? y-diff) y-diff 0))
+                :max-height to-max-height
+                :max-width 700
+                ;; TODO: auto responsive fixed size
+                :width "fit-content"
+                :z-index    11}
+               (when set-default-width?
+                 {:width max-width})
+               (let [^js/HTMLElement editor
+                     (js/document.querySelector ".editor-wrapper")]
+                 (if (<= (.-clientWidth editor) (+ left (if set-default-width? max-width 500)))
+                   {:right 0}
+                   {:left (if (or (nil? y-diff) (and y-diff (= y-diff 0))) left 0)})))]
     [:div.absolute.rounded-md.shadow-lg.absolute-modal
      {:ref *el
       :class (if y-overflow-vh? "is-overflow-vh-y" "")
       :on-mouse-down (fn [e]
                        (.stopPropagation e))
-      :style (merge
-              {:top        (+ top offset-top (if (int? y-diff) y-diff 0))
-               :max-height to-max-height
-               :max-width 700
-               ;; TODO: auto responsive fixed size
-               :width "fit-content"
-               :z-index    11}
-              (when set-default-width?
-                {:width max-width})
-              (let [^js/HTMLElement editor
-                    (js/document.querySelector ".editor-wrapper")]
-                (if (<= (.-clientWidth editor) (+ left (if set-default-width? max-width 500)))
-                  {:right 0}
-                  {:left (if (and y-diff (= y-diff 0)) left 0)})))}
+      :style style}
      cp]))
 
 (rum/defc transition-cp < rum/reactive
-  [cp set-default-width? pos]
-  (when pos
-    (when-let [pos (rum/react pos)]
-      (ui/css-transition
-       {:class-names "fade"
-        :timeout     {:enter 500
-                      :exit  300}}
-       (absolute-modal cp set-default-width? pos)))))
+  [cp set-default-width?]
+  (when-let [pos (:pos (state/sub :editor/action-data))]
+    (ui/css-transition
+     {:class-names "fade"
+      :timeout     {:enter 500
+                    :exit  300}}
+     (absolute-modal cp set-default-width? pos))))
 
 (rum/defc image-uploader < rum/reactive
   [id format]
@@ -354,8 +406,7 @@
         [:div.flex.flex-row.align-center.rounded-md.shadow-sm.bg-base-2.px-1.py-1
          (ui/loading
           (util/format "Uploading %s%" (util/format "%2d" processing)))]
-        false
-        *slash-caret-pos)))])
+        false)))])
 
 (defn- set-up-key-down!
   [state format]
@@ -452,9 +503,9 @@
   (let [content (state/sub-edit-content)]
     (mock-textarea content)))
 
-(defn animated-modal
-  [key component set-default-width? *pos]
-  (when *pos
+(rum/defc animated-modal < rum/reactive
+  [key component set-default-width?]
+  (when-let [pos (:pos (state/get-editor-action-data))]
     (ui/css-transition
      {:key key
       :class-names {:enter "origin-top-left opacity-0 transform scale-95"
@@ -466,48 +517,48 @@
        (absolute-modal
         component
         set-default-width?
-        *pos)))))
+        pos)))))
 
 (rum/defc modals < rum/reactive
   "React to atom changes, find and render the correct modal"
   [id format]
-  (ui/transition-group
-   (cond
-     (and (util/react *show-commands)
-          (not (state/sub :editor/show-page-search?))
-          (not (state/sub :editor/show-block-search?))
-          (not (state/sub :editor/show-template-search?))
-          (not (state/sub :editor/show-input))
-          (not (state/sub :editor/show-zotero))
-          (not (state/sub :editor/show-date-picker?)))
-     (animated-modal "commands" (commands id format) true (util/react *slash-caret-pos))
-
-     (and (util/react *show-block-commands) @*angle-bracket-caret-pos)
-     (animated-modal "block-commands" (block-commands id format) true (util/react *angle-bracket-caret-pos))
-
-     (state/sub :editor/show-page-search?)
-     (animated-modal "page-search" (page-search id format) true (util/react *slash-caret-pos))
-
-     (state/sub :editor/show-block-search?)
-     (animated-modal "block-search" (block-search id format) false (util/react *slash-caret-pos))
-
-     (state/sub :editor/show-template-search?)
-     (animated-modal "template-search" (template-search id format) true (util/react *slash-caret-pos))
-
-     (state/sub :editor/show-date-picker?)
-     (animated-modal "date-picker" (datetime-comp/date-picker id format nil) false (util/react *slash-caret-pos))
-
-     (state/sub :editor/show-input)
-     (animated-modal "input" (input id
-                                    (fn [command m _pos]
-                                      (editor-handler/handle-command-input command id format m)))
-                     true (util/react *slash-caret-pos))
-
-     (state/sub :editor/show-zotero)
-     (animated-modal "zotero-search" (zotero/zotero-search id) false (util/react *slash-caret-pos))
-
-     :else
-     nil)))
+  (let [action (state/sub :editor/action)]
+    (cond
+      (= action :commands)
+      (animated-modal "commands" (commands id format) true)
+
+      (= action :block-commands)
+      (animated-modal "block-commands" (block-commands id format) true)
+
+      (contains? #{:page-search :page-search-hashtag} action)
+      (animated-modal "page-search" (page-search id format) true)
+
+      (= :block-search action)
+      (animated-modal "block-search" (block-search id format) true)
+
+      (= :template-search action)
+      (animated-modal "template-search" (template-search id format) true)
+
+      (= :property-search action)
+      (animated-modal "property-search" (property-search id) true)
+
+      (= :property-value-search action)
+      (animated-modal "property-value-search" (property-value-search id) true)
+
+      (= :datepicker action)
+      (animated-modal "date-picker" (datetime-comp/date-picker id format nil) false)
+
+      (= :input action)
+      (animated-modal "input" (input id
+                                     (fn [command m]
+                                       (editor-handler/handle-command-input command id format m)))
+                      true)
+
+      (= :zotero action)
+      (animated-modal "zotero-search" (zotero/zotero-search id) false)
+
+      :else
+      nil)))
 
 (rum/defcs box < rum/reactive
   {:init (fn [state]

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

@@ -172,10 +172,10 @@
 
        {:title [:div.flex-row.flex.justify-between.items-center
                 [:span (t :join-community)]]
-        :options {:href "https://discord.gg/KpN4eHY"
-                  :title (t :discord-title)
+        :options {:href "https://discuss.logseq.com"
+                  :title (t :discourse-title)
                   :target "_blank"}
-        :icon (ui/icon "brand-discord")}]
+        :icon (ui/icon "message-circle")}]
       (concat page-menu-and-hr)
       (remove nil?))
      {})))

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

@@ -56,10 +56,7 @@
 
       (if today?
         (blocks-cp repo page format)
-        (ui/lazy-visible (fn []
-                           (blocks-cp repo page format))
-                         nil
-                         {}))
+        (ui/lazy-visible (fn [] (blocks-cp repo page format))))
 
       {})
 

+ 4 - 4
src/main/frontend/components/onboarding.cljs

@@ -12,9 +12,9 @@
 (defn help
   []
   [:div.help.cp__sidebar-help-docs
-   (let [discord-with-icon [:div.flex-row.inline-flex.items-center
-                            [:span.mr-1 (t :help/community)]
-                            (ui/icon "brand-discord" {:style {:font-size 20}})]
+   (let [discourse-with-icon [:div.flex-row.inline-flex.items-center
+                            [:span.mr-1 (t :help/forum-community)]
+                            (ui/icon "message-circle" {:style {:font-size 20}})]
          list
          [{:title "Usage"
            :children [[[:a
@@ -29,7 +29,7 @@
           {:title "Community"
            :children [[(t :help/awesome-logseq) "https://github.com/logseq/awesome-logseq"]
                       [(t :help/blog) "https://blog.logseq.com"]
-                      [discord-with-icon "https://discord.gg/KpN4eHY"]]}
+                      [discourse-with-icon "https://discuss.logseq.com"]]}
 
           {:title "Development"
            :children [[(t :help/roadmap) "https://trello.com/b/8txSM12G/roadmap"]

+ 13 - 8
src/main/frontend/components/plugins.cljs

@@ -855,26 +855,31 @@
   "type of :toolbar, :pagebar"
   [_state type]
   (when (state/sub [:plugin/installed-ui-items])
-    (let [pinned-items (state/sub [:plugin/preferences :pinnedToolbarItems])
+    (let [toolbar?     (= :toolbar type)
+          pinned-items (state/sub [:plugin/preferences :pinnedToolbarItems])
           pinned-items (and (sequential? pinned-items) (into #{} pinned-items))
           items        (state/get-plugins-ui-items-with-type type)
           items        (sort-by #(:key (second %)) items)]
 
       (when-let [items (and (seq items)
-                            (map #(assoc-in % [1 :pinned?]
-                                            (let [[_ {:keys [key]} pid] %
-                                                  pkey (str (name pid) ":" key)]
-                                              (contains? pinned-items pkey)))
-                                 items))]
+                            (if toolbar?
+                              (map #(assoc-in % [1 :pinned?]
+                                              (let [[_ {:keys [key]} pid] %
+                                                    pkey (str (name pid) ":" key)]
+                                                (contains? pinned-items pkey)))
+                                   items)
+                              items))]
 
         [:div {:class     (str "ui-items-container")
                :data-type (name type)}
          (conj (for [[_ {:keys [key pinned?] :as opts} pid] items]
-                 (when (or (not (set? pinned-items)) pinned?)
+                 (when (or (not toolbar?)
+                           (not (set? pinned-items)) pinned?)
                    (rum/with-key (ui-item-renderer pid type opts) key))))
 
          ;; manage plugin buttons
-         (toolbar-plugins-manager-list items)]))))
+         (when toolbar?
+           (toolbar-plugins-manager-list items))]))))
 
 (rum/defcs hook-ui-fenced-code < rum/reactive
   [_state content {:keys [render edit] :as _opts}]

+ 1 - 3
src/main/frontend/components/reference.cljs

@@ -169,9 +169,7 @@
    (ui/component-error "Linked References: Unexpected error")
    (ui/lazy-visible
     (fn []
-      (references* page-name))
-    nil
-    {})))
+      (references* page-name)))))
 
 (rum/defcs unlinked-references-aux
   < rum/reactive db-mixins/query

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

@@ -216,7 +216,7 @@
 
        :new-page
        [:div.text.font-bold (str (t :new-page) ": ")
-        [:span.ml-1 (str "\"" search-q "\"")]]
+        [:span.ml-1 (str "\"" (string/trim search-q) "\"")]]
 
        :go-to-whiteboard
        [:div.text.font-bold (str (t :go-to-whiteboard) ": ")

+ 2 - 2
src/main/frontend/components/widgets.cljs

@@ -39,9 +39,9 @@
 
          [:li.mt-8
           [:div.font-bold.mb-2 "I need some help"]
-          [:p "👋 Join our discord group to chat with the makers and our helpful community members."]
+          [:p "👋 Join our Forum to chat with the makers and our helpful community members."]
           (ui/button "Join the community"
-            :href "https://discord.gg/KpN4eHY"
+            :href "https://discuss.logseq.com"
             :target "_blank")]]]
        [:div.cp__widgets-open-local-directory
         [:div.select-file-wrap.cursor

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

@@ -31,7 +31,7 @@
    "
     Welcome to Logseq!
     If you encounter any problem, feel free to file an issue on GitHub (https://github.com/logseq/logseq)
-    or join our Discord server (https://discord.gg/KpN4eHY).
+    or join our forum (https://discuss.logseq.com).
     .____
     |    |    ____   ____  ______ ____  ______
     |    |   /  _ \\ / ___\\/  ___// __ \\/ ____/

+ 34 - 0
src/main/frontend/db/model.cljs

@@ -1341,6 +1341,40 @@
                 [(get m :template) e]))
          (into {}))))
 
+(defn get-all-properties
+  []
+  (let [properties (d/q
+                     '[:find [?p ...]
+                       :where
+                       [_ :block/properties ?p]]
+                     (conn/get-db))
+        properties (remove (fn [m] (empty? m)) properties)]
+    (->> (map keys properties)
+         (apply concat)
+         distinct
+         (remove #{:id})
+         sort)))
+
+(defn get-property-values
+  [property]
+  (let [pred (fn [_db properties]
+               (get properties property))]
+    (->>
+     (d/q
+       '[:find [?property-val ...]
+         :in $ ?pred
+         :where
+         [_ :block/properties ?p]
+         [(?pred $ ?p) ?property-val]]
+       (conn/get-db)
+       pred)
+     (map (fn [x] (if (coll? x) x [x])))
+     (apply concat)
+     (map str)
+     (remove string/blank?)
+     (distinct)
+     (sort))))
+
 (defn get-template-by-name
   [name]
   (when (string? name)

+ 14 - 12
src/main/frontend/dicts.cljc

@@ -26,6 +26,7 @@
         :help/privacy "Privacy policy"
         :help/terms "Terms"
         :help/community "Discord community"
+        :help/forum-community "Forum community"
         :help/awesome-logseq "Awesome Logseq"
         :help/shortcuts "Keyboard shortcuts"
         :help/shortcuts-triggers "Triggers"
@@ -253,7 +254,8 @@
         :import "Import"
         :join-community "Join the community"
         :sponsor-us "Sponsor Us"
-        :discord-title "Our discord group!"
+        :discourse-title "Our forum!"
+        :discord-title "Our discord group!" ;; unused
         :help-shortcut-title "Click to check shortcuts and other tips"
         :loading "Loading"
         :cloning "Cloning"
@@ -809,11 +811,11 @@
            :right-side-bar/favorites "收藏"
            :right-side-bar/page-graph "页面图谱:"
            :right-side-bar/block-ref "块引用"
-           :right-side-bar/graph-view "Graph view"
-           :right-side-bar/all-pages "All pages"
-           :right-side-bar/flashcards "Flashcards"
-           :right-side-bar/new-page "New page"
-           :left-side-bar/journals "Journals"
+           :right-side-bar/graph-view "图谱视角"
+           :right-side-bar/all-pages "全部页面"
+           :right-side-bar/flashcards "记忆卡片"
+           :right-side-bar/new-page "新页面"
+           :left-side-bar/journals "日志"
            :left-side-bar/new-page "新页面"
            :left-side-bar/nav-favorites "收藏页面"
            :left-side-bar/nav-shortcuts "快捷导航"
@@ -964,7 +966,7 @@
            :all-journals "日记"
            :publishing "发布"
            :export "导出"
-           :all-graphs "所有"
+           :all-graphs "所有图谱"
            :all-pages "所有页面"
            :all-files "所有文件"
            :remove-orphaned-pages "删除空页面"
@@ -1104,11 +1106,11 @@
              :right-side-bar/recent "最近"
              :right-side-bar/contents "目錄"
              :right-side-bar/block-ref "塊引用"
-             :right-side-bar/graph-view "Graph view"
-             :right-side-bar/all-pages "All pages"
-             :right-side-bar/flashcards "Flashcards"
-             :right-side-bar/new-page "New page"
-             :left-side-bar/journals "Journals"
+             :right-side-bar/graph-view "圖譜視角"
+             :right-side-bar/all-pages "全部頁面"
+             :right-side-bar/flashcards "記憶卡片"
+             :right-side-bar/new-page "新頁面"
+             :left-side-bar/journals "日誌"
              :format/preferred-mode "請選擇偏好格式"
              :format/markdown "Markdown"
              :format/org-mode "Org Mode"

+ 30 - 13
src/main/frontend/extensions/html_parser.cljs

@@ -11,7 +11,11 @@
   [hiccup]
   (walk/postwalk (fn [f]
                    (if (map? f)
-                     (dissoc f :style)
+                      (apply dissoc f (conj (filter (fn [key]
+                                                      (string/starts-with? (str key) ":data-"))
+                                                    (keys f))
+                                            :style
+                                            :class))
                      f)) hiccup))
 
 (defn- export-hiccup
@@ -73,7 +77,7 @@
                                              :else
                                              nil)
                                    children' (map-join children)]
-                               (when-not (string/blank? children')
+                               (when (not-empty children')
                                  (str (if (string? pattern) pattern (apply str pattern))
                                       children'
                                       (if (string? pattern) pattern (apply str (reverse pattern)))))))
@@ -122,11 +126,15 @@
                                       :org (util/format "[[%s][%s]]" href label)
                                       nil))))
                            :img (let [src (:src attrs)
-                                      alt (or (:alt attrs) "")]
-                                  (case format
-                                    :markdown (util/format "![%s](%s)" alt src)
-                                    :org (util/format "[[%s][%s]]" src alt)
-                                    nil))
+                                      alt (or (:alt attrs) "")
+                                      ;; reject url-encoded and utf8-encoded(svg)
+                                      unsafe-data-url? (and (string/starts-with? src "data:")
+                                                            (not (re-find #"^data:.*?;base64," src)))]
+                                  (when-not unsafe-data-url?
+                                    (case format
+                                      :markdown (util/format "![%s](%s)" alt src)
+                                      :org (util/format "[[%s][%s]]" src alt)
+                                      nil)))
                            :p (util/format "%s"
                                            (map-join children))
 
@@ -140,12 +148,19 @@
                                                    :span} %))
                            (emphasis-transform tag attrs children)
 
-                           :code (if @*inside-pre?
+                           :code (cond
+                                   @*inside-pre?
                                    (map-join children)
+
+                                   (string? (first children))
                                    (let [pattern (config/get-code format)]
                                      (str " "
                                           (str pattern (first children) pattern)
-                                          " ")))
+                                          " "))
+
+                                   ;; skip monospace style, since it has more complex children
+                                   :else
+                                   (map-join children))
 
                            :pre
                            (do
@@ -170,6 +185,9 @@
                            :li
                            (str "- " (map-join children))
 
+                           :br
+                           "\n"
+
                            :dt
                            (case format
                              :org (str "- " (map-join children) " ")
@@ -218,7 +236,7 @@
                  (for [x hiccup]
                    (single-hiccup-transform x))
                  (single-hiccup-transform hiccup))]
-    (string/replace (apply str result) #"\n\n+" "\n\n")))
+    (apply str result)))
 
 (defn hiccup->doc
   [format hiccup]
@@ -226,9 +244,8 @@
     (if (string/blank? s)
       ""
       (-> s
-          (string/trim)
-          (string/replace "\n\n\n\n" "\n\n")
-          (string/replace "\n\n\n" "\n\n")))))
+          string/trim
+          (string/replace #"\n\n+" "\n\n")))))
 
 (defn html-decode-hiccup
   [hiccup]

+ 50 - 40
src/main/frontend/extensions/srs.cljs

@@ -254,6 +254,10 @@
                        :or {use-cache? true}}]
    (when (string? query-string)
      (let [query-string (template/resolve-dynamic-template! query-string)
+           query-string (if (and (not (string/starts-with? query-string "("))
+                                 (not (string/starts-with? query-string "[")))
+                          (util/format "[[%s]]" (string/trim query-string))
+                          query-string)
            {:keys [query sort-by rules]} (query-dsl/parse query-string)
            query* (concat [['?b :block/refs '?bp] ['?bp :block/name card-hash-tag]]
                           (if (coll? (first query))
@@ -395,14 +399,15 @@
   [:p.p-2 "Congrats, you've reviewed all the cards for this query, see you next time! 💯"])
 
 (defn- btn-with-shortcut [{:keys [shortcut id btn-text background on-click]}]
-  (ui/button [:span btn-text " " (ui/render-keyboard-shortcut shortcut)]
-             :id id
-             :background background
-             :on-click (fn [e]
-                         (when-let [elem (gobj/get e "target")]
-                           (js/console.log (.-classList elem))
-                           (.add (.-classList elem) "opacity-25"))
-                         (js/setTimeout #(on-click) 10))))
+  (ui/button
+    [:span btn-text " " (ui/render-keyboard-shortcut shortcut)]
+    :id id
+    :class id
+    :background background
+    :on-click (fn [e]
+                (when-let [elem (gobj/get e "target")]
+                  (.add (.-classList elem) "opacity-25"))
+                (js/setTimeout #(on-click) 10))))
 
 (rum/defcs view
   < rum/reactive
@@ -419,7 +424,8 @@
                  modal? :modal?
                  cb :callback}
    card-index]
-  (let [cards (map ->card blocks)
+  (let [blocks (if (fn? blocks) (blocks) blocks)
+        cards (map ->card blocks)
         review-records (::review-records state)
         ;; TODO: needs refactor
         card (if preview?
@@ -433,7 +439,10 @@
             root-block-id (:block/uuid root-block)]
         [:div.ls-card.content
          {:class (when (or preview? modal?)
-                   (util/hiccup->class ".flex.flex-col.resize.overflow-y-auto"))}
+                   (str (util/hiccup->class ".flex.flex-col.resize.overflow-y-auto")
+                        (when modal? " modal-cards")))
+          :on-mouse-down (fn [e]
+                           (util/stop e))}
          (let [repo (state/get-current-repo)]
            [:div {:style {:margin-top 20}}
             (component-block/breadcrumb {} repo root-block-id {})])
@@ -446,20 +455,20 @@
          (if (or preview? modal?)
            [:div.flex.my-4.justify-between
             (when-not (and (not preview?) (= next-phase 1))
-              (ui/button [:span (case next-phase
-                                  1 "Hide answers"
-                                  2 "Show answers"
-                                  3 "Show clozes")
-                          (ui/render-keyboard-shortcut [:s])]
-                         :id "card-answers"
-                         :class "mr-2"
-                         :on-click #(reset! phase next-phase)))
-
+              (ui/button
+                [:span (case next-phase
+                         1 "Hide answers"
+                         2 "Show answers"
+                         3 "Show clozes")
+                 (ui/render-keyboard-shortcut [:s])]
+                :class "mr-2 card-answers"
+                :on-click #(reset! phase next-phase)))
             (when (and (> (count cards) 1) preview?)
               (ui/button [:span "Next " (ui/render-keyboard-shortcut [:n])]
-                         :id "card-next"
-                         :class "mr-2"
-                         :on-click #(skip-card card card-index cards phase review-records cb)))
+                :class "mr-2 card-next"
+                :on-click (fn [e]
+                            (util/stop e)
+                            (skip-card card card-index cards phase review-records cb))))
 
             (when (and (not preview?) (= 1 next-phase))
               [:<>
@@ -489,9 +498,11 @@
                          :class "tippy-hover"
                          :interactive true}
                         (ui/button [:span "Reset"]
-                                   :id "card-reset"
-                                   :class (util/hiccup->class "opacity-60.hover:opacity-100")
-                                   :on-click #(operation-reset! card))))]
+                          :id "card-reset"
+                          :class (util/hiccup->class "opacity-60.hover:opacity-100.card-reset")
+                          :on-click (fn [e]
+                                      (util/stop e)
+                                      (operation-reset! card)))))]
            [:div.my-3 (ui/button "Review cards" :small? true)])]))))
 
 (rum/defc view-modal <
@@ -504,9 +515,10 @@
         blocks (if (:random-mode? option)
                  (shuffle blocks)
                  blocks)]
-    (if (seq blocks)
-      (view blocks option card-index)
-      review-finished)))
+    [:div#cards-modal
+     (if (seq blocks)
+       (view blocks option card-index)
+       review-finished)]))
 
 (rum/defc preview-cp
   [block-id]
@@ -639,28 +651,26 @@
                                                  :font-weight 600}
                                                  @*random-mode?
                                                  (assoc :color "orange"))})])]]
-         (if (seq review-cards)
+         (if (or @*preview-mode? (seq review-cards))
            [:div.px-1
-            (when-not modal?
+            (when (and (not modal?) (not @*preview-mode?))
               {:on-click (fn []
-                           (let [blocks-f (if @*preview-mode?
-                                            (fn [] (query repo query-string))
-                                            (fn []
-                                              (let [query-result (query repo query-string)]
-                                                (:result (query-scheduled repo query-result (tl/local-now))))))]
+                           (let [blocks-f (fn []
+                                            (let [query-result (query repo query-string)]
+                                              (:result (query-scheduled repo query-result (tl/local-now)))))]
                              (state/set-modal! #(view-modal
                                                  blocks-f
                                                  {:modal? true
                                                   :random-mode? *random-mode?
-                                                  :preview? @*preview-mode?
+                                                  :preview? false
                                                   :callback callback-fn}
                                                  *card-index)
                                                {:id :srs})))})
             (let [view-fn (if modal? view-modal view)
-                  blocks-fn (if @*preview-mode?
-                              (fn [] (query repo query-string))
-                              review-cards)]
-              (view-fn blocks-fn
+                  blocks (if @*preview-mode?
+                           (query repo query-string)
+                           review-cards)]
+              (view-fn blocks
                (merge config
                       {:global? global?
                        :random-mode? @*random-mode?

+ 5 - 3
src/main/frontend/extensions/srs/handler.cljs

@@ -1,9 +1,11 @@
-(ns frontend.extensions.srs.handler)
+(ns frontend.extensions.srs.handler
+  (:require [dommy.core :refer-macros [sel]]))
 
 (defn click
   [id]
-  (when-let [node (js/document.getElementById id)]
-    (.click node)))
+  (let [nodes (sel [:#cards-modal (str "." id)])]
+    (doseq [node nodes]
+      (.click node))))
 
 (defn toggle-answers []
   (click "card-answers"))

+ 1 - 1
src/main/frontend/extensions/zotero/handler.cljs

@@ -41,7 +41,7 @@
 
 (defn handle-command-zotero
   [id page-name]
-  (state/set-editor-show-zotero! false)
+  (state/clear-editor-action!)
   (editor-handler/insert-command! id (str "[[" page-name "]]") nil {}))
 
 (defn- create-abstract-note!

+ 1 - 1
src/main/frontend/fs/watcher_handler.cljs

@@ -90,7 +90,7 @@
             (when dir-exists?
               (when-let [page-name (db/get-file-page path)]
                 (println "Delete page: " page-name ", file path: " path ".")
-                (page-handler/delete! page-name #() :unlink-file? true))))
+                (page-handler/delete! page-name #() :delete-file? false))))
 
           (and (contains? #{"add" "change" "unlink"} type)
                (string/ends-with? path "logseq/custom.css"))

+ 201 - 156
src/main/frontend/handler/editor.cljs

@@ -4,10 +4,7 @@
             [clojure.string :as string]
             [clojure.walk :as w]
             [dommy.core :as dom]
-            [frontend.commands :as commands
-             :refer [*angle-bracket-caret-pos
-                     *show-block-commands *show-commands
-                     *slash-caret-pos]]
+            [frontend.commands :as commands]
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.db :as db]
@@ -525,8 +522,7 @@
 
 (defn clear-when-saved!
   []
-  (state/clear-editor-show-state!)
-  (commands/restore-state true))
+  (commands/restore-state))
 
 (defn get-state
   []
@@ -664,7 +660,7 @@
 (defn properties-block
   [properties format page]
   (let [content (property/insert-properties format "" properties)
-        refs (gp-block/get-page-refs-from-properties properties
+        refs (gp-block/get-page-refs-from-properties format properties
                                                      (db/get-db (state/get-current-repo))
                                                      (state/get-date-formatter))]
     {:block/pre-block? true
@@ -1292,14 +1288,7 @@
    ;; non English input method
    (when-not (state/editor-in-composition?)
      (when (state/get-current-repo)
-       (when (and (not @commands/*show-commands)
-                  (not @commands/*show-block-commands)
-                  (not (state/get-editor-show-page-search?))
-                  (not (state/get-editor-show-page-search-hashtag?))
-                  (not (state/get-editor-show-block-search?))
-                  (not (state/get-editor-show-date-picker?))
-                  (not (state/get-editor-show-template-search?))
-                  (not (state/get-editor-show-input)))
+       (when-not (state/get-editor-action)
          (try
            (let [input-id (state/get-edit-input-id)
                  block (state/get-edit-block)
@@ -1357,13 +1346,7 @@
     nil)
 
   (when restore?
-    (let [restore-slash-caret-pos? (if (and
-                                        (seq? command-output)
-                                        (= :editor/click-hidden-file-input
-                                           (ffirst command-output)))
-                                     false
-                                     true)]
-      (commands/restore-state restore-slash-caret-pos?))))
+    (commands/restore-state)))
 
 (defn get-asset-file-link
   [format url file-name image?]
@@ -1570,12 +1553,12 @@
           "[["
           (do
             (commands/handle-step [:editor/search-page])
-            (reset! commands/*slash-caret-pos (cursor/get-caret-pos input)))
+            (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)}))
 
           "(("
           (do
             (commands/handle-step [:editor/search-block :reference])
-            (reset! commands/*slash-caret-pos (cursor/get-caret-pos input)))
+            (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)}))
 
           nil)))))
 
@@ -1626,12 +1609,20 @@
   [q]
   (search/template-search q))
 
+(defn get-matched-properties
+  [q]
+  (search/property-search q))
+
+(defn get-matched-property-values
+  [property q]
+  (search/property-value-search property q))
+
 (defn get-matched-commands
   [input]
   (try
     (let [edit-content (or (gobj/get input "value") "")
           pos (cursor/pos input)
-          last-slash-caret-pos (:pos @*slash-caret-pos)
+          last-slash-caret-pos (:pos (:pos (state/get-editor-action-data)))
           last-command (and last-slash-caret-pos (subs edit-content last-slash-caret-pos pos))]
       (when (> pos 0)
         (or
@@ -1649,7 +1640,7 @@
     (let [edit-content (gobj/get input "value")
           pos (cursor/pos input)
           last-command (subs edit-content
-                             (:pos @*angle-bracket-caret-pos)
+                             (:pos (:pos (state/get-editor-action-data)))
                              pos)]
       (when (> pos 0)
         (or
@@ -1664,14 +1655,8 @@
 
 (defn auto-complete?
   []
-  (or @*show-commands
-      @*show-block-commands
-      @*asset-uploading?
-      (state/get-editor-show-input)
-      (state/get-editor-show-page-search?)
-      (state/get-editor-show-block-search?)
-      (state/get-editor-show-template-search?)
-      (state/get-editor-show-date-picker?)))
+  (or @*asset-uploading?
+      (state/get-editor-action)))
 
 (defn get-current-input-char
   [input]
@@ -1785,9 +1770,7 @@
 (defn close-autocomplete-if-outside
   [input]
   (when (and input
-             (or (state/get-editor-show-page-search?)
-                 (state/get-editor-show-page-search-hashtag?)
-                 (state/get-editor-show-block-search?))
+             (state/get-editor-action)
              (not (wrapped-by? input "[[" "]]")))
     (when (get-search-q)
       (let [value (gobj/get input "value")
@@ -1800,9 +1783,7 @@
                     (string/includes? between "]")
                     (string/includes? between "(")
                     (string/includes? between ")")))
-          (state/set-editor-show-block-search! false)
-          (state/set-editor-show-page-search! false)
-          (state/set-editor-show-page-search-hashtag! false))))))
+          (state/clear-editor-action!))))))
 
 (defn resize-image!
   [block-id metadata full_text size]
@@ -1842,25 +1823,51 @@
 (defn handle-last-input []
   (let [input           (state/get-input)
         pos             (cursor/pos input)
-        last-input-char (util/nth-safe (.-value input) (dec pos))]
+        last-input-char (util/nth-safe (.-value input) (dec pos))
+        last-prev-input-char (util/nth-safe (.-value input) (dec (dec pos)))
+        prev-prev-input-char (util/nth-safe (.-value input) (- pos 3))]
 
     ;; TODO: is it cross-browser compatible?
     ;; (not= (gobj/get native-e "inputType") "insertFromPaste")
-    (when (= last-input-char (state/get-editor-command-trigger))
-      (when (seq (get-matched-commands input))
-        (reset! commands/*slash-caret-pos (cursor/get-caret-pos input))
-        (reset! commands/*show-commands true)))
-
-    (if (= last-input-char commands/angle-bracket)
-      (when (seq (get-matched-block-commands input))
-        (reset! commands/*angle-bracket-caret-pos (cursor/get-caret-pos input))
-        (reset! commands/*show-block-commands true))
+    (cond
+      (= last-input-char (state/get-editor-command-trigger))
+      (do
+        (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
+        (commands/reinit-matched-commands!)
+        (state/set-editor-show-commands!))
+
+      (= last-input-char commands/angle-bracket)
+      (do
+        (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
+        (commands/reinit-matched-block-commands!)
+        (state/set-editor-show-block-commands!))
+
+      (and (= last-input-char last-prev-input-char commands/colon)
+           (or (nil? prev-prev-input-char)
+               (= prev-prev-input-char "\n")))
+      (do
+        (cursor/move-cursor-backward input 2)
+        (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
+        (state/set-editor-action! :property-search))
+
+      (and
+       (not= :property-search (state/get-editor-action))
+       (or (wrapped-by? input "" "::")
+           (wrapped-by? input "\n" "::")))
+      (do
+        (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
+        (state/set-editor-action! :property-search))
+
+      (and (= last-input-char commands/colon) (= :property-search (state/get-editor-action)))
+      (state/clear-editor-action!)
+
+      :else
       nil)))
 
 (defn block-on-chosen-handler
   [_input id q format]
   (fn [chosen _click?]
-    (state/set-editor-show-block-search! false)
+    (state/clear-editor-action!)
     (let [uuid-string (str (:block/uuid chosen))]
 
       ;; block reference
@@ -1883,7 +1890,7 @@
 (defn block-non-exist-handler
   [input]
   (fn []
-    (state/set-editor-show-block-search! false)
+    (state/clear-editor-action!)
     (cursor/move-cursor-forward input 2)))
 
 (defn- paste-block-cleanup
@@ -2053,6 +2060,44 @@
     (insert-template! element-id db-id
                       {:replace-empty-target? true})))
 
+(defn get-searching-property
+  [input]
+  (let [value (.-value input)
+        pos (util/get-selection-start input)
+        postfix (subs value pos)
+        end-index (when-let [idx (string/index-of postfix "::")]
+                    (+ (max 0 (count (subs value 0 pos))) idx))
+        start-index (or (when-let [p (string/last-index-of (subs value 0 pos) "\n")]
+                          (inc p))
+                        0)]
+    {:end-index end-index
+     :searching-property (when (and start-index end-index (>= end-index start-index))
+                           (subs value start-index end-index))}))
+
+(defn property-on-chosen-handler
+  [element-id q]
+  (fn [property]
+    (when-let [input (gdom/getElement element-id)]
+      (let [{:keys [end-index searching-property]} (get-searching-property input)]
+        (cursor/move-cursor-to input (+ end-index 2))
+        (commands/insert! element-id (str (or property q) ":: ")
+                          {:last-pattern (str searching-property "::")})
+        (state/clear-editor-action!)
+        (js/setTimeout (fn []
+                         (let [pos (let [input (gdom/getElement element-id)]
+                                     (cursor/get-caret-pos input))]
+                           (state/set-editor-action-data! {:property (or property q)
+                                                           :pos pos})
+                           (state/set-editor-action! :property-value-search)))
+                       50)))))
+
+(defn property-value-on-chosen-handler
+  [element-id q]
+  (fn [property-value]
+    (commands/insert! element-id (str ":: " (or property-value q))
+                      {:last-pattern (str ":: " q)})
+    (state/clear-editor-action!)))
+
 (defn parent-is-page?
   [{{:block/keys [parent page]} :data :as node}]
   {:pre [(tree/satisfied-inode? node)]}
@@ -2252,9 +2297,9 @@
                            parent-id bounds
                            {:backward-pos backward-pos
                             :check-fn (fn [_ _ _]
-                                        (reset! commands/*slash-caret-pos new-pos)
+                                        (state/set-editor-action-data! {:pos new-pos})
                                         (commands/handle-step [:editor/search-page]))}))]
-        (state/set-editor-show-page-search! false)
+        (state/clear-editor-action!)
         (let [selection (get-selection-and-format)
               {:keys [selection-start selection-end selection]} selection]
           (if selection
@@ -2285,9 +2330,9 @@
                             parent-id bounds
                             {:backward-pos backward-pos
                              :check-fn     (fn [_ _ _]
-                                             (reset! commands/*slash-caret-pos new-pos)
+                                             (state/set-editor-action-data! {:pos new-pos})
                                              (commands/handle-step [:editor/search-block]))}))]
-        (state/set-editor-show-block-search! false)
+        (state/clear-editor-action!)
         (if-let [embed-ref (thingatpt/embed-macro-at-point input)]
           (let [{:keys [raw-content start end]} embed-ref]
             (delete-and-update input start end)
@@ -2568,16 +2613,14 @@
            (= (util/nth-safe value (dec current-pos)) (state/get-editor-command-trigger)))
       (do
         (util/stop e)
-        (reset! *slash-caret-pos nil)
-        (reset! *show-commands false)
+        (commands/restore-state)
         (delete-and-update input (dec current-pos) current-pos))
 
       (and (> current-pos 1)
            (= (util/nth-safe value (dec current-pos)) commands/angle-bracket))
       (do
         (util/stop e)
-        (reset! *angle-bracket-caret-pos nil)
-        (reset! *show-block-commands false)
+        (commands/restore-state)
         (delete-and-update input (dec current-pos) current-pos))
 
       ;; pair
@@ -2595,10 +2638,10 @@
         (commands/delete-pair! id)
         (cond
           (and (= deleted "[") (state/get-editor-show-page-search?))
-          (state/set-editor-show-page-search! false)
+          (state/clear-editor-action!)
 
           (and (= deleted "(") (state/get-editor-show-block-search?))
-          (state/set-editor-show-block-search! false)
+          (state/clear-editor-action!)
 
           :else
           nil))
@@ -2606,7 +2649,7 @@
       ;; deleting hashtag
       (and (= deleted "#") (state/get-editor-show-page-search-hashtag?))
       (do
-        (state/set-editor-show-page-search-hashtag! false)
+        (state/clear-editor-action!)
         (delete-and-update input (dec current-pos) current-pos))
 
       ;; just delete
@@ -2635,9 +2678,7 @@
   (fn [e]
     (cond
       (state/editing?)
-      (when (and (not (state/get-editor-show-input))
-                 (not (state/get-editor-show-date-picker?))
-                 (not (state/get-editor-show-template-search?)))
+      (when-not (state/get-editor-action)
         (util/stop e)
         (indent-outdent (not (= :left direction))))
 
@@ -2688,7 +2729,7 @@
         (and (= key "#")
              (and (> pos 0)
                   (= "#" (util/nth-safe value (dec pos)))))
-        (state/set-editor-show-page-search-hashtag! false)
+        (state/clear-editor-action!)
 
         (and (contains? (set/difference (set (keys reversed-autopair-map))
                                         #{"`"})
@@ -2720,7 +2761,7 @@
           (if (= key "#")
             (state/set-editor-last-pos! (inc (cursor/pos input))) ;; In keydown handler, the `#` is not inserted yet.
             (state/set-editor-last-pos! (cursor/pos input)))
-          (reset! commands/*slash-caret-pos (cursor/get-caret-pos input)))
+          (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)}))
 
         (let [sym "$"]
           (and (= key sym)
@@ -2759,92 +2800,92 @@
             blank-selected? (string/blank? (util/get-selected-text))
             is-processed? (util/event-is-composing? e true) ;; #3440
             non-enter-processed? (and is-processed? ;; #3251
-                                      (not= code keycode/enter-code))] ;; #3459
-        (when-not (or (state/get-editor-show-input) non-enter-processed?)
-          (cond
-            (and (not (contains? #{"ArrowDown" "ArrowLeft" "ArrowRight" "ArrowUp"} k))
-                 (not (:editor/show-page-search? @state/state))
-                 (not (:editor/show-page-search-hashtag? @state/state))
-                 (wrapped-by? input "[[" "]]"))
-            (let [orig-pos (cursor/get-caret-pos input)
-                  value (gobj/get input "value")
-                  square-pos (string/last-index-of (subs value 0 (:pos orig-pos)) "[[")
-                  pos (+ square-pos 2)
-                  _ (state/set-editor-last-pos! pos)
-                  pos (assoc orig-pos :pos pos)
-                  command-step (if (= \# (util/nth-safe value (dec square-pos)))
-                                 :editor/search-page-hashtag
-                                 :editor/search-page)]
-              (commands/handle-step [command-step])
-              (reset! commands/*slash-caret-pos pos))
-
-            (and blank-selected?
-                 (contains? keycode/left-square-brackets-keys k)
-                 (= (:key last-key-code) k)
-                 (> current-pos 0)
-                 (not (wrapped-by? input "[[" "]]")))
-            (do
-              (commands/handle-step [:editor/input "[[]]" {:backward-truncate-number 2
-                                                           :backward-pos 2}])
-              (commands/handle-step [:editor/search-page])
-              (reset! commands/*slash-caret-pos (cursor/get-caret-pos input)))
-
-            (and blank-selected?
-                 (contains? keycode/left-paren-keys k)
-                 (= (:key last-key-code) k)
-                 (> current-pos 0)
-                 (not (wrapped-by? input "((" "))")))
-            (do
-              (commands/handle-step [:editor/input "(())" {:backward-truncate-number 2
-                                                           :backward-pos 2}])
-              (commands/handle-step [:editor/search-block :reference])
-              (reset! commands/*slash-caret-pos (cursor/get-caret-pos input)))
-
-            (and (= "〈" c)
-                 (= "《" (util/nth-safe value (dec (dec current-pos))))
-                 (> current-pos 0))
-            (do
-              (commands/handle-step [:editor/input commands/angle-bracket {:last-pattern "《〈"
-                                                                           :backward-pos 0}])
-              (reset! commands/*angle-bracket-caret-pos (cursor/get-caret-pos input))
-              (reset! commands/*show-block-commands true))
-
-            (and (= c " ")
-                 (or (= (util/nth-safe value (dec (dec current-pos))) "#")
-                     (not (state/get-editor-show-page-search?))
-                     (and (state/get-editor-show-page-search?)
-                          (not= (util/nth-safe value current-pos) "]"))))
-            (state/set-editor-show-page-search-hashtag! false)
-
-            (and @*show-commands (not= k (state/get-editor-command-trigger)))
-            (let [matched-commands (get-matched-commands input)]
-              (if (seq matched-commands)
+                                      (not= code keycode/enter-code)) ;; #3459
+            editor-action (state/get-editor-action)]
+        (cond
+          (and (= :commands (state/get-editor-action)) (not= k (state/get-editor-command-trigger)))
+          (let [matched-commands (get-matched-commands input)]
+            (if (seq matched-commands)
+              (reset! commands/*matched-commands matched-commands)
+              (state/clear-editor-action!)))
+
+          (and (= :block-commands editor-action) (not= key-code 188)) ; not <
+          (let [matched-block-commands (get-matched-block-commands input)]
+            (if (seq matched-block-commands)
+              (cond
+                (= key-code 9)       ;tab
                 (do
-                  (reset! *show-commands true)
-                  (reset! commands/*matched-commands matched-commands))
-                (reset! *show-commands false)))
-
-            (and @*show-block-commands (not= key-code 188)) ; not <
-            (let [matched-block-commands (get-matched-block-commands input)]
-              (if (seq matched-block-commands)
-                (cond
-                  (= key-code 9)       ;tab
-                  (when @*show-block-commands
-                    (util/stop e)
-                    (insert-command! input-id
-                                     (last (first matched-block-commands))
-                                     format
-                                     {:last-pattern commands/angle-bracket}))
-
-                  :else
-                  (reset! commands/*matched-block-commands matched-block-commands))
-                (reset! *show-block-commands false)))
-
-            (nil? @search-timeout)
-            (close-autocomplete-if-outside input)
+                  (util/stop e)
+                  (insert-command! input-id
+                                   (last (first matched-block-commands))
+                                   format
+                                   {:last-pattern commands/angle-bracket}))
 
-            :else
-            nil))
+                :else
+                (reset! commands/*matched-block-commands matched-block-commands))
+              (state/clear-editor-action!)))
+
+          (and (contains? #{:commands :block-commands} (state/get-editor-action))
+               (= c (util/nth-safe value (dec (dec current-pos))) " "))
+          (state/clear-editor-action!)
+
+          (and (state/get-editor-show-page-search-hashtag?)
+               (= c " "))
+          (state/clear-editor-action!)
+
+          :else
+          (when (and (not editor-action) (not non-enter-processed?))
+            (cond
+              (and (not (contains? #{"ArrowDown" "ArrowLeft" "ArrowRight" "ArrowUp"} k))
+                   (wrapped-by? input "[[" "]]"))
+              (let [orig-pos (cursor/get-caret-pos input)
+                    value (gobj/get input "value")
+                    square-pos (string/last-index-of (subs value 0 (:pos orig-pos)) "[[")
+                    pos (+ square-pos 2)
+                    _ (state/set-editor-last-pos! pos)
+                    pos (assoc orig-pos :pos pos)
+                    command-step (if (= \# (util/nth-safe value (dec square-pos)))
+                                   :editor/search-page-hashtag
+                                   :editor/search-page)]
+                (commands/handle-step [command-step])
+                (state/set-editor-action-data! {:pos pos}))
+
+              (and blank-selected?
+                   (contains? keycode/left-square-brackets-keys k)
+                   (= (:key last-key-code) k)
+                   (> current-pos 0)
+                   (not (wrapped-by? input "[[" "]]")))
+              (do
+                (commands/handle-step [:editor/input "[[]]" {:backward-truncate-number 2
+                                                             :backward-pos 2}])
+                (commands/handle-step [:editor/search-page])
+                (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)}))
+
+              (and blank-selected?
+                   (contains? keycode/left-paren-keys k)
+                   (= (:key last-key-code) k)
+                   (> current-pos 0)
+                   (not (wrapped-by? input "((" "))")))
+              (do
+                (commands/handle-step [:editor/input "(())" {:backward-truncate-number 2
+                                                             :backward-pos 2}])
+                (commands/handle-step [:editor/search-block :reference])
+                (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)}))
+
+              (and (= "〈" c)
+                   (= "《" (util/nth-safe value (dec (dec current-pos))))
+                   (> current-pos 0))
+              (do
+                (commands/handle-step [:editor/input commands/angle-bracket {:last-pattern "《〈"
+                                                                             :backward-pos 0}])
+                (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
+                (state/set-editor-show-block-commands!))
+
+              (nil? @search-timeout)
+              (close-autocomplete-if-outside input)
+
+              :else
+              nil)))
         (when-not (or (= k "Shift") is-processed?)
           (state/set-last-key-code! {:key-code key-code
                                      :code code
@@ -2861,7 +2902,7 @@
 (defn editor-on-change!
   [block id search-timeout]
   (fn [e]
-    (if (state/sub :editor/show-block-search?)
+    (if (= :block-search (state/sub :editor/action))
       (let [timeout 300]
         (when @search-timeout
           (js/clearTimeout @search-timeout))
@@ -2952,14 +2993,18 @@
   "shortcut cut action:
   * when in selection mode, cut selected blocks
   * when in edit mode with text selected, cut selected text
-  * otherwise same as delete shortcut"
+  * otherwise nothing need to be handled."
   [e]
   (cond
     (state/selection?)
     (shortcut-cut-selection e)
 
-    (state/editing?)
-    (keydown-backspace-handler true e)))
+    (and (state/editing?) (util/input-text-selected?
+                           (gdom/getElement (state/get-edit-input-id))))
+    (keydown-backspace-handler true e)
+
+    :else
+    nil))
 
 (defn delete-selection
   [e]

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

@@ -207,7 +207,8 @@
                            (set block-properties)
                            (set all-properties))
         shown-properties (set/intersection (set all-properties) shown-properties)]
-    (state/set-modal! (query-properties-settings block shown-properties all-properties))))
+    (state/set-modal! (query-properties-settings block shown-properties all-properties)
+                      {:center? true})))
 
 (defmethod handle :modal/show-cards [_]
   (state/set-modal! srs/global-cards {:id :srs

+ 29 - 9
src/main/frontend/handler/page.cljs

@@ -74,7 +74,7 @@
    (let [p (common-handler/get-page-default-properties title)
          ps (merge p properties)
          content (page-property/insert-properties format "" ps)
-         refs (gp-block/get-page-refs-from-properties properties
+         refs (gp-block/get-page-refs-from-properties format properties
                                                       (db/get-db (state/get-current-repo))
                                                       (state/get-date-formatter))]
      {:block/uuid (db/new-block-id)
@@ -243,13 +243,21 @@
         (util/replace-ignore-case (str " " old-tag " ") (str " " new-tag " "))
         (util/replace-ignore-case (str " " old-tag "$") (str " " new-tag)))))
 
+(defn- replace-property-ref!
+  [content old-name new-name]
+  (let [new-name (keyword (string/replace (string/lower-case new-name) #"\s+" "-"))
+        old-property (str old-name "::")
+        new-property (str (name new-name) "::")]
+    (util/replace-ignore-case content old-property new-property)))
+
 (defn- replace-old-page!
   "Unsanitized names"
   [content old-name new-name]
   (when (and (string? content) (string? old-name) (string? new-name))
     (-> content
         (replace-page-ref! old-name new-name)
-        (replace-tag-ref! old-name new-name))))
+        (replace-tag-ref! old-name new-name)
+        (replace-property-ref! old-name new-name))))
 
 (defn- walk-replace-old-page!
   "Unsanitized names"
@@ -268,6 +276,9 @@
                        new-name
                        (replace-old-page! f old-name new-name))
 
+                     (and (keyword f) (= (name f) old-name))
+                     (keyword (string/replace (string/lower-case new-name) #"\s+" "-"))
+
                      :else
                      f))
                  form))
@@ -312,6 +323,7 @@
 (defn delete!
   [page-name ok-handler & {:keys [delete-file?]
                            :or {delete-file? true}}]
+  (route-handler/redirect-to-home!)
   (when page-name
     (when-let [repo (state/get-current-repo)]
       (let [page-name (util/page-name-sanity-lc page-name)
@@ -371,6 +383,7 @@
                                   {: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))
                                    :block/path-refs (rename-update-block-refs! (:block/path-refs block) (:db/id page) (:db/id to-page))})))) blocks)
                       (remove nil?))]
@@ -478,7 +491,11 @@
         pages (cons page pages)]
     (doseq [{:block/keys [name original-name]} pages]
       (let [old-page-title (or original-name name)
-            new-page-title (string/replace old-page-title old-name new-name)
+            ;; only replace one time, for the case that the namespace is a sub-string of the sub-namespace page name
+            ;; Example: has pages [[work]] [[work/worklog]],
+            ;; we want to rename [[work/worklog]] to [[work1/worklog]] when rename [[work]] to [[work1]],
+            ;; but don't rename [[work/worklog]] to [[work1/work1log]]
+            new-page-title (string/replace-first old-page-title old-name new-name)
             redirect? (= name (:block/name page))]
         (when (and old-page-title new-page-title)
           (p/let [_ (rename-page-aux old-page-title new-page-title redirect?)]
@@ -555,7 +572,8 @@
           (rename-namespace-pages! repo old-name new-name))
         (rename-nested-pages old-name new-name))
       (when (string/blank? new-name)
-        (notification/show! "Please use a valid name, empty name is not allowed!" :error)))))
+        (notification/show! "Please use a valid name, empty name is not allowed!" :error)))
+    (ui-handler/re-render-root!)))
 
 (defn- split-col-by-element
   [col element]
@@ -668,7 +686,7 @@
 ;; Editor
 (defn page-not-exists-handler
   [input id q current-pos]
-  (state/set-editor-show-page-search! false)
+  (state/clear-editor-action!)
   (if (state/org-mode-file-link? (state/get-current-repo))
     (let [page-ref-text (get-page-ref-text q)
           value (gobj/get input "value")
@@ -689,15 +707,17 @@
   [input id _q pos format]
   (let [current-pos (cursor/pos input)
         edit-content (state/sub [:editor/content id])
+        action (state/get-editor-action)
+        hashtag? (= action :page-search-hashtag)
         q (or
            @editor-handler/*selected-text
-           (when (state/sub :editor/show-page-search-hashtag?)
+           (when hashtag?
              (gp-util/safe-subs edit-content pos current-pos))
            (when (> (count edit-content) current-pos)
              (gp-util/safe-subs edit-content pos current-pos)))]
-    (if (state/sub :editor/show-page-search-hashtag?)
+    (if hashtag?
       (fn [chosen _click?]
-        (state/set-editor-show-page-search! false)
+        (state/clear-editor-action!)
         (let [wrapped? (= "[[" (gp-util/safe-subs edit-content (- pos 2) pos))
               chosen (if (string/starts-with? chosen "New page: ") ;; FIXME: What if a page named "New page: XXX"?
                        (subs chosen 10)
@@ -719,7 +739,7 @@
                                            :end-pattern (when wrapped? "]]")
                                            :forward-pos forward-pos})))
       (fn [chosen _click?]
-        (state/set-editor-show-page-search! false)
+        (state/clear-editor-action!)
         (let [chosen (if (string/starts-with? chosen "New page: ")
                        (subs chosen 10)
                        chosen)

+ 33 - 5
src/main/frontend/search.cljs

@@ -3,6 +3,7 @@
             [clojure.string :as string]
             [logseq.graph-parser.config :as gp-config]
             [frontend.db :as db]
+            [frontend.db.model :as db-model]
             [frontend.regex :as regex]
             [frontend.search.browser :as search-browser]
             [frontend.search.db :as search-db :refer [indices]]
@@ -155,11 +156,38 @@
   ([q]
    (template-search q 10))
   ([q limit]
-   (let [q (clean-str q)
-         templates (db/get-all-templates)]
-     (when (seq templates)
-       (let [result (fuzzy-search (keys templates) q :limit limit)]
-         (vec (select-keys templates result)))))))
+   (when q
+     (let [q (clean-str q)
+           templates (db/get-all-templates)]
+       (when (seq templates)
+         (let [result (fuzzy-search (keys templates) q :limit limit)]
+           (vec (select-keys templates result))))))))
+
+(defn property-search
+  ([q]
+   (property-search q 10))
+  ([q limit]
+   (when q
+     (let [q (clean-str q)
+           properties (map name (db-model/get-all-properties))]
+       (when (seq properties)
+         (if (string/blank? q)
+           properties
+           (let [result (fuzzy-search properties q :limit limit)]
+             (vec result))))))))
+
+(defn property-value-search
+  ([property q]
+   (property-value-search property q 10))
+  ([property q limit]
+   (when q
+     (let [q (clean-str q)
+           result (db-model/get-property-values (keyword property))]
+       (when (seq result)
+         (if (string/blank? q)
+           result
+           (let [result (fuzzy-search result q :limit limit)]
+             (vec result))))))))
 
 (defn sync-search-indice!
   [repo tx-report]

+ 38 - 46
src/main/frontend/state.cljs

@@ -97,12 +97,9 @@
      :config                                {}
      :block/component-editing-mode?         false
      :editor/draw-mode?                     false
-     :editor/show-page-search?              false
-     :editor/show-page-search-hashtag?      false
-     :editor/show-date-picker?              false
+     :editor/action                         nil
+     :editor/action-data                    nil
      ;; With label or other data
-     :editor/show-input                     nil
-     :editor/show-zotero                    false
      :editor/last-saved-cursor              nil
      :editor/editing?                       nil
      :editor/in-composition?                false
@@ -598,63 +595,58 @@
   [value]
   (set-state! :search/mode value))
 
-(defn set-editor-show-page-search!
+(defn set-editor-action!
   [value]
-  (set-state! :editor/show-page-search? value))
+  (set-state! :editor/action value))
+
+(defn set-editor-action-data!
+  [value]
+  (set-state! :editor/action-data value))
+
+(defn get-editor-action
+  []
+  (:editor/action @state))
+
+(defn get-editor-action-data
+  []
+  (:editor/action-data @state))
 
 (defn get-editor-show-page-search?
   []
-  (get @state :editor/show-page-search?))
+  (= (get-editor-action) :page-search))
 
-(defn set-editor-show-page-search-hashtag!
-  [value]
-  (set-state! :editor/show-page-search? value)
-  (set-state! :editor/show-page-search-hashtag? value))
 (defn get-editor-show-page-search-hashtag?
   []
-  (get @state :editor/show-page-search-hashtag?))
-(defn set-editor-show-block-search!
-  [value]
-  (set-state! :editor/show-block-search? value))
+  (= (get-editor-action) :page-search-hashtag))
+
 (defn get-editor-show-block-search?
   []
-  (get @state :editor/show-block-search?))
-(defn set-editor-show-template-search!
-  [value]
-  (set-state! :editor/show-template-search? value))
-(defn get-editor-show-template-search?
-  []
-  (get @state :editor/show-template-search?))
-(defn set-editor-show-date-picker!
-  [value]
-  (set-state! :editor/show-date-picker? value))
-(defn get-editor-show-date-picker?
-  []
-  (get @state :editor/show-date-picker?))
+  (= (get-editor-action) :block-search))
+
 (defn set-editor-show-input!
   [value]
-  (set-state! :editor/show-input value))
+  (if value
+    (do
+      (set-editor-action-data! (assoc (get-editor-action-data) :options value))
+      (set-editor-action! :input))
+    (do
+      (set-editor-action! nil)
+      (set-editor-action-data! nil))))
 (defn get-editor-show-input
   []
-  (get @state :editor/show-input))
-
-
-(defn set-editor-show-zotero!
-  [value]
-  (set-state! :editor/show-zotero value))
+  (when (= (get-editor-action) :input)
+    (get @state :editor/action-data)))
+(defn set-editor-show-commands!
+  []
+  (when-not (get-editor-action) (set-editor-action! :commands)))
+(defn set-editor-show-block-commands!
+  []
+  (when-not (get-editor-action) (set-editor-action! :block-commands)))
 
-;; TODO: refactor, use one state
-(defn clear-editor-show-state!
+(defn clear-editor-action!
   []
   (swap! state (fn [state]
-                 (assoc state
-                        :editor/show-input nil
-                        :editor/show-zotero false
-                        :editor/show-date-picker? false
-                        :editor/show-block-search? false
-                        :editor/show-template-search? false
-                        :editor/show-page-search? false
-                        :editor/show-page-search-hashtag? false))))
+                 (assoc state :editor/action nil))))
 
 (defn set-edit-input-id!
   [input-id]

+ 19 - 35
src/main/frontend/ui.cljs

@@ -25,7 +25,7 @@
             ["react-tippy" :as react-tippy]
             ["react-transition-group" :refer [CSSTransition TransitionGroup]]
             ["@logseq/react-tweet-embed" :as react-tweet-embed]
-            ["react-visibility-sensor" :as rvs]
+            ["react-intersection-observer" :as react-intersection-observer]
             [rum.core :as rum]
             [frontend.db-mixins :as db-mixins]
             [frontend.mobile.util :as mobile-util]
@@ -38,7 +38,7 @@
 (def resize-consumer (r/adapt-class (gobj/get Resize "ResizeConsumer")))
 (def Tippy (r/adapt-class (gobj/get react-tippy "Tooltip")))
 (def ReactTweetEmbed (r/adapt-class react-tweet-embed))
-(def visibility-sensor (r/adapt-class (gobj/get rvs "default")))
+(def useInView (gobj/get react-intersection-observer "useInView"))
 
 (defn reset-ios-whole-page-offset!
   []
@@ -65,10 +65,7 @@
                             (plugin-handler/hook-plugin-editor :input-selection-end (bean/->js e)))))))
                 state)}
   [{:keys [on-change] :as props}]
-  (let [skip-composition? (or
-                           (state/sub :editor/show-page-search?)
-                           (state/sub :editor/show-block-search?)
-                           (state/sub :editor/show-template-search?))
+  (let [skip-composition? (state/sub :editor/action)
         on-composition (fn [e]
                          (if skip-composition?
                            (on-change e)
@@ -400,11 +397,13 @@
            get-group-name
            empty-placeholder
            item-render
-           class]}]
+           class
+           header]}]
   (let [current-idx (get state ::current-idx)]
     [:div#ui__ac {:class class}
      (if (seq matched)
        [:div#ui__ac-inner.hide-scrollbar
+        (when header header)
         (for [[idx item] (medley/indexed matched)]
           [:<>
            {:key idx}
@@ -901,13 +900,10 @@
      label-right]]
    (progress-bar width)])
 
-(rum/defcs lazy-visible-inner < rum/reactive
-  {:init (fn [state]
-           (assoc state
-                  ::ref (atom nil)))}
-  [state visible? content-fn]
+(rum/defcs lazy-visible-inner
+  [state visible? content-fn ref]
   [:div.lazy-visibility
-   {:ref #(reset! (::ref state) %)
+   {:ref ref
     :style {:min-height 24}}
    (if visible?
      (when (fn? content-fn)
@@ -925,25 +921,13 @@
           [:div.h-2.bg-base-4.rounded.col-span-1]]
          [:div.h-2.bg-base-4.rounded]]]]])])
 
-(rum/defcs lazy-visible <
-  (rum/local false ::visible?)
-  (rum/local true ::active?)
-  [state content-fn sensor-opts {:keys [once?]}]
-  (let [*active? (::active? state)]
-    (if (or (util/mobile?) (mobile-util/native-platform?))
-      (content-fn)
-      (let [*visible? (::visible? state)]
-        (visibility-sensor
-         (merge
-          {:on-change (fn [v]
-                        (reset! *visible? v)
-                        (when (and once? v)
-                          (reset! *active? false)))
-           :partialVisibility true
-           :offset {:top -300
-                    :bottom -300}
-           :scrollCheck true
-           :scrollThrottle 500
-           :active @*active?}
-          sensor-opts)
-         (lazy-visible-inner @*visible? content-fn))))))
+(rum/defc lazy-visible
+  [content-fn]
+  (let [[hasBeenSeen setHasBeenSeen] (rum/use-state false)
+        inViewState (useInView #js {:rootMargin "100px"
+                                    :onChange (fn [v entry]
+                                                (let [self-top (.-top (.-boundingClientRect entry))
+                                                      v (if v v (if (> self-top 0) false true))]
+                                                  (setHasBeenSeen v)))})
+        ref (.-ref inViewState)]
+    (lazy-visible-inner hasBeenSeen content-fn ref)))

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

@@ -312,6 +312,11 @@
   (when input
     (.-selectionEnd input)))
 
+(defn input-text-selected?
+  [input]
+  (not= (get-selection-start input)
+        (get-selection-end input)))
+
 (defn get-selection-direction
   [input]
   (when input

+ 4 - 3
src/main/frontend/util/cursor.cljs

@@ -111,9 +111,10 @@
 (defn beginning-of-line?
   [input]
   (let [[content pos] (get-input-content&pos input)]
-    (or (zero? pos)
-        (when-let [pre-char (subs content (dec pos) pos)]
-          (= pre-char \newline)))))
+    (when content
+      (or (zero? pos)
+         (when-let [pre-char (subs content (dec pos) pos)]
+           (= pre-char \newline))))))
 
 (defn move-cursor-to-line-end
   [input]

+ 10 - 8
src/main/frontend/util/text.cljs

@@ -86,19 +86,21 @@
 
 (defn get-string-all-indexes
   "Get all indexes of `value` in the string `s`."
-  [s value]
-  (loop [acc []
-         i 0]
-    (if-let [i (string/index-of s value i)]
-      (recur (conj acc i) (+ i (count value)))
-      acc)))
+  [s value {:keys [before?] :or {before? true}}]
+  (if (= value "")
+    (if before? [0] [(dec (count s))])
+    (loop [acc []
+          i 0]
+     (if-let [i (string/index-of s value i)]
+       (recur (conj acc i) (+ i (count value)))
+       acc))))
 
 (defn wrapped-by?
   "`pos` must be wrapped by `before` and `and` in string `value`, e.g. ((a|b))"
   [value pos before end]
-  (let [before-matches (->> (get-string-all-indexes value before)
+  (let [before-matches (->> (get-string-all-indexes value before {:before? true})
                             (map (fn [i] [i :before])))
-        end-matches (->> (get-string-all-indexes value end)
+        end-matches (->> (get-string-all-indexes value end {:before? false})
                          (map (fn [i] [i :end])))
         indexes (sort-by first (concat before-matches end-matches [[pos :between]]))
         ks (map second indexes)

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

@@ -1,3 +1,3 @@
 (ns frontend.version)
 
-(defonce version "0.7.5")
+(defonce version "0.7.6")

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

@@ -394,7 +394,7 @@ tags: other
                 (dsl-query "(or [[tag2]] [[page 3]])")))
         "OR query with nonexistent page should return meaningful results")
 
-    (is (= ["foo:: bar\n" "b1 [[page 1]] #tag2" "b3"]
+    (is (= ["b1 [[page 1]] #tag2" "foo:: bar\n" "b3"]
            (->> (dsl-query "(not [[page 2]])")
                 ;; Only filter to page1 to get meaningful results
                 (filter #(= "page1" (get-in % [:block/page :block/name])))

+ 8 - 1
src/test/frontend/extensions/calc_test.cljc

@@ -101,7 +101,14 @@
       0.0  "acos(cos(0))"
       5.0  "2 * log(10) + 3"
       1.0  "-2 * log(10) + 3"
-      10.0 "ln(1) + 10")))
+      10.0 "ln(1) + 10"))
+  (testing "avoiding rounding errors"
+    (are [value expr] (= value (run expr))
+      3.3 "1.1 + 2.2"
+      2.2 "3.3 - 1.1"
+      0.0001 "1/10000"
+      1e-7 "1/10000000"
+      )))
 
 (deftest variables
   (testing "variables can be remembered"

+ 3 - 3
src/test/frontend/util/text_test.cljs

@@ -23,11 +23,11 @@
 (deftest get-string-all-indexes
   []
   (are [x y] (= x y)
-    (text-util/get-string-all-indexes "[[hello]] [[world]]" "[[")
+    (text-util/get-string-all-indexes "[[hello]] [[world]]" "[[" {})
     [0 10]
 
-    (text-util/get-string-all-indexes "abc abc ab" "ab")
+    (text-util/get-string-all-indexes "abc abc ab" "ab" {})
     [0 4 8]
 
-    (text-util/get-string-all-indexes "a.c a.c ab" "a.")
+    (text-util/get-string-all-indexes "a.c a.c ab" "a." {})
     [0 4]))

+ 13 - 8
yarn.lock

@@ -6623,7 +6623,7 @@ prompts@^2.3.2:
     kleur "^3.0.3"
     sisteransi "^1.0.5"
 
[email protected], prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
[email protected], prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.2:
   version "15.8.1"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
   integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -6811,6 +6811,11 @@ [email protected]:
     react-draggable "3.x"
     react-resizable "1.x"
 
[email protected]:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/react-icon-base/-/react-icon-base-2.1.0.tgz#a196e33fdf1e7aaa1fda3aefbb68bdad9e82a79d"
+  integrity sha512-9wwKJa2LB8ujtJB5MAXYYEM7JfYThZTj0YnfGxzLLWkifaLIGc7iTde2EpJ7ka5MjneRHnlxbIn5VV9k2WjUVA==
+
 react-icon-base@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/react-icon-base/-/react-icon-base-2.1.2.tgz#a17101dad9c1192652356096860a9ab43a0766c7"
@@ -6820,6 +6825,13 @@ [email protected]:
   version "2.2.7"
   resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-2.2.7.tgz#d7860826b258557510dac10680abea5ca23cf650"
   integrity sha512-0n4lcGqzJFcIQLoQytLdJCE0DKSA9dkwEZRYoGrIDJZFvIT6Hbajx5mv9geqhqFiNjUgtxg8kPyDfjlhymbGFg==
+  dependencies:
+    react-icon-base "2.1.0"
+
+react-intersection-observer@^9.3.5:
+  version "9.3.5"
+  resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.3.5.tgz#df97584c1ef1549a47d4af6380db2fb4b76d7bba"
+  integrity sha512-TiJXVUapzAaIrZCAMBLjyWvwGYNGm0Xpkcwm3NY23b9PsJEBavul0hRFmrwc/LOmBUA/8TlkjCj7lCvjM0q1Hg==
 
 react-is@^16.13.1, react-is@^16.3.1, react-is@^16.7.0:
   version "16.13.1"
@@ -6880,13 +6892,6 @@ [email protected]:
     loose-envify "^1.4.0"
     prop-types "^15.6.2"
 
-react-visibility-sensor@^5.1.1:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/react-visibility-sensor/-/react-visibility-sensor-5.1.1.tgz#5238380960d3a0b2be0b7faddff38541e337f5a9"
-  integrity sha512-cTUHqIK+zDYpeK19rzW6zF9YfT4486TIgizZW53wEZ+/GPBbK7cNS0EHyJVyHYacwFEvvHLEKfgJndbemWhB/w==
-  dependencies:
-    prop-types "^15.7.2"
-
 [email protected]:
   version "17.0.2"
   resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"