浏览代码

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

Peng Xiao 3 年之前
父节点
当前提交
fd94862312
共有 100 个文件被更改,包括 1887 次插入1433 次删除
  1. 4 2
      .github/workflows/build-android.yml
  2. 3 0
      .github/workflows/db.yml
  3. 3 0
      .github/workflows/graph-parser.yml
  4. 1 1
      android/app/src/main/assets/capacitor.plugins.json
  5. 4 1
      deps/db/bb.edn
  6. 1 0
      deps/db/src/logseq/db.cljs
  7. 1 0
      deps/db/src/logseq/db/default.cljs
  8. 7 2
      deps/db/src/logseq/db/schema.cljs
  9. 7 0
      deps/graph-parser/.carve/ignore
  10. 9 3
      deps/graph-parser/bb.edn
  11. 1 2
      deps/graph-parser/deps.edn
  12. 1 1
      deps/graph-parser/package.json
  13. 2 1
      deps/graph-parser/src/logseq/graph_parser.cljs
  14. 58 76
      deps/graph-parser/src/logseq/graph_parser/block.cljs
  15. 1 1
      deps/graph-parser/src/logseq/graph_parser/config.cljs
  16. 78 72
      deps/graph-parser/src/logseq/graph_parser/extract.cljc
  17. 2 2
      deps/graph-parser/src/logseq/graph_parser/log.cljs
  18. 40 87
      deps/graph-parser/src/logseq/graph_parser/mldoc.cljc
  19. 6 8
      deps/graph-parser/src/logseq/graph_parser/property.cljs
  20. 1 1
      deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs
  21. 88 161
      deps/graph-parser/src/logseq/graph_parser/text.cljs
  22. 2 1
      deps/graph-parser/src/logseq/graph_parser/utf8.cljs
  23. 1 1
      deps/graph-parser/src/logseq/graph_parser/util/block_ref.cljs
  24. 2 10
      deps/graph-parser/src/logseq/graph_parser/util/page_ref.cljs
  25. 38 28
      deps/graph-parser/test/logseq/graph_parser/block_test.cljs
  26. 28 29
      deps/graph-parser/test/logseq/graph_parser/mldoc_test.cljs
  27. 1 14
      deps/graph-parser/test/logseq/graph_parser/property_test.cljs
  28. 44 54
      deps/graph-parser/test/logseq/graph_parser/text_test.cljs
  29. 67 51
      deps/graph-parser/test/logseq/graph_parser_test.cljs
  30. 4 4
      deps/graph-parser/yarn.lock
  31. 7 0
      docs/dev-practices.md
  32. 5 0
      gulpfile.js
  33. 3 4
      package.json
  34. 9 6
      resources/css/common.css
  35. 1 0
      resources/electron.html
  36. 1 0
      resources/index.html
  37. 0 0
      resources/js/tabler.min.js
  38. 1 1
      resources/package.json
  39. 1 1
      shadow-cljs.edn
  40. 2 2
      src/electron/electron/configs.cljs
  41. 4 3
      src/electron/electron/core.cljs
  42. 15 1
      src/electron/electron/file_sync_rsapi.cljs
  43. 2 2
      src/electron/electron/git.cljs
  44. 5 5
      src/electron/electron/handler.cljs
  45. 2 2
      src/electron/electron/utils.cljs
  46. 2 2
      src/electron/electron/window.cljs
  47. 6 0
      src/main/electron/listener.cljs
  48. 15 14
      src/main/frontend/components/block.cljs
  49. 54 54
      src/main/frontend/components/block.css
  50. 24 15
      src/main/frontend/components/command_palette.css
  51. 1 1
      src/main/frontend/components/editor.cljs
  52. 14 13
      src/main/frontend/components/encryption.cljs
  53. 311 114
      src/main/frontend/components/file_sync.cljs
  54. 145 14
      src/main/frontend/components/file_sync.css
  55. 8 8
      src/main/frontend/components/header.cljs
  56. 10 10
      src/main/frontend/components/header.css
  57. 12 6
      src/main/frontend/components/page.cljs
  58. 5 5
      src/main/frontend/components/page.css
  59. 15 12
      src/main/frontend/components/plugins.cljs
  60. 5 0
      src/main/frontend/components/plugins.css
  61. 82 76
      src/main/frontend/components/query_table.cljs
  62. 4 2
      src/main/frontend/components/repo.cljs
  63. 4 4
      src/main/frontend/components/search.cljs
  64. 2 1
      src/main/frontend/components/settings.cljs
  65. 12 19
      src/main/frontend/components/settings.css
  66. 23 22
      src/main/frontend/components/sidebar.cljs
  67. 23 17
      src/main/frontend/components/sidebar.css
  68. 3 3
      src/main/frontend/date.cljs
  69. 1 1
      src/main/frontend/db.cljs
  70. 1 0
      src/main/frontend/db/model.cljs
  71. 1 1
      src/main/frontend/db/outliner.cljs
  72. 39 40
      src/main/frontend/db/query_dsl.cljs
  73. 2 2
      src/main/frontend/db/query_react.cljs
  74. 2 2
      src/main/frontend/db/react.cljs
  75. 2 2
      src/main/frontend/db/utils.cljs
  76. 21 1
      src/main/frontend/dicts.cljc
  77. 1 1
      src/main/frontend/diff.cljs
  78. 1 1
      src/main/frontend/extensions/excalidraw.cljs
  79. 3 3
      src/main/frontend/extensions/graph/pixi.cljs
  80. 1 1
      src/main/frontend/extensions/latex.cljs
  81. 2 2
      src/main/frontend/extensions/pdf/utils.cljs
  82. 1 1
      src/main/frontend/extensions/sci.cljs
  83. 1 1
      src/main/frontend/extensions/srs.cljs
  84. 3 1
      src/main/frontend/format/block.cljs
  85. 6 6
      src/main/frontend/format/mldoc.cljs
  86. 356 264
      src/main/frontend/fs/sync.cljs
  87. 1 1
      src/main/frontend/fs/watcher_handler.cljs
  88. 2 0
      src/main/frontend/handler.cljs
  89. 1 1
      src/main/frontend/handler/block.cljs
  90. 7 8
      src/main/frontend/handler/editor.cljs
  91. 1 1
      src/main/frontend/handler/editor/lifecycle.cljs
  92. 9 10
      src/main/frontend/handler/events.cljs
  93. 5 3
      src/main/frontend/handler/external.cljs
  94. 52 14
      src/main/frontend/handler/file_sync.cljs
  95. 1 1
      src/main/frontend/handler/image.cljs
  96. 10 1
      src/main/frontend/handler/metadata.cljs
  97. 2 3
      src/main/frontend/handler/page.cljs
  98. 4 4
      src/main/frontend/handler/plugin.cljs
  99. 4 3
      src/main/frontend/handler/repo.cljs
  100. 1 1
      src/main/frontend/handler/ui.cljs

+ 4 - 2
.github/workflows/build-android.yml

@@ -43,6 +43,7 @@ on:
 env:
   CLOJURE_VERSION: '1.10.1.763'
   NODE_VERSION: '16'
+  JAVA_VERSION: '11'
 
 jobs:
   build-apk:
@@ -72,9 +73,10 @@ jobs:
             ${{ runner.os }}-yarn-
 
       - name: Setup Java JDK
-        uses: actions/setup-java@v1.4.3
+        uses: actions/setup-java@v2
         with:
-          java-version: 1.8
+          distribution: 'zulu'
+          java-version: ${{ env.JAVA_VERSION }}
 
       - name: Cache clojure deps
         uses: actions/cache@v2

+ 3 - 0
.github/workflows/db.yml

@@ -91,3 +91,6 @@ jobs:
 
       - name: Lint datalog rules
         run: bb lint:rules
+
+      - name: Lint for namespaces that aren't documented
+        run: bb lint:ns-docstrings

+ 3 - 0
.github/workflows/graph-parser.yml

@@ -120,3 +120,6 @@ jobs:
 
       - name: Lint for vars that are too large
         run: bb lint:large-vars
+
+      - name: Lint for namespaces that aren't documented
+        run: bb lint:ns-docstrings

+ 1 - 1
android/app/src/main/assets/capacitor.plugins.json

@@ -37,7 +37,7 @@
 	},
 	{
 		"pkg": "@logseq/capacitor-file-sync",
-		"classpath": "com.logseq.app.FileSyncPlugin"
+		"classpath": "com.logseq.app.filesync.FileSyncPlugin"
 	},
 	{
 		"pkg": "capacitor-voice-recorder",

+ 4 - 1
deps/db/bb.edn

@@ -4,7 +4,7 @@
  {logseq/bb-tasks
   #_{:local/root "../../../bb-tasks"}
   {:git/url "https://github.com/logseq/bb-tasks"
-   :git/sha "abb32ccd26405d56fd28a29d56f3cb902b8c4334"}}
+   :git/sha "1815db538241082a01e95601e23e4290dd64d0c0"}}
 
  :pods
  {clj-kondo/clj-kondo {:version "2022.02.09"}}
@@ -19,6 +19,9 @@
   lint:carve
   logseq.bb-tasks.lint.carve/-main
 
+  lint:ns-docstrings
+  logseq.bb-tasks.lint.ns-docstrings/-main
+
   lint:rules
   {:requires ([logseq.bb-tasks.lint.datalog :as datalog]
               [logseq.db.rules :as rules])

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

@@ -1,4 +1,5 @@
 (ns logseq.db
+  "Main namespace for public db fns"
   (:require [logseq.db.default :as default-db]
             [logseq.db.schema :as db-schema]
             [datascript.core :as d]))

+ 1 - 0
deps/db/src/logseq/db/default.cljs

@@ -1,4 +1,5 @@
 (ns logseq.db.default
+  "Provides fns for seeding default data in a logseq db"
   (:require [clojure.string :as string]))
 
 (defonce built-in-pages-names

+ 7 - 2
deps/db/src/logseq/db/schema.cljs

@@ -1,4 +1,5 @@
-(ns logseq.db.schema)
+(ns logseq.db.schema
+  "Main db schema for the Logseq app")
 
 (defonce version 1)
 (defonce ast-version 1)
@@ -51,10 +52,12 @@
    ;; "A", "B", "C"
    :block/priority {}
 
-   ;; block key value properties
+   ;; map, key -> set of refs in property value or full text if none are found
    :block/properties {}
    ;; vector
    :block/properties-order {}
+   ;; map, key -> original property value's content
+   :block/properties-text-values {}
 
    ;; first block that's not a heading or unordered list
    :block/pre-block? {}
@@ -120,6 +123,7 @@
     :block/type
     :block/properties
     :block/properties-order
+    :block/properties-text-values
     :block/invalid-properties
     :block/created-at
     :block/updated-at
@@ -138,6 +142,7 @@
     :block/content
     :block/properties
     :block/properties-order
+    :block/properties-text-values
     :block/invalid-properties
     :block/alias
     :block/tags})

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

@@ -3,6 +3,8 @@ logseq.graph-parser.cli/parse-graph
 ;; For CLI
 logseq.graph-parser.mldoc/ast-export-markdown
 ;; API
+logseq.graph-parser.mldoc/link?
+;; API
 logseq.graph-parser.property/register-built-in-properties
 ;; API
 logseq.graph-parser.util.block-ref/left-and-right-parens
@@ -20,6 +22,11 @@ logseq.graph-parser.util.page-ref/->page-ref
 logseq.graph-parser.util.page-ref/get-page-name!
 ;; API
 logseq.graph-parser.config/remove-asset-protocol
+;; API
+logseq.graph-parser.util/unquote-string
+;; API
+logseq.graph-parser.util.page-ref/page-ref-re
+;; API
 logseq.graph-parser.property/->block-content
 ;; API
 logseq.graph-parser.property/property-value-from-content

+ 9 - 3
deps/graph-parser/bb.edn

@@ -3,8 +3,8 @@
  {logseq/bb-tasks
   #_{:local/root "../../../bb-tasks"}
   {:git/url "https://github.com/logseq/bb-tasks"
-   :git/sha "abb32ccd26405d56fd28a29d56f3cb902b8c4334"}}
-
+   :git/sha "1815db538241082a01e95601e23e4290dd64d0c0"}}
+ 
  :pods
  {clj-kondo/clj-kondo {:version "2022.02.09"}}
 
@@ -16,7 +16,13 @@
   logseq.bb-tasks.lint.large-vars/-main
 
   lint:carve
-  logseq.bb-tasks.lint.carve/-main}
+  logseq.bb-tasks.lint.carve/-main
+
+  lint:ns-docstrings
+  logseq.bb-tasks.lint.ns-docstrings/-main
+
+  lint:minimize-public-vars
+  logseq.bb-tasks.lint.minimize-public-vars/-main}
 
  :tasks/config
  {:large-vars

+ 1 - 2
deps/graph-parser/deps.edn

@@ -1,8 +1,7 @@
 {:paths ["src"]
  :deps
  ;; External deps should be kept in sync with https://github.com/logseq/nbb-logseq/blob/main/bb.edn
- {frankiesardo/linked {:mvn/version "1.3.0"}
-  com.andrewmcveigh/cljs-time {:git/url "https://github.com/logseq/cljs-time" ;; fork
+ {com.andrewmcveigh/cljs-time {:git/url "https://github.com/logseq/cljs-time" ;; fork
                                :sha     "5704fbf48d3478eedcf24d458c8964b3c2fd59a9"}
   ;; local dep
   logseq/db {:local/root "../db"}

+ 1 - 1
deps/graph-parser/package.json

@@ -6,7 +6,7 @@
     "@logseq/nbb-logseq": "^0.7.133"
   },
   "dependencies": {
-    "mldoc": "^1.3.9"
+    "mldoc": "^1.4.9"
   },
   "scripts": {
     "test": "nbb-logseq -cp src:test:../db/src -m logseq.graph-parser.nbb-test-runner/run-tests"

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

@@ -1,5 +1,6 @@
 (ns logseq.graph-parser
-  "Main ns used by logseq app to parse graph from source files"
+  "Main ns used by logseq app to parse graph from source files and then save to
+  the given database connection"
   (:require [datascript.core :as d]
             [logseq.graph-parser.extract :as extract]
             [logseq.graph-parser.util :as gp-util]

+ 58 - 76
deps/graph-parser/src/logseq/graph_parser/block.cljs

@@ -1,5 +1,5 @@
 (ns logseq.graph-parser.block
-  "Block related code needed for graph-parser"
+  "Given mldoc ast, prepares block data in preparation for db transaction"
   (:require [clojure.set :as set]
             [clojure.string :as string]
             [clojure.walk :as walk]
@@ -154,99 +154,76 @@
 (defn- get-page-refs-from-property-names
   [properties {:property-pages/keys [enabled? excludelist]}]
   (if (contains? #{true nil} enabled?)
-    (some->> properties
-             (map (comp name first))
-             (remove string/blank?)
-             (remove (set (map name excludelist)))
-             ;; Remove built-in properties as we don't want pages
-             ;; created for them by default
-             (remove (set (map name (into (gp-property/editable-built-in-properties)
-                                          (gp-property/hidden-built-in-properties)))))
-             distinct)
+    (sequence
+     (comp (map (comp name first))
+           (remove string/blank?)
+           (remove (set (map name excludelist)))
+           ;; Remove built-in properties as we don't want pages
+           ;; created for them by default
+           (remove (into #{}
+                         (map name)
+                         (apply conj
+                                (gp-property/editable-built-in-properties)
+                                (gp-property/hidden-built-in-properties))))
+           (distinct))
+     properties)
     []))
 
-;; TODO: Use text/parse-property to determine refs rather than maintain this similar
-;; implementation to parse-property
 (defn- get-page-ref-names-from-properties
-  [format properties user-config]
+  [properties user-config]
   (let [page-refs (->>
                    properties
                    (remove (fn [[k _]]
                              (contains?
                               (set/union (apply disj
-                                           (gp-property/editable-built-in-properties)
-                                           gp-property/editable-linkable-built-in-properties)
+                                                (gp-property/editable-built-in-properties)
+                                                gp-property/editable-linkable-built-in-properties)
                                          (gp-property/hidden-built-in-properties))
                               (keyword k))))
+                   ;; get links ast
                    (map last)
-                   (map (fn [v]
-                          (cond
-                            (and (string? v)
-                                 (not (gp-mldoc/link? format v)))
-                            (let [v (string/trim v)
-                                  result (if (:rich-property-values? user-config)
-                                           (if (gp-util/wrapped-by-quotes? v)
-                                             []
-                                             (text/extract-page-refs-and-tags v))
-                                           (text/split-page-refs-without-brackets v {:un-brackets? false}))]
-                              (if (coll? result)
-                                (map text/page-ref-un-brackets! result)
-                                []))
-
-                            (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))
+                   (mapcat (or (:extract-refs-from-property-value-fn user-config)
+                               text/extract-refs-from-mldoc-ast))
+                   ;; comma separated collections
+                   (concat (->> (map second properties)
+                                (filter coll?)
+                                (apply concat))))
         page-refs-from-property-names (get-page-refs-from-property-names properties user-config)]
     (->> (concat page-refs page-refs-from-property-names)
          (remove string/blank?)
          distinct)))
 
 (defn extract-properties
-  [format properties user-config]
+  [properties user-config]
   (when (seq properties)
     (let [properties (seq properties)
-          page-refs (get-page-ref-names-from-properties format properties user-config)
           *invalid-properties (atom #{})
           properties (->> properties
-                          (map (fn [[k v]]
-                                 (let [k (-> (string/lower-case (name k))
+                          (map (fn [[k v mldoc-ast]]
+                                 (let [k (if (or (keyword? k) (symbol? k))
+                                           (subs (str k) 1)
+                                           k)
+                                       k (-> (string/lower-case k)
                                              (string/replace " " "-")
-                                             (string/replace "_" "-")
-                                             (string/replace #"[\"|^|(|)|{|}]+" ""))]
+                                             (string/replace "_" "-"))]
                                    (if (gp-property/valid-property-name? (str ":" 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? gp-property/editable-linkable-built-in-properties k))
-                                               (set [v])
-                                               v)
-                                           v (if (coll? v) (set v) v)]
-                                       [k v])
+                                     (let [k' (keyword
+                                               (if (contains? #{"custom_id" "custom-id"} k)
+                                                 "id"
+                                                 k))
+                                           v' (text/parse-property k v mldoc-ast user-config)]
+                                       [k' v' mldoc-ast v])
                                      (do (swap! *invalid-properties conj k)
                                          nil)))))
                           (remove #(nil? (second %))))
+          page-refs (get-page-ref-names-from-properties properties user-config)
+          properties-text-values (->> (map (fn [[k _v _refs original-text]] [k original-text]) properties)
+                                      (into {}))
+          properties (map (fn [[k v _]] [k v]) properties)
           properties' (into {} properties)]
       {:properties properties'
        :properties-order (map first properties)
+       :properties-text-values properties-text-values
        :invalid-properties @*invalid-properties
        :page-refs page-refs})))
 
@@ -452,8 +429,8 @@
       (d/squuid)))
 
 (defn get-page-refs-from-properties
-  [format properties db date-formatter user-config]
-  (let [page-refs (get-page-ref-names-from-properties format properties user-config)]
+  [properties db date-formatter user-config]
+  (let [page-refs (get-page-ref-names-from-properties properties user-config)]
     (map (fn [page] (page-name->map page true db true date-formatter)) page-refs)))
 
 (defn- with-page-block-refs
@@ -532,10 +509,9 @@
      ast)
     (mapv macro->block @*result)))
 
-(defn- with-pre-block-if-exists
+(defn with-pre-block-if-exists
   [blocks body pre-block-properties encoded-content {:keys [supported-formats db date-formatter user-config]}]
   (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
@@ -544,15 +520,22 @@
                  (cons
                   (merge
                    (let [content (utf8/substring encoded-content 0 first-block-start-pos)
-                         {:keys [properties properties-order invalid-properties]} pre-block-properties
+                         {:keys [properties properties-order properties-text-values invalid-properties]} pre-block-properties
                          id (get-custom-id-or-new-id {:properties properties})
-                         property-refs (->> (get-page-refs-from-properties format properties db date-formatter user-config)
+                         property-refs (->> (get-page-refs-from-properties
+                                             properties db date-formatter
+                                             (assoc user-config
+                                                    :extract-refs-from-property-value-fn
+                                                    (fn [refs]
+                                                      (when (coll? refs)
+                                                        refs))))
                                             (map :block/original-name))
                          block {:uuid id
                                 :content content
                                 :level 1
                                 :properties properties
                                 :properties-order (vec properties-order)
+                                :properties-text-values properties-text-values
                                 :invalid-properties invalid-properties
                                 :refs property-refs
                                 :pre-block? true
@@ -587,10 +570,9 @@
                        :format format
                        :meta pos-meta)
                 (seq (:properties properties))
-                (assoc :properties (:properties properties))
-
-                (seq (:properties-order properties))
-                (assoc :properties-order (vec (:properties-order properties)))
+                (assoc :properties (:properties properties)
+                       :properties-text-values (:properties-text-values properties)
+                       :properties-order (vec (:properties-order properties)))
 
                 (seq (:invalid-properties properties))
                 (assoc :invalid-properties (:invalid-properties properties)))
@@ -647,7 +629,7 @@
                   (recur headings (rest blocks) timestamps' properties body))
 
                 (gp-property/properties-ast? block)
-                (let [properties (extract-properties format (second block) user-config)]
+                (let [properties (extract-properties (second block) (assoc user-config :format format))]
                   (recur headings (rest blocks) timestamps properties body))
 
                 (heading-block? block)

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

@@ -1,5 +1,5 @@
 (ns logseq.graph-parser.config
-  "Config that is shared between graph-parser and rest of app"
+  "App config that is shared between graph-parser and rest of app"
   (:require [clojure.set :as set]
             [clojure.string :as string]))
 

+ 78 - 72
deps/graph-parser/src/logseq/graph_parser/extract.cljc

@@ -1,4 +1,6 @@
 (ns logseq.graph-parser.extract
+  "Handles extraction of blocks, pages and mldoc ast in preparation for db
+  transaction"
   ;; Disable clj linters since we don't support clj
   #?(:clj {:clj-kondo/config {:linters {:unresolved-namespace {:level :off}
                                         :unresolved-symbol {:level :off}}}})
@@ -31,9 +33,10 @@
     (if (string/includes? file "pages/contents.")
       "Contents"
       (let [first-block (last (first (filter gp-block/heading-block? ast)))
-            property-name (when (and (contains? #{"Properties" "Property_Drawer"} (ffirst ast))
-                                     (not (string/blank? (:title (last (first ast))))))
-                            (:title (last (first ast))))
+            property-name (when (contains? #{"Properties" "Property_Drawer"} (ffirst ast))
+                            (let [properties-ast (second (first ast))
+                                  properties (zipmap (map (comp keyword first) properties-ast) (map second properties-ast))]
+                              (:title properties)))
             first-block-name (let [title (last (first (:title first-block)))]
                                (and first-block
                                     (string? title)
@@ -44,62 +47,69 @@
               (or first-block-name file-name)
               (or file-name first-block-name)))))))
 
-(defn- build-page-entity
-  [properties file page-name page ref-tags {:keys [date-formatter db from-page]}]
+(defn- extract-page-alias-and-tags
+  [page-m page page-name properties]
   (let [alias (:alias properties)
-        alias' (if (string? alias) [alias] alias)
+        alias' (if (coll? alias) alias [(str alias)])
         aliases (and alias'
                      (seq (remove #(or (= page-name (gp-util/page-name-sanity-lc %))
                                        (string/blank? %)) ;; disable blank alias
                                   alias')))
-        aliases' (->>
-                 (map
-                  (fn [alias]
-                    (let [page-name (gp-util/page-name-sanity-lc alias)
-                          aliases (distinct
-                                   (conj
-                                    (remove #{alias} aliases)
-                                    page))
-                          aliases (when (seq aliases)
-                                    (map
-                                     (fn [alias]
-                                       {:block/name (gp-util/page-name-sanity-lc alias)})
-                                     aliases))]
-                      (if (seq aliases)
-                        {:block/name page-name
-                         :block/alias aliases}
-                        {:block/name page-name})))
-                  aliases)
-                 (remove nil?))
-        [*valid-properties *invalid-properties]
+        aliases' (keep
+                   (fn [alias]
+                     (let [page-name (gp-util/page-name-sanity-lc alias)
+                           aliases (distinct
+                                    (conj
+                                     (remove #{alias} aliases)
+                                     page))
+                           aliases (when (seq aliases)
+                                     (map
+                                       (fn [alias]
+                                         {:block/name (gp-util/page-name-sanity-lc alias)})
+                                       aliases))]
+                       (if (seq aliases)
+                         {:block/name page-name
+                          :block/original-name alias
+                          :block/alias aliases}
+                         {:block/name page-name
+                          :block/original-name alias})))
+                   aliases)
+        result (cond-> page-m
+                 (seq aliases')
+                 (assoc :block/alias aliases')
+
+                 (:tags properties)
+                 (assoc :block/tags (let [tags (:tags properties)
+                                          tags (if (coll? tags) tags [(str tags)])
+                                          tags (remove string/blank? tags)]
+                                      (map (fn [tag] {:block/name (gp-util/page-name-sanity-lc tag)
+                                                      :block/original-name tag})
+                                        tags))))]
+    (update result :block/properties #(apply dissoc % gp-property/editable-linkable-built-in-properties))))
+
+(defn- build-page-map
+  [properties invalid-properties file page page-name {:keys [date-formatter db from-page]}]
+  (let [[*valid-properties *invalid-properties]
         ((juxt filter remove)
          (fn [[k _v]] (gp-property/valid-property-name? (str k))) properties)
         valid-properties (into {} *valid-properties)
-        invalid-properties (set (map (comp name first) *invalid-properties))]
+        invalid-properties (set (->> (map (comp name first) *invalid-properties)
+                                     (concat invalid-properties)))
+        page-m (->
+                (gp-util/remove-nils
+                 (assoc
+                  (gp-block/page-name->map page false db true date-formatter
+                                           :from-page from-page)
+                  :block/file {:file/path (gp-util/path-normalize file)}))
+                (extract-page-alias-and-tags page page-name properties))]
     (cond->
-     (gp-util/remove-nils
-      (assoc
-       (gp-block/page-name->map page false db true date-formatter
-                                :from-page from-page)
-       :block/file {:file/path (gp-util/path-normalize file)}))
-
-     (seq valid-properties)
-     (assoc :block/properties valid-properties)
+      page-m
 
-     (seq invalid-properties)
-     (assoc :block/invalid-properties invalid-properties)
+      (seq valid-properties)
+      (assoc :block/properties valid-properties)
 
-     (seq aliases')
-     (assoc :block/alias aliases')
-
-      (:tags properties)
-      (assoc :block/tags (let [tags (:tags properties)
-                               tags (if (string? tags) [tags] tags)
-                               tags (remove string/blank? tags)]
-                           (swap! ref-tags set/union (set tags))
-                           (map (fn [tag] {:block/name (gp-util/page-name-sanity-lc tag)
-                                           :block/original-name tag})
-                                tags))))))
+      (seq invalid-properties)
+      (assoc :block/invalid-properties invalid-properties))))
 
 ;; TODO: performance improvement
 (defn- extract-pages-and-blocks
@@ -107,10 +117,14 @@
   (try
     (let [page (get-page-name file ast page-name-order)
           [page page-name _journal-day] (gp-block/convert-page-if-journal page date-formatter)
-          blocks (->> (gp-block/extract-blocks ast content false format (dissoc options :page-name-order))
-                      (gp-block/with-parent-and-left {:block/name page-name}))
+          options' (-> options
+                       (assoc :page-name page-name
+                              :original-page-name page)
+                       (dissoc :page-name-order))
+          blocks (->> (gp-block/extract-blocks ast content false format options')
+                      (gp-block/with-parent-and-left {:block/name page-name})
+                      (vec))
           ref-pages (atom #{})
-          ref-tags (atom #{})
           blocks (map (fn [block]
                         (if (contains? #{"macro"} (:block/type block))
                           block
@@ -127,22 +141,20 @@
                                        :block/refs block-ref-pages
                                        :block/path-refs block-path-ref-pages)))))
                    blocks)
-          page-entity (build-page-entity properties file page-name page ref-tags
-                                         (assoc options :from-page page))
-          namespace-pages (let [page (:block/original-name page-entity)]
+          [properties invalid-properties] (if (:block/pre-block? (first blocks))
+                                            [(:block/properties (first blocks))
+                                             (:block/invalid-properties (first blocks))]
+                                            [properties []])
+          page-map (build-page-map properties invalid-properties file page page-name (assoc options' :from-page page))
+          namespace-pages (let [page (:block/original-name page-map)]
                             (when (text/namespace-page? page)
                               (->> (gp-util/split-namespace-pages page)
                                    (map (fn [page]
                                           (-> (gp-block/page-name->map page true db true date-formatter)
                                               (assoc :block/format format)))))))
           pages (->> (concat
-                      [page-entity]
+                      [page-map]
                       @ref-pages
-                      (map
-                       (fn [page]
-                         {:block/original-name page
-                          :block/name (gp-util/page-name-sanity-lc page)})
-                       @ref-tags)
                       namespace-pages)
                      ;; remove block references
                      (remove vector?)
@@ -165,18 +177,16 @@
           _ (when verbose (println "Parsing start: " file))
           ast (gp-mldoc/->edn content (gp-mldoc/default-config format
                                         ;; {:parse_outline_only? true}
-                                                               )
-                              user-config)]
+                                        ))]
       (when verbose (println "Parsing finished: " file))
       (let [first-block (ffirst ast)
             properties (let [properties (and (gp-property/properties-ast? first-block)
                                              (->> (last first-block)
-                                                  (map (fn [[x y]]
-                                                         [x (if (and (string? y)
-                                                                     (not (and (= (keyword x) :file-path)
-                                                                               (string/starts-with? y "file:"))))
-                                                              (text/parse-property format x y user-config)
-                                                              y)]))
+                                                  (map (fn [[x y mldoc-ast]]
+                                                         (let [k (if (keyword? x)
+                                                                   (subs (str x) 1)
+                                                                   x)]
+                                                           [(string/lower-case k) (text/parse-property k y mldoc-ast (assoc user-config :format format))])))
                                                   (into {})
                                                   (walk/keywordize-keys)))]
                          (when (and properties (seq properties))
@@ -271,7 +281,3 @@
          vals
          (map (partial apply merge))
          (with-block-uuid))))
-
-;; TODO: Properly fix this circular dependency:
-;; mldoc/->edn > text/parse-property > mldoc/link? ->mldoc/inline->edn + mldoc/default-config
-(set! gp-mldoc/parse-property text/parse-property)

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

@@ -1,6 +1,6 @@
 (ns logseq.graph-parser.log
-  "Minimal logging ns that implements basic lambdaisland.glogi fns. May use
-  glogi later if this ns is used more")
+  "Minimal, logging ns that shims lambdaisland.glogi fns for nbb. Could port
+  glogi to nbb later if this shim gets too big")
 
 (defn error [& msgs]
   (apply js/console.error (map clj->js msgs)))

+ 40 - 87
deps/graph-parser/src/logseq/graph_parser/mldoc.cljc

@@ -1,4 +1,6 @@
 (ns logseq.graph-parser.mldoc
+  "Wraps https://github.com/logseq/mldoc to parse files into mldoc ast. This ns
+  encapsulates mldoc's json api by only taking and returning edn"
   ;; Disable clj linters since we don't support clj
   #?(:clj {:clj-kondo/config {:linters {:unresolved-namespace {:level :off}
                                         :unresolved-symbol {:level :off}}}})
@@ -10,13 +12,13 @@
             [cljs-bean.core :as bean]
             [logseq.graph-parser.utf8 :as utf8]
             [clojure.string :as string]
-            [linked.core :as linked]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.config :as gp-config]))
 
 (defonce parseJson (gobj/get Mldoc "parseJson"))
 (defonce parseInlineJson (gobj/get Mldoc "parseInlineJson"))
 (defonce astExportMarkdown (gobj/get Mldoc "astExportMarkdown"))
+(defonce getReferences (gobj/get Mldoc "getReferences"))
 
 (def default-references
   (js/JSON.stringify
@@ -40,6 +42,11 @@
   [text config]
   (parseInlineJson text config))
 
+(defn get-references
+  [text config]
+  (when-not (string/blank? text)
+    (gp-util/json->clj (getReferences text config))))
+
 (defn ast-export-markdown
   [ast config references]
   (astExportMarkdown ast
@@ -93,85 +100,27 @@
               [block pos-meta])
             [block pos-meta])) ast)))
 
-(defn- ->vec
-  [s]
-  (if (string? s) [s] s))
-
-(defn- ->vec-concat
-  [& coll]
-  (->> (map ->vec coll)
-       (remove nil?)
-       (apply concat)
-       (distinct)))
-
 (defn collect-page-properties
-  [ast parse-property config-state]
-  (if (seq ast)
+  [ast config]
+  (when (seq ast)
     (let [original-ast ast
-          ast (map first ast)           ; without position meta
           directive? (fn [[item _]] (= "directive" (string/lower-case (first item))))
           grouped-ast (group-by directive? original-ast)
-          directive-ast (take-while directive? original-ast)
-          [properties-ast other-ast] (if (= "Property_Drawer" (ffirst ast))
-                                       [(last (first ast))
-                                        (rest original-ast)]
-                                       [(->> (map first directive-ast)
-                                             (map rest))
-                                        (get grouped-ast false)])
-          properties (->>
-                      properties-ast
-                      (map (fn [[k v]]
-                             (let [k (keyword (string/lower-case k))
-                                   v (if (contains? #{:title :description :filters :macro} k)
-                                       v
-                                       (parse-property k v config-state))]
-                               [k v]))))
-          properties (into (linked/map) properties)
-          macro-properties (filter (fn [x] (= :macro (first x))) properties)
-          macros (if (seq macro-properties)
-                   (->>
-                    (map
-                     (fn [[_ v]]
-                       (let [[k v] (gp-util/split-first " " v)]
-                         (mapv
-                          string/trim
-                          [k v])))
-                     macro-properties)
-                    (into {}))
-                   {})
-          properties (->> (remove (fn [x] (= :macro (first x))) properties)
-                          (into (linked/map)))
-          properties (cond-> properties
-                             (seq macros)
-                             (assoc :macros macros))
-          alias (:alias properties)
-          alias (when alias
-                  (if (coll? alias)
-                    (remove string/blank? alias)
-                    [alias]))
-          filetags (when-let [org-file-tags (:filetags properties)]
-                     (->> (string/split org-file-tags ":")
-                          (remove string/blank?)))
-          tags (:tags properties)
-          tags (->> (->vec-concat tags filetags)
-                    (remove string/blank?)
-                    vec)
-          properties (assoc properties :tags tags :alias alias)
-          properties (-> properties
-                         (update :filetags (constantly filetags)))
-          properties (into (linked/map)
-                           (remove (fn [[_k v]]
-                                     (or (nil? v) (and (coll? v) (empty? v))))
-                                   properties))]
+          [properties-ast other-ast] [(->> (get grouped-ast true)
+                                           (map first))
+                                      (get grouped-ast false)]
+          properties (map (fn [[_directive k v]]
+                            (let [kname (string/lower-case k)
+                                  k (keyword kname)
+                                  mldoc-ast (get-references v config)]
+                              [k v mldoc-ast]))
+                       properties-ast)]
       (if (seq properties)
         (cons [["Properties" properties] nil] other-ast)
-        original-ast))
-    ast))
-
-(def parse-property nil)
+        original-ast))))
 
 (defn ->edn
-  [content config config-state]
+  [content config]
   (if (string? content)
     (try
       (if (string/blank? content)
@@ -180,7 +129,7 @@
             (parse-json config)
             (gp-util/json->clj)
             (update-src-full-content content)
-            (collect-page-properties parse-property config-state)))
+            (collect-page-properties config)))
       (catch :default e
         (log/error :unexpected-error e)
         []))
@@ -197,20 +146,24 @@
     (catch :default _e
       [])))
 
+(defn ast-link?
+  [[type link]]
+  (let [[ref-type ref-value] (:url link)]
+    (and (= "Link" type)
+         (or
+          ;; 1. url
+          (not (contains? #{"Page_ref" "Block_ref"} ref-type))
+
+          (and (contains? #{"Page_ref"} ref-type)
+               (or
+                ;; 2. excalidraw link
+                (gp-config/draw? ref-value)
+
+                ;; 3. local asset link
+                (boolean (gp-config/local-asset? ref-value))))))))
+
 (defn link?
   [format link]
   (when (string? link)
-    (let [[type link] (first (inline->edn link (default-config format)))
-          [ref-type ref-value] (:url link)]
-      (and (= "Link" type)
-           (or
-            ;; 1. url
-            (not (contains? #{"Page_ref" "Block_ref"} ref-type))
-
-            (and (contains? #{"Page_ref"} ref-type)
-                 (or
-                  ;; 2. excalidraw link
-                  (gp-config/draw? ref-value)
-
-                  ;; 3. local asset link
-                  (boolean (gp-config/local-asset? ref-value)))))))))
+    (some-> (first (inline->edn link (default-config format)))
+            ast-link?)))

+ 6 - 8
deps/graph-parser/src/logseq/graph_parser/property.cljs

@@ -1,5 +1,5 @@
 (ns logseq.graph-parser.property
-  "Property fns needed by graph-parser"
+  "Core vars and util fns for properties"
   (:require [logseq.graph-parser.util :as gp-util]
             [clojure.string :as string]
             [clojure.set :as set]
@@ -15,13 +15,11 @@
        (map #(str (name (key %)) (str colons " ") (val %)))
        (string/join "\n")))
 
-(defn property-value-from-content
-  "Extracts full property value from block content"
-  [property content]
-  (second (re-find (re-pattern (str property colons "\\s+(.*)"))
-                   content)))
-
-(def valid-property-name? gp-util/valid-edn-keyword?)
+(defn valid-property-name?
+  [s]
+  [:pre (string? s)]
+  (and (gp-util/valid-edn-keyword? s)
+       (not (re-find #"[\"|^|(|)|{|}]+" s))))
 
 (defn properties-ast?
   [block]

+ 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 (= 41776 (count (d/datoms db :eavt))) "Correct datoms count")
+    (is (= 42070 (count (d/datoms db :eavt))) "Correct datoms count")
 
     (is (= 3600
            (ffirst

+ 88 - 161
deps/graph-parser/src/logseq/graph_parser/text.cljs

@@ -1,12 +1,13 @@
 (ns logseq.graph-parser.text
+  "Miscellaneous text util fns for the parser"
   (:require ["path" :as path]
             [goog.string :as gstring]
             [clojure.string :as string]
             [clojure.set :as set]
+            [logseq.graph-parser.property :as gp-property]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.util :as gp-util]
-            [logseq.graph-parser.property :as gp-property]
-            [logseq.graph-parser.util.page-ref :as page-ref :refer [right-brackets]]))
+            [logseq.graph-parser.util.page-ref :as page-ref]))
 
 (defn get-file-basename
   [path]
@@ -36,131 +37,11 @@
   [s]
   (or (get-page-name s) s))
 
-;; E.g "Foo Bar"
-(defn sep-by-comma
-  [s]
-  (when s
-    (some->>
-     (string/split s #"[\,|,]{1}")
-     (remove string/blank?)
-     (map string/trim))))
-
-(defn sep-by-hashtag
-  [s]
-  (when s
-    (some->>
-     (string/split s #"#")
-     (remove string/blank?)
-     (map string/trim))))
-
-(defn- not-matched-nested-pages
-  [s]
-  (and (string? s)
-       (> (count (re-seq page-ref/left-brackets-re s))
-          (count (re-seq page-ref/right-brackets-re s)))))
-
-(defn- ref-matched?
-  [s]
-  (let [x (re-seq page-ref/left-brackets-re s)
-        y (re-seq page-ref/right-brackets-re s)]
-    (and (> (count x) 0) (= (count x) (count y)))))
-
 (defn get-nested-page-name
   [page-name]
   (when-let [first-match (re-find page-ref/page-ref-without-nested-re page-name)]
     (second first-match)))
 
-(defn- concat-nested-pages
-  [coll]
-  (first
-   (reduce (fn [[acc not-matched-s] s]
-             (cond
-               (and not-matched-s (= s right-brackets))
-               (let [s' (str not-matched-s s)]
-                 (if (ref-matched? s')
-                   [(conj acc s') nil]
-                   [acc s']))
-
-               not-matched-s
-               [acc (str not-matched-s s)]
-
-               (not-matched-nested-pages s)
-               [acc s]
-
-               :else
-               [(conj acc s) not-matched-s])) [[] nil] coll)))
-
-(defn- sep-by-quotes
-  [s]
-  (string/split s #"(\"[^\"]*\")"))
-
-(def markdown-link #"\[([^\[]+)\](\(.*\))")
-
-(defn split-page-refs-without-brackets
-  ([s]
-   (split-page-refs-without-brackets s {}))
-  ([s {:keys [un-brackets?]
-       :or {un-brackets? true}}]
-   (cond
-     (and (string? s) (gp-util/wrapped-by-quotes? s))
-     (gp-util/unquote-string s)
-
-     (and (string? s) (re-find markdown-link s))
-     s
-
-     (and (string? s)
-            ;; Either a page ref, a tag or a comma separated collection
-            (or (re-find page-ref/page-ref-re s)
-                (re-find #"[\,|,|#|\"]+" s)))
-     (let [result (->> (sep-by-quotes s)
-                       (mapcat
-                        (fn [s]
-                          (when-not (gp-util/wrapped-by-quotes? (string/trim s))
-                            (string/split s page-ref/page-ref-outer-capture-re))))
-                       (mapcat (fn [s]
-                                 (cond
-                                   (gp-util/wrapped-by-quotes? s)
-                                   nil
-
-                                   (string/includes? (string/trimr s)
-                                                     (str right-brackets ","))
-                                   (let [idx (string/index-of s (str right-brackets ","))]
-                                     [(subs s 0 idx)
-                                      right-brackets
-                                      (subs s (+ idx 3))])
-
-                                   :else
-                                   [s])))
-                       (remove #(= % ""))
-                       (mapcat (fn [s] (if (string/ends-with? s right-brackets)
-                                         [(subs s 0 (- (count s) 2))
-                                          right-brackets]
-                                         [s])))
-                       concat-nested-pages
-                       (remove string/blank?)
-                       (mapcat (fn [s]
-                                 (cond
-                                   (gp-util/wrapped-by-quotes? s)
-                                   nil
-
-                                   (page-ref/page-ref? s)
-                                   [(if un-brackets? (page-ref-un-brackets! s) s)]
-
-                                   :else
-                                   (->> (sep-by-comma s)
-                                        (mapcat sep-by-hashtag)))))
-                       (distinct))]
-       (if (or (coll? result)
-               (and (string? result)
-                    (string/starts-with? result "#")))
-         (let [result (if coll? result [result])
-               result (map (fn [s] (string/replace s #"^#+" "")) result)]
-           (set result))
-         (first result)))
-
-     :else
-     s)))
-
 (defn- remove-level-space-aux!
   [text pattern space? trim-left?]
   (let [pattern (gstring/format
@@ -213,49 +94,95 @@
     (re-find #"^\d+$" v)
     (parse-long v)))
 
-(def ^:private page-ref-or-tag-re
-  (re-pattern (str "#?" (page-ref/->page-ref-re-str "(.*?)") "|"
-                   ;; Don't capture punctuation at end of a tag
-                   "#([\\S]+[^\\s.!,])")))
+(defn- get-ref-from-ast
+  [[typ data]]
+  (case typ
+    "Link"
+    (case (first (:url data))
+      "Page_ref"
+      (second (:url data))
 
-(defn extract-page-refs-and-tags
-  "Returns set of page-refs and tags in given string or returns string if none
-  are found"
-  [string]
-  (let [refs (map #(or (second %) (get % 2))
-                  (re-seq page-ref-or-tag-re string))]
-    (if (seq refs) (set refs) string)))
+      "Search"
+      (second (:url data))
 
-(defn parse-property
-  "Property value parsing that takes into account built-in properties, format
-  and user config"
-  ([k v config-state]
-   (parse-property :markdown k v config-state))
-  ([format k v config-state]
-   (let [k (name k)
-         v (if (or (symbol? v) (keyword? v)) (name v) (str v))
-         v (string/trim v)]
-     (cond
-       (contains? (set/union
-                   #{"title" "filters"}
-                   (get config-state :ignored-page-references-keywords)) k)
-       v
+      nil)
 
-       (gp-util/wrapped-by-quotes? v) ; wrapped in ""
-       v
+    "Nested_link"
+    (page-ref/get-page-name (:content data))
 
-       (contains? @non-parsing-properties (string/lower-case k))
-       v
+    "Tag"
+    (if (= "Plain" (ffirst data))
+      (second (first data))
+      (get-ref-from-ast (first data)))
 
-       (gp-mldoc/link? format v)
-       v
+    nil))
 
-       (contains? gp-property/editable-linkable-built-in-properties (keyword k))
-       (split-page-refs-without-brackets v)
+(defn extract-refs-from-mldoc-ast
+  [v]
+  (into #{}
+        (comp
+         (remove gp-mldoc/ast-link?)
+         (keep get-ref-from-ast)
+         (map string/trim))
+        v))
+
+(defn- sep-by-comma
+  [s]
+  {:pre (string? s)}
+  (->>
+   (string/split s #"[\,,]{1}")
+   (map string/trim)
+   (remove string/blank?)
+   (set)))
+
+(defn separated-by-commas?
+  [config-state k]
+  (let [k' (if (keyword? k) k (keyword k))]
+    (contains? (set/union gp-property/editable-linkable-built-in-properties
+                          (set (get config-state :property/separated-by-commas)))
+               k')))
+
+(defn- extract-refs-by-commas
+  [v format]
+  (let [plains (->> (map first (gp-mldoc/->edn v (gp-mldoc/default-config format)))
+                    first
+                    second
+                    (filter #(and (vector? %) (= "Plain" (first %))))
+                    (map second))]
+    (set (mapcat sep-by-comma plains))))
 
-       :else
-       (if-some [res (parse-non-string-property-value v)]
-         res
-         (if (:rich-property-values? config-state)
-           (extract-page-refs-and-tags v)
-           (split-page-refs-without-brackets v)))))))
+(defn parse-property
+  "Property value parsing that takes into account built-in properties, and user config"
+  [k v mldoc-references-ast config-state]
+  (let [refs (extract-refs-from-mldoc-ast mldoc-references-ast)
+        property-separated-by-commas? (separated-by-commas? config-state k)
+        refs' (if property-separated-by-commas?
+                (->> (extract-refs-by-commas v (get config-state :format :markdown))
+                     (set/union refs))
+                refs)
+        k (if (or (symbol? k) (keyword? k)) (subs (str k) 1) k)
+        v (string/trim (str v))
+        non-string-property (parse-non-string-property-value v)]
+    (cond
+      (contains? (set/union
+                  #{"filters" "macro"}
+                  (get config-state :ignored-page-references-keywords)) k)
+      v
+
+      (@non-parsing-properties k)
+      v
+
+      (string/blank? v)
+      nil
+
+      (and (string? v) (gp-util/wrapped-by-quotes? v))
+      v
+
+      (seq refs')
+      refs'
+
+      (some? non-string-property)
+      non-string-property
+
+      :else
+      v)))

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

@@ -1,4 +1,5 @@
-(ns logseq.graph-parser.utf8)
+(ns logseq.graph-parser.utf8
+  "Utf8 utilities used by the parser")
 
 (defonce encoder
   (js/TextEncoder. "utf-8"))

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

@@ -1,5 +1,5 @@
 (ns logseq.graph-parser.util.block-ref
-  "General purpose vars and util fns for block-refs"
+  "Core vars and util fns for block-refs"
   (:require [clojure.string :as string]))
 
 (def left-parens "Opening characters for block-ref" "((")

+ 2 - 10
deps/graph-parser/src/logseq/graph_parser/util/page_ref.cljs

@@ -1,6 +1,6 @@
 (ns logseq.graph-parser.util.page-ref
-  "General purpose vars and util fns for page-ref. Currently this only handles
-a logseq page-ref e.g. [[page name]]"
+  "Core vars and util fns for page-ref. Currently this only handles a logseq
+  page-ref e.g. [[page name]]"
   (:require [clojure.string :as string]))
 
 (def left-brackets "Opening characters for page-ref" "[[")
@@ -9,10 +9,7 @@ a logseq page-ref e.g. [[page name]]"
   (str left-brackets right-brackets))
 
 ;; common regular expressions
-(def left-brackets-re #"\[\[")
-(def right-brackets-re #"\]\]")
 (def page-ref-re "Inner capture and doesn't match nested brackets" #"\[\[(.*?)\]\]")
-(def page-ref-outer-capture-re #"(\[\[.*?\]\])")
 (def page-ref-without-nested-re "Matches most inner nested brackets" #"\[\[([^\[\]]+)\]\]")
 (def page-ref-any-re "Inner capture that matches anything between brackets" #"\[\[(.*)\]\]")
 
@@ -27,11 +24,6 @@ a logseq page-ref e.g. [[page name]]"
   [page-name]
   (str left-brackets page-name right-brackets))
 
-(defn ->page-ref-re-str
-  "Create a page ref regex escaped string given a page name"
-  [page-name]
-  (string/replace (->page-ref page-name) #"([\[\]])" "\\$1"))
-
 (defn get-page-name
   "Extracts page-name from page-ref string"
   [s]

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

@@ -1,72 +1,82 @@
 (ns logseq.graph-parser.block-test
   (:require [logseq.graph-parser.block :as gp-block]
+            [logseq.graph-parser.mldoc :as gp-mldoc]
             [cljs.test :refer [deftest are testing is]]))
 
+(defn- extract-properties
+  [properties user-config]
+  (gp-block/extract-properties
+   (map
+    (fn [[k v]]
+      (let [mldoc-ast (gp-mldoc/get-references v (gp-mldoc/default-config :markdown))]
+        [k v mldoc-ast]))
+    properties)
+   user-config))
+
 (deftest test-extract-properties
-  (are [x y] (= (:properties (gp-block/extract-properties :markdown x {})) y)
+  (are [x y] (= (:properties (extract-properties x {})) y)
        ;; Built-in properties
        [["background-color" "#000000"]] {:background-color "#000000"}
-       [["alias" "name/with space"]] {:alias #{"name/with space"}}
-       [["tags" "foo, bar"]] {:tags #{"foo" "bar"}}
-       [["tags" "'bar'"]] {:tags #{"'bar'"}}
+       [["alias" "[[name/with space]]"]] {:alias #{"name/with space"}}
+       [["tags" "[[foo]], [[bar]]"]] {:tags #{"foo" "bar"}}
+       [["tags" "[[foo]] [[bar]]"]] {:tags #{"foo" "bar"}}
+       [["tags" "bar"]] {:tags #{"bar"}}
        [["file-path" "file:///home/x, y.pdf"]] {:file-path "file:///home/x, y.pdf"}
 
        ;; User properties
        [["year" "1000"]] {:year 1000}
        [["year" "\"1000\""]] {:year "\"1000\""}
-       [["year" "1000"] ["alias" "name/with space"]] {:year 1000, :alias #{"name/with space"}}
-       [["year" "1000"] ["tags" "name/with space"]] {:year 1000, :tags #{"name/with space"}}
-       [["year" "1000"] ["tags" "name/with space, another"]] {:year 1000, :tags #{"name/with space" "another"}}
-       [["year" "1000"] ["alias" "name/with space, another"]] {:year 1000, :alias #{"name/with space" "another"}}
-       [["year" "1000"] ["alias" "name/with space, [[another [[nested]]]]"]] {:year 1000, :alias #{"name/with space" "another [[nested]]"}}
-       [["year" "1000"] ["alias" "name/with space, [[[[nested]] another]]"]] {:year 1000, :alias #{"name/with space" "[[nested]] another"}}
+       [["year" "1000"] ["alias" "[[name/with space]]"]] {:year 1000, :alias #{"name/with space"}}
+       [["year" "1000"] ["tags" "[[name/with space]]"]] {:year 1000, :tags #{"name/with space"}}
+       [["year" "1000"] ["tags" "[[name/with space]], [[another]]"]] {:year 1000, :tags #{"name/with space" "another"}}
+       [["year" "1000"] ["alias" "[[name/with space]], [[another]]"]] {:year 1000, :alias #{"name/with space" "another"}}
+       [["year" "1000"] ["alias" "[[name/with space]], [[another [[nested]]]]"]] {:year 1000, :alias #{"name/with space" "another [[nested]]"}}
+       [["year" "1000"] ["alias" "[[name/with space]], [[[[nested]] another]]"]] {:year 1000, :alias #{"name/with space" "[[nested]] another"}}
        [["foo" "bar"]] {:foo "bar"}
-       [["foo" "bar, baz"]] {:foo #{"bar" "baz"}}
-       [["foo" "bar, [[baz]]"]] {:foo #{"bar" "baz"}}
+       [["foo" "[[bar]], [[baz]]"]] {:foo #{"bar" "baz"}}
+       [["foo" "[[bar]], [[baz]]"]] {:foo #{"bar" "baz"}}
        [["foo" "[[bar]], [[baz]]"]] {:foo #{"bar" "baz"}}
        [["foo" "[[bar]], [[nested [[baz]]]]"]] {:foo #{"bar" "nested [[baz]]"}}
        [["foo" "[[bar]], [[nested [[baz]]]]"]] {:foo #{"bar" "nested [[baz]]"}}
-       [["foo" "bar, [[baz, test]]"]] {:foo #{"bar" "baz, test"}}
-       [["foo" "bar, [[baz, test, [[nested]]]]"]] {:foo #{"bar" "baz, test, [[nested]]"}})
+       [["foo" "[[bar]], [[baz, test]]"]] {:foo #{"bar" "baz, test"}}
+       [["foo" "[[bar]], [[baz, test, [[nested]]]]"]] {:foo #{"bar" "baz, test, [[nested]]"}})
 
   (testing "page-refs"
     (are [x y] (= (vec (:page-refs
-                        (gp-block/extract-properties :markdown x {:property-pages/enabled? true}))) y)
+                        (extract-properties x {:property-pages/enabled? true}))) y)
          [["year" "1000"]] ["year"]
          [["year" "\"1000\""]] ["year"]
          [["year" "1000"] ["month" "12"]] ["year" "month"]
-         [["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]] test"]] ["bar" "foo"]
+         [["foo" "[[bar]] test [[baz]]"]] ["bar" "baz" "foo"]
+         [["foo" "[[bar]] test [[baz]] [[nested [[baz]]]]"]] ["bar" "baz" "nested [[baz]]" "foo"]
          [["foo" "#bar, #baz"]] ["bar" "baz" "foo"]
-         [["foo" "[[nested [[page]]]], test"]] ["nested [[page]]" "test" "foo"])
+         [["foo" "[[nested [[page]]]], test"]] ["nested [[page]]" "foo"])
 
 
     (are [x y] (= (vec (:page-refs
-                        (gp-block/extract-properties :markdown x {:property-pages/enabled? false}))) y)
+                        (extract-properties x {:property-pages/enabled? false}))) y)
          [["year" "1000"]] []
          [["year" "1000"] ["month" "12"]] []
-         [["foo" "[[bar]] test"]] ["bar" "test"])
+         [["foo" "[[bar]] test"]] ["bar"])
 
     (is (= ["year"]
            (:page-refs
-            (gp-block/extract-properties :markdown
-                                         [["year" "1000"] ["month" "12"]]
+            (extract-properties [["year" "1000"] ["month" "12"]]
                                          {:property-pages/enabled? true
                                           :property-pages/excludelist #{:month :day}})))
         ":property-pages/exclude-list excludes specified properties")
 
     (is (= ["year"]
            (:page-refs
-            (gp-block/extract-properties :markdown
-                                         [["year" "1000"]]
+            (extract-properties [["year" "1000"]]
                                          {})))
         "Default to enabled when :property-pages/enabled? is not in config")
 
     (is (= ["foo" "bar"]
            (:page-refs
-            (gp-block/extract-properties :markdown
-                                         ;; tags is linkable and background-color is not
-                                         [["tags" "foo, bar"] ["background-color" "#008000"]]
+            (extract-properties
+             ;; tags is linkable and background-color is not
+             [["tags" "[[foo]], [[bar]]"] ["background-color" "#008000"]]
                                          {:property-pages/enabled? true})))
         "Only editable linkable built-in properties have page-refs in property values")))

+ 28 - 29
deps/graph-parser/test/logseq/graph_parser/mldoc_test.cljs

@@ -1,5 +1,6 @@
 (ns logseq.graph-parser.mldoc-test
   (:require [logseq.graph-parser.mldoc :as gp-mldoc]
+            [logseq.graph-parser.text :as text]
             [clojure.string :as string]
             [logseq.graph-parser.test.docs-graph-helper :as docs-graph-helper]
             [logseq.graph-parser.cli :as gp-cli]
@@ -51,41 +52,40 @@
           {:start_pos 0, :end_pos 15}]
          (first (gp-mldoc/->edn "```
 : hello
-```" md-config {})))
+```" md-config)))
       "Basic src example")
 
   (is (= [["Src"
-            {:lines ["  hello" "\n" "  world" "\n"],
-             :pos_meta {:start_pos 7, :end_pos 25},
-             :full_content "```\nhello\nworld\n```"}]
-           {:start_pos 1, :end_pos 29}]
+           {:lines ["  hello" "\n" "  world" "\n"],
+            :pos_meta {:start_pos 7, :end_pos 25},
+            :full_content "```\nhello\nworld\n```"}]
+          {:start_pos 1, :end_pos 29}]
          (second (gp-mldoc/->edn "
   ```
   hello
   world
   ```
-" md-config {})))
+" md-config)))
       "Src example with leading whitespace"))
 
-(deftest md-properties-test
-  (are [x y] (= [["Properties" y] nil]
-                (first (gp-mldoc/->edn x md-config {})))
-
-       ;; comma separates values
-       "property:: foo, bar"
-       {:property #{"foo" "bar"}}
+(defn- get-properties
+  [x]
+  (->> (gp-mldoc/->edn x md-config)
+       ffirst second
+       (map (fn [[k v ast]]
+              [(keyword k) (text/parse-property k v ast {})]))
+       (into {})))
 
-       ;; alias property
-       "alias:: foo,, bar"
-       {:alias ["foo" "bar"]}
+(deftest md-properties-test
+  (are [x y] (= y (get-properties x))
 
-       ;; tags property
-       "tags:: foo,bar,foo"
-       {:tags ["foo" "bar"]}
+    ;; reference values
+    "property:: [[foo]], [[bar]]"
+    {:property #{"foo" "bar"}}
 
-       ;; title property
-       "title:: comma, is ok"
-       {:title "comma, is ok"}))
+    ;; comma separated
+    "tags:: foo, bar, foo"
+    {:tags #{"foo" "bar"}}))
 
 (deftest name-definition-test
   (is (= [["List"
@@ -96,11 +96,11 @@
              :ordered false}]]
           {:start_pos 0, :end_pos 17}]
          (first (gp-mldoc/->edn "term
-: definition" md-config {})))))
+: definition" md-config)))))
 
 (defn- parse-properties
   [text]
-  (->> (gp-mldoc/->edn text (gp-mldoc/default-config :org) {})
+  (->> (gp-mldoc/->edn text (gp-mldoc/default-config :org))
        (filter #(= "Properties" (ffirst %)))
        ffirst
        second))
@@ -110,7 +110,7 @@
   (testing "just title"
     (let [content "#+TITLE:   some title   "
           props (parse-properties content)]
-      (is (= "some title   " (:title props)))))
+      (is (= "some title   " (second (first props))))))
 
   (testing "filetags"
     (let [content "#+FILETAGS:   :tag1:tag2:@tag:
@@ -130,8 +130,7 @@ body"
                                                 :org :markdown)]
                                    [path
                                     (gp-mldoc/->edn content
-                                                    (gp-mldoc/default-config format)
-                                                    {})])))
+                                                    (gp-mldoc/default-config format))])))
                           (into {}))]
     (is (= {"CommentBlock" 1,
             "Custom" 41,
@@ -143,8 +142,8 @@ body"
             "Hiccup" 15,
             "List" 37,
             "Paragraph" 417,
-            "Properties" 104,
-            "Property_Drawer" 188,
+            "Properties" 91,
+            "Property_Drawer" 201,
             "Quote" 9,
             "Raw_Html" 12,
             "Src" 56,

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

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

+ 44 - 54
deps/graph-parser/test/logseq/graph_parser/text_test.cljs

@@ -1,6 +1,7 @@
 (ns logseq.graph-parser.text-test
-  (:require [cljs.test :refer [are deftest testing is]]
-            [logseq.graph-parser.text :as text]))
+  (:require [cljs.test :refer [are deftest testing]]
+            [logseq.graph-parser.text :as text]
+            [logseq.graph-parser.mldoc :as gp-mldoc]))
 
 (deftest test-get-page-name
   []
@@ -35,37 +36,6 @@
     "[single bracket]" "[single bracket]"
     "no brackets" "no brackets"))
 
-(deftest sep-by-comma
-  []
-  (are [x y] (= (text/sep-by-comma x) y)
-    "foo,bar" ["foo" "bar"]
-    "foo, bar" ["foo" "bar"]
-    "foo bar" ["foo bar"]
-    "[[foo]] [[bar]]" ["[[foo]] [[bar]]"]
-    "[[foo]],[[bar]]" ["[[foo]]", "[[bar]]"]
-    "[[foo]], [[bar]]" ["[[foo]]", "[[bar]]"]
-    "[[foo]]" ["[[foo]]"]
-    "[[nested [[foo]]]]" ["[[nested [[foo]]]]"]))
-
-(deftest split-page-refs-without-brackets
-  []
-  (are [x y] (= (text/split-page-refs-without-brackets x) y)
-    "foobar" "foobar"
-    "foo bar" "foo bar"
-    "foo, bar" #{"foo" "bar"}
-    "[[foo]] [[bar]]" #{"foo" "bar"}
-    "[[foo]],[[bar]]" #{"foo", "bar"}
-    "[[foo]], [[bar]]" #{"foo", "bar"}
-    "[[foo]]" #{"foo"}
-    "[[nested [[foo]]]]" #{"nested [[foo]]"}
-    "[[nested [[foo]]]], [[foo]]" #{"nested [[foo]]" "foo"}
-    "[[nested [[foo]] [[bar]]]], [[foo]]" #{"nested [[foo]] [[bar]]" "foo"}
-    "[[nested [[foo]], [[bar]]]], [[foo]]" #{"nested [[foo]], [[bar]]" "foo"}
-    "#tag," #{"tag"}
-    "#tag" #{"tag"}
-    "#tag1,#tag2" #{"tag1" "tag2"}
-    "[[Jan 26th, 2021]], hello" #{"hello" "Jan 26th, 2021"}))
-
 (def block-patterns
   {:markdown "-"
    :org "*"})
@@ -90,28 +60,48 @@
       "**foobar" "foobar"
       "*********************foobar" "foobar")))
 
+(defn- parse-property
+  [k v user-config]
+  (let [references (gp-mldoc/get-references v (gp-mldoc/default-config :markdown))]
+    (text/parse-property k v references user-config)))
+
 (deftest test-parse-property
-  (testing "parse-property"
-    (are [k v y] (= (text/parse-property k v {}) y)
-      :tags "foo" "foo"
-      :tags "foo, bar" #{"foo" "bar"}
-      :tags "foo,bar" #{"foo" "bar"}
-      :tags "[[foo]]" #{"foo"}
-      :tags "[[foo]] [[bar]]" #{"foo" "bar"}
-      :tags "[[foo]], [[bar]]" #{"foo" "bar"}
-      :tags "[[foo]], [[bar]], #baz" #{"foo" "bar" "baz"}
-      :tags "#baz, [[foo]], [[bar]]" #{"foo" "bar" "baz"}
-      :tags "[[foo [[bar]]]]" #{"foo [[bar]]"}
-      :tags "[[foo [[bar]]]], baz" #{"baz" "foo [[bar]]"}))
+  (testing "for default comma separated properties"
+    (are [k v y] (= (parse-property k v {}) y)
+         :tags "foo" #{"foo"}
+         :tags "comma, separated" #{"comma" "separated"}
+         :alias "one, two, one" #{"one" "two"}))
+
+  (testing "for user comma separated properties"
+    (are [k v y] (= (parse-property k v {:property/separated-by-commas #{:comma-prop}}) y)
+         :comma-prop "foo" #{"foo"}
+         :comma-prop "comma, separated" #{"comma" "separated"}
+         :comma-prop "one, two, one" #{"one" "two"}))
+
+  (testing "for user comma separated properties with mixed values"
+    (are [k v y] (= (parse-property k v {:property/separated-by-commas #{:comma-prop}}) y)
+      :comma-prop "foo, #bar" #{"foo", "bar"}
+      :comma-prop "comma, separated, [[page ref]], [[nested [[page]]]], #[[nested [[tag]]]], end" #{"page ref" "nested [[page]]" "nested [[tag]]" "comma" "separated" "end"}))
+
+  (testing "for normal properties"
+    (are [k v y] (= (parse-property k v {}) y)
+         :normal "[[foo]] [[bar]]" #{"foo" "bar"}
+         :normal "[[foo]], [[bar]]" #{"foo" "bar"}
+         :normal "[[foo]]" #{"foo"}
+         :normal "[[foo]], [[bar]], #baz" #{"foo" "bar" "baz"}
+         :normal "[[foo [[bar]]]]" #{"foo [[bar]]"}
+         :normal "[[foo [[bar]]]], [[baz]]" #{"baz" "foo [[bar]]"}
+         :title "comma, is ok" "comma, is ok"))
+
+  (testing "for tags in properties with punctuation"
+    (are [k v y] (= (parse-property k v {}) y)
+         :prop "#foo, #bar. #baz!" #{"foo" "bar" "baz"}
+         :prop "#foo: '#bar'" #{"foo" "bar"}))
+
   (testing "parse-property with quoted strings"
-    (are [k v y] (= (text/parse-property k v {}) y)
-      :tags "\"foo, bar\"" "\"foo, bar\""
-      :tags "\"[[foo]], [[bar]]\"" "\"[[foo]], [[bar]]\""
-      :tags "baz, \"[[foo]], [[bar]]\"" #{"baz"})))
-
-(deftest extract-page-refs-and-tags
-  (is (= #{"cljs" "nbb" "js" "amazing"}
-       (text/extract-page-refs-and-tags "This project is written with #cljs, #nbb and #js. #amazing!"))
-      "Don't extract punctation at end of a tag"))
+    (are [k v y] (= (parse-property k v {}) y)
+         :tags "\"foo, bar\"" "\"foo, bar\""
+         :tags "\"[[foo]], [[bar]]\"" "\"[[foo]], [[bar]]\"")))
+
 
 #_(cljs.test/test-ns 'logseq.graph-parser.text-test)

+ 67 - 51
deps/graph-parser/test/logseq/graph_parser_test.cljs

@@ -3,6 +3,7 @@
             [clojure.string :as string]
             [logseq.graph-parser :as graph-parser]
             [logseq.db :as ldb]
+            [logseq.db.default :as default-db]
             [logseq.graph-parser.block :as gp-block]
             [logseq.graph-parser.property :as gp-property]
             [datascript.core :as d]))
@@ -99,13 +100,12 @@
   (testing "Sort order and persistence of 10 properties"
     (test-property-order 10)))
 
-(defn- quoted-property-values-test
-  [user-config]
+(deftest quoted-property-values
   (let [conn (ldb/start-conn)
         _ (graph-parser/parse-file conn
                                    "foo.md"
                                    "- desc:: \"#foo is not a ref\""
-                                   {:extract-options {:user-config user-config}})
+                                   {:extract-options {:user-config {}}})
         block (->> (d/q '[:find (pull ?b [* {:block/refs [*]}])
                        :in $
                        :where [?b :block/properties]]
@@ -119,47 +119,55 @@
            (map :block/original-name (:block/refs block)))
         "No refs from property value")))
 
-(deftest quoted-property-values
-  (testing "With default config"
-    (quoted-property-values-test {}))
-  (testing "With :rich-property-values config"
-    (quoted-property-values-test {:rich-property-values? true})))
-
-(deftest page-properties-persistence
-  (testing "Non-string property values"
-    (let [conn (ldb/start-conn)]
-      (graph-parser/parse-file conn
-                               "lythe-of-heaven.md"
-                               "rating:: 8\nrecommend:: true\narchive:: false"
-                               {})
-      (is (= {:rating 8 :recommend true :archive false}
-             (->> (d/q '[:find (pull ?b [*])
-                         :in $
-                         :where [?b :block/properties]]
-                       @conn)
-                  (map (comp :block/properties first))
-                  first)))))
+(deftest non-string-property-values
+  (let [conn (ldb/start-conn)]
+    (graph-parser/parse-file conn
+                             "lythe-of-heaven.md"
+                             "rating:: 8\nrecommend:: true\narchive:: false"
+                             {})
+    (is (= {:rating 8 :recommend true :archive false}
+           (->> (d/q '[:find (pull ?b [*])
+                       :in $
+                       :where [?b :block/properties]]
+                     @conn)
+                (map (comp :block/properties first))
+                first)))))
 
-  (testing "Linkable built-in properties"
-    (let [conn (ldb/start-conn)
-          _ (graph-parser/parse-file conn
-                                     "lol.md"
-                                     "alias:: 233\ntags:: fun, facts"
-                                     {})
-          block (->> (d/q '[:find (pull ?b [:block/properties {:block/alias [:block/name]} {:block/tags [:block/name]}])
-                            :in $
-                            :where [?b :block/name "lol"]]
-                          @conn)
-                     (map first)
-                     first)]
+(deftest linkable-built-in-properties
+  (let [conn (ldb/start-conn)
+        _ (graph-parser/parse-file conn
+                                   "lol.md"
+                                   (str "alias:: 233\ntags:: fun, facts"
+                                        "\n- "
+                                        "alias:: 666\ntags:: block, facts")
+                                   {})
+        page-block (->> (d/q '[:find (pull ?b [:block/properties {:block/alias [:block/name]} {:block/tags [:block/name]}])
+                               :in $
+                               :where [?b :block/name "lol"]]
+                             @conn)
+                        (map first)
+                        first)
+        block (->> (d/q '[:find (pull ?b [:block/properties])
+                          :in $
+                          :where
+                          [?b :block/properties]
+                          [(missing? $ ?b :block/pre-block?)]
+                          [(missing? $ ?b :block/name)]]
+                        @conn)
+                   (map first)
+                   first)]
 
-      (is (= {:block/alias [{:block/name "233"}]
-              :block/tags [{:block/name "fun"} {:block/name "facts"}]
-              :block/properties {:alias ["233"] :tags ["fun" "facts"]}}
-             block))
+    (is (= {:block/alias [{:block/name "233"}]
+            :block/tags [{:block/name "fun"} {:block/name "facts"}]
+            :block/properties {:alias #{"233"} :tags #{"fun" "facts"}}}
+           page-block)
+        "page properties, alias and tags are correct")
+    (is (every? set? (vals (:block/properties page-block)))
+        "Linked built-in property values as sets provides for easier transforms")
 
-      (is (every? vector? (vals (:block/properties block)))
-          "Linked built-in property values as vectors provides for easier transforms"))))
+    (is (= {:block/properties {:alias #{"666"} :tags #{"block" "facts"}}}
+           block)
+        "block properties are correct")))
 
 (defn- property-relationships-test
   "Runs tests on page properties and block properties. file-properties is what is
@@ -218,22 +226,13 @@
                     :desc "This is a multiple sentence description. It has one [[link]]"
                     :comma-prop "one, two,three"}]
     (testing "With default config"
-      (property-relationships-test
-       properties
-       {:single-link #{"bar"}
-        :multi-link #{"Logseq" "is the fastest" "triples" "text editor"}
-        :desc #{"This is a multiple sentence description. It has one" "link"}
-        :comma-prop #{"one" "two" "three"}}
-       {}))
-
-    (testing "With :rich-property-values config"
       (property-relationships-test
        properties
        {:single-link #{"bar"}
         :multi-link #{"Logseq" "triples" "text editor"}
         :desc #{"link"}
         :comma-prop "one, two,three"}
-       {:rich-property-values? true}))))
+       {}))))
 
 (deftest invalid-properties
   (let [conn (ldb/start-conn)
@@ -266,3 +265,20 @@
                 (map first)
                 (map #(select-keys % [:block/properties :block/invalid-properties]))))
         "Has correct (in)valid page properties")))
+
+(deftest correct-page-names-created
+  (testing "from title"
+    (let [conn (ldb/start-conn)
+          built-in-pages (set (map string/lower-case default-db/built-in-pages-names))]
+      (graph-parser/parse-file conn
+                               "foo.md"
+                               "title:: core.async"
+                               {})
+      (is (= #{"core.async"}
+             (->> (d/q '[:find (pull ?b [*])
+                         :in $
+                         :where [?b :block/name]]
+                       @conn)
+                  (map (comp :block/name first))
+                  (remove built-in-pages)
+                  set))))))

+ 4 - 4
deps/graph-parser/yarn.lock

@@ -177,10 +177,10 @@ mimic-fn@^2.0.0:
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
   integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
 
-mldoc@^1.3.9:
-  version "1.3.9"
-  resolved "https://registry.yarnpkg.com/mldoc/-/mldoc-1.3.9.tgz#9e45a25ba79596f3b0b0eace65f651a4c5a0c30a"
-  integrity sha512-UfqNuBphOj7paSBvozTdin5BLB5+W2tr2SGKTfp5eae6VQPx23oICx6RPQprft7KGFtn8T3rpM1YMUN8FaJLhg==
+mldoc@^1.4.9:
+  version "1.4.9"
+  resolved "https://registry.yarnpkg.com/mldoc/-/mldoc-1.4.9.tgz#a61d93e98f19bf4ec6303810cc3573c5e14eab44"
+  integrity sha512-rroc1tWJnNm2n33MYk+sWgLjGWmP93Am+6zvpBe0BGEyccHQT3AxHHv8fNpF7HVvT7YPtU/dtHWTh6nE2n7vPA==
   dependencies:
     yargs "^12.0.2"
 

+ 7 - 0
docs/dev-practices.md

@@ -52,6 +52,13 @@ bb lint:large-vars
 
 To configure the linter, see the `[:tasks/config :large-vars]` path of bb.edn.
 
+### Document namespaces
+
+Documentation helps teams share their knowledge and enables more individuals to contribute to the codebase. Documenting our namespaces is a good first step to improving our documentation. Currently this linter is only run on our deps/. To run this linter:
+```
+bb lint:ns-docstrings
+```
+
 ### Datalog linting
 
 We use [datascript](https://github.com/tonsky/datascript)'s datalog to power our

+ 5 - 0
gulpfile.js

@@ -111,6 +111,11 @@ const common = {
       })
     })
 
+    cp.execSync(`rm -rf ios/App/App/public/static/out`, {
+      stdio: 'inherit'
+    })
+
+
     cp.execSync(`npx cap run ${mode} --external`, {
       stdio: 'inherit',
       env: Object.assign(process.env, {

+ 3 - 4
package.json

@@ -85,11 +85,11 @@
         "@capacitor/status-bar": "^4.0.0",
         "@excalidraw/excalidraw": "0.10.0",
         "@kanru/rage-wasm": "0.2.1",
-        "@logseq/capacitor-file-sync": "0.0.6",
+        "@logseq/capacitor-file-sync": "0.0.10",
         "@logseq/react-tweet-embed": "1.3.1-1",
         "@sentry/react": "^6.18.2",
         "@sentry/tracing": "^6.18.2",
-        "@tabler/icons": "1.78.0",
+        "@tabler/icons": "^1.96.0",
         "@tippyjs/react": "4.2.5",
         "aes-js": "3.1.2",
         "bignumber.js": "^9.0.2",
@@ -112,8 +112,7 @@
         "ignore": "5.1.8",
         "is-svg": "4.3.0",
         "jszip": "3.7.0",
-        "mldoc": "1.4.0",
-        "mobx": "^6.5.0",
+        "mldoc": "^1.4.9",
         "path": "0.12.7",
         "pixi-graph-fork": "0.2.0",
         "pixi.js": "6.2.0",

+ 9 - 6
resources/css/common.css

@@ -14,6 +14,9 @@
   --ls-left-sidebar-width: 246px;
   --ls-left-sidebar-sm-width: 70%;
   --ls-left-sidebar-nav-btn-size: 38px;
+  --ls-color-file-sync-error: #ff0000;
+  --ls-color-file-sync-pending: #ffbb4d;
+  --ls-color-file-sync-idle: #04b404;
 }
 
 @media (prefers-color-scheme: dark) {
@@ -86,6 +89,8 @@ html[data-theme='dark'] {
   --ls-search-icon-color: var(--ls-link-text-color);
   --ls-a-chosen-bg: var(--ls-secondary-background-color);
   --ls-right-sidebar-code-bg-color: #04303c;
+  --ls-pie-bg-color: #01303b;
+  --ls-pie-fg-color: #0b5869;
   --ls-focus-ring-color: rgba(18, 98, 119, 0.5);
   --color-level-1: var(--ls-secondary-background-color);
   --color-level-2: var(--ls-tertiary-background-color);
@@ -143,10 +148,12 @@ html[data-theme='light'] {
   --ls-scrollbar-thumb-hover-color: rgba(0, 0, 0, 0.2);
   --ls-head-text-color: var(--ls-link-text-color);
   --ls-cloze-text-color: #0000cd;
-  --ls-icon-color: #908e8b;
+  --ls-icon-color: #646464;
   --ls-search-icon-color: var(--ls-icon-color);
   --ls-a-chosen-bg: #f7f7f7;
   --ls-right-sidebar-code-bg-color: var(--ls-secondary-background-color);
+  --ls-pie-bg-color: #e1e1e1;
+  --ls-pie-fg-color: #0a4a5d;
   --ls-focus-ring-color: rgba(66, 133, 244, 0.5);
   --color-level-1: var(--ls-secondary-background-color);
   --color-level-2: var(--ls-tertiary-background-color);
@@ -176,10 +183,6 @@ body {
   word-break: break-word; /* compatible for overflow-wrap: anywhere */
 }
 
-a svg {
-  color: var(--ls-icon-color);
-}
-
 svg {
   pointer-events: none;
 }
@@ -908,7 +911,7 @@ button.menu:focus {
   background-color: var(--ls-menu-hover-color, #f4f5f7);
 }
 
-.menu-links-wrapper {
+.menu-links-wrapper, .menu-links-outer {
   @apply py-2 rounded-md shadow-lg overflow-y-auto;
 
   max-height: calc(100vh - 100px) !important;

+ 1 - 0
resources/electron.html

@@ -52,6 +52,7 @@ const portal = new MagicPortal(worker);
 <script defer src="./js/interact.min.js"></script>
 <script defer src="./js/lsplugin.core.js"></script>
 <script defer src="./js/main.js"></script>
+<script defer src="./js/tabler.min.js"></script>
 <script defer src="./js/code-editor.js"></script>
 <script defer src="./js/age-encryption.js"></script>
 <script defer src="./js/excalidraw.js"></script>

+ 1 - 0
resources/index.html

@@ -51,6 +51,7 @@ const portal = new MagicPortal(worker);
 <script defer src="./js/interact.min.js"></script>
 <script defer src="./js/lsplugin.core.js"></script>
 <script defer src="./js/main.js"></script>
+<script defer src="./js/tabler.min.js"></script>
 <script defer src="./js/code-editor.js"></script>
 <script defer src="./js/age-encryption.js"></script>
 <script defer src="./js/excalidraw.js"></script>

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


+ 1 - 1
resources/package.json

@@ -37,7 +37,7 @@
     "https-proxy-agent": "5.0.0",
     "@sentry/electron": "2.5.1",
     "posthog-js": "1.10.2",
-    "@logseq/rsapi": "0.0.44",
+    "@logseq/rsapi": "0.0.46",
     "electron-deeplink": "1.0.10",
     "abort-controller": "3.0.0"
   },

+ 1 - 1
shadow-cljs.edn

@@ -38,7 +38,7 @@
                                                 :redef false}}
         :closure-defines  {goog.debug.LOGGING_ENABLED       true
                            frontend.config/ENABLE-PLUGINS   #shadow/env ["ENABLE_PLUGINS"   :as :bool :default true]
-                           frontend.config/ENABLE-FILE-SYNC-PRODUCTION #shadow/env ["ENABLE_FILE_SYNC_PRODUCTION" :as :bool :default false]}
+                           frontend.config/ENABLE-FILE-SYNC-PRODUCTION #shadow/env ["ENABLE_FILE_SYNC_PRODUCTION" :as :bool :default true]}
 
         ;; NOTE: electron, browser/mobile-app use different asset-paths.
         ;;   For browser/mobile-app devs, assets are located in /static/js(via HTTP root).

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

@@ -15,7 +15,7 @@
     (.ensureFileSync fs cfg-path)
     (let [body (.toString (.readFileSync fs cfg-path))]
       (if (seq body) (reader/read-string body) {}))
-    (catch js/Error e
+    (catch :default e
       (js/console.error :cfg-error e)
       {})))
 
@@ -23,7 +23,7 @@
   [cfg]
   (try
     (.writeFileSync fs cfg-path (pr-str cfg)) cfg
-    (catch js/Error e
+    (catch :default e
       (js/console.error :cfg-error e))))
 
 (defn set-item!

+ 4 - 3
src/electron/electron/core.cljs

@@ -166,7 +166,7 @@
                (fn [_ type & args]
                  (try
                    (js-invoke app type args)
-                   (catch js/Error e
+                   (catch :default e
                      (logger/error (str call-app-channel " " e))))))
 
       (.handle call-win-channel
@@ -174,7 +174,7 @@
                  (let [win (get-win-from-sender e)]
                    (try
                      (js-invoke win type args)
-                     (catch js/Error e
+                     (catch :default e
                        (logger/error (str call-win-channel " " e))))))))
 
     #(do (clear-win-effects!)
@@ -276,7 +276,7 @@
                                      (try
                                        (fs-watcher/close-watcher!)
                                        (search/close!)
-                                       (catch js/Error e
+                                       (catch :default e
                                          (logger/error "window-all-closed" e)))
                                      (.quit app)))
       (.on app "ready"
@@ -317,6 +317,7 @@
                                   (when @*quit-dirty? ;; when not updating
                                     (.preventDefault e)
                                     (let [web-contents (. win -webContents)]
+                                      (.send web-contents "persist-zoom-level" (.getZoomLevel web-contents))
                                       (.send web-contents "persistent-dbs"))
                                     (async/go
                                       (let [_ (async/<! state/persistent-dbs-chan)]

+ 15 - 1
src/electron/electron/file_sync_rsapi.cljs

@@ -1,11 +1,16 @@
 (ns electron.file-sync-rsapi
-  (:require ["@logseq/rsapi" :as rsapi]))
+  (:require ["@logseq/rsapi" :as rsapi]
+            [electron.window :as window]
+            [cljs-bean.core :as bean]))
 
 (defn key-gen [] (rsapi/keygen))
 
 (defn set-env [graph-uuid env private-key public-key]
   (rsapi/setEnv graph-uuid env private-key public-key))
 
+(defn set-progress-callback [callback]
+  (rsapi/setProgressCallback callback))
+
 (defn get-local-files-meta [graph-uuid base-path file-paths]
   (rsapi/getLocalFilesMeta graph-uuid base-path (clj->js file-paths)))
 
@@ -41,3 +46,12 @@
 
 (defn decrypt-with-passphrase [passphrase data]
   (rsapi/ageDecryptWithPassphrase passphrase data))
+
+(defonce progress-notify-chan "file-sync-progress")
+(set-progress-callback (fn [error progress-info]
+                         (when-not error
+                           (doseq [^js win (window/get-all-windows)]
+                             (when-not (.isDestroyed win)
+                               (.. win -webContents
+                                   (send progress-notify-chan (bean/->js progress-info))))))))
+                                   

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

@@ -46,7 +46,7 @@
   (try
     (let [p (.join path (state/get-graph-path) ".git")]
       (.isDirectory (fs/statSync p)))
-    (catch js/Error _e
+    (catch :default _e
       nil)))
 
 (defn remove-dot-git-file!
@@ -66,7 +66,7 @@
                      (string/includes? content ".logseq/")
                      (not (fs/existsSync dir-path)))
             (fs/unlinkSync p)))))
-    (catch js/Error e
+    (catch :default e
       (log-error e))))
 
 (defn init!

+ 5 - 5
src/electron/electron/handler.cljs

@@ -67,7 +67,7 @@
                             (string/replace "\\" "_"))
             recycle-dir (str repo-dir "/logseq/.recycle")
             _           (fs-extra/ensureDirSync recycle-dir)
-            new-path    (str recycle-dir "/" file-name)] 
+            new-path    (str recycle-dir "/" file-name)]
         (fs/renameSync path new-path)
         (logger/debug ::unlink "recycle to" new-path))
       (catch :default e
@@ -233,7 +233,7 @@
         (when-let [sync-meta (and (not (string/blank? root))
                                   (.toString (.readFileSync fs txid-path)))]
           (reader/read-string sync-meta))))
-    (catch js/Error e
+    (catch :default e
       (js/console.debug "[read txid meta] #" root (.-message e)))))
 
 (defmethod handle :inflateGraphsInfo [_win [_ graphs]]
@@ -316,7 +316,7 @@
       (let [path (path/join path dir)]
         (try
           (fs-extra/removeSync path)
-          (catch js/Error e
+          (catch :default e
             (logger/error "Clear cache:" e)))))
     (utils/send-to-renderer window "redirect" {:payload {:to :home}})))
 
@@ -352,7 +352,7 @@
                  (try
                    (and (fs-extra/pathExistsSync url)
                         (fs-extra/pathExistsSync (path/join url "package.json")))
-                   (catch js/Error _e false)))))
+                   (catch :default _e false)))))
 
 (defmethod handle :relaunchApp []
   (.relaunch app) (.quit app))
@@ -623,7 +623,7 @@
                    ;; exception -
                    ;; https://www.electronjs.org/docs/latest/breaking-changes#behavior-changed-sending-non-js-objects-over-ipc-now-throws-an-exception
                    (bean/->js (handle (or (utils/get-win-from-sender event) window) message)))
-                 (catch js/Error e
+                 (catch :default e
                    (when-not (contains? #{"mkdir" "stat"} (nth args-js 0))
                      (logger/error "IPC error: " {:event event
                                                   :args args-js}

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

@@ -98,7 +98,7 @@
   (try
     (when (fs/existsSync path)
       (.toString (fs/readFileSync path)))
-    (catch js/Error e
+    (catch :default e
       (logger/error "Read file:" e))))
 
 (defn get-focused-window
@@ -109,7 +109,7 @@
   [^js evt]
   (try
     (.fromWebContents BrowserWindow (.-sender evt))
-    (catch js/Error _
+    (catch :default _
       nil)))
 
 (defn send-to-renderer

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

@@ -111,7 +111,7 @@
 (defn- open-default-app!
   [url default-open]
   (let [URL (.-URL URL)
-        parsed-url (try (URL. url) (catch js/Error _ nil))]
+        parsed-url (try (URL. url) (catch :default _ nil))]
     (if (and parsed-url (contains? #{"https:" "http:" "mailto:"} (.-protocol parsed-url)))
       (.openExternal shell url)
       (when default-open (default-open url)))))
@@ -146,7 +146,7 @@
         (.on "new-window" new-win-handler)
         (.on "will-navigate" will-navigate-handler)
         (.on "did-start-navigation" #(.send web-contents "persist-zoom-level" (.getZoomLevel web-contents)))
-        (.on "did-navigate-in-page" #(.send web-contents "restore-zoom-level")))
+        (.on "page-title-updated" #(.send web-contents "restore-zoom-level")))
 
       (doto win
         (.on "enter-full-screen" #(.send web-contents "full-screen" "enter"))

+ 6 - 0
src/main/electron/listener.cljs

@@ -49,6 +49,12 @@
                          (when (file-sync-handler/enable-sync?)
                            (sync/file-watch-handler type payload)))))
 
+  (js/window.apis.on "file-sync-progress"
+                     (fn [data]
+                       (let [payload (bean/->clj data)]
+                         (state/set-state! [:file-sync/progress (:graphUUID payload) (:file payload)] payload)
+                         nil)))
+
   (js/window.apis.on "notification"
                      (fn [data]
                        (let [{:keys [type payload]} (bean/->clj data)

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

@@ -1226,7 +1226,7 @@
              f (sci/eval-string fn-string)]
          (when (fn? f)
            (try (f query-result)
-                (catch js/Error e
+                (catch :default e
                   (js/console.error e)))))))
    [:span.warning
     (util/format "{{function %s}}" (first arguments))]))
@@ -1930,13 +1930,14 @@
   [config block k value]
   (let [date (and (= k :date) (date/get-locale-string (str value)))
         user-config (state/get-config)
-        ;; In this mode and when value is a set of refs, display full property text
+        ;; When value is a set of refs, display full property text
         ;; because :block/properties value only contains refs but user wants to see text
-        v (if (and (:rich-property-values? user-config)
-                   (coll? value)
-                   (not (contains? gp-property/editable-linkable-built-in-properties k)))
-            (gp-property/property-value-from-content (name k) (:block/content block))
-            value)
+        property-separated-by-commas? (text/separated-by-commas? (state/get-config) k)
+        v (or
+           (when (and (coll? value) (seq value)
+                      (not property-separated-by-commas?))
+             (get (:block/properties-text-values block) k))
+           value)
         property-pages-enabled? (contains? #{true nil} (:property-pages/enabled? user-config))]
     [:div
      (if property-pages-enabled?
@@ -1953,7 +1954,10 @@
        date
        date
 
-       (coll? v)
+       (and (string? v) (gp-util/wrapped-by-quotes? v))
+       (gp-util/unquote-string v)
+
+       (and property-separated-by-commas? (coll? v))
        (let [v (->> (remove string/blank? v)
                     (filter string?))
              vals (for [v-item v]
@@ -1962,9 +1966,6 @@
          (for [elem elems]
            (rum/with-key elem (str (random-uuid)))))
 
-       (and (string? v) (gp-util/wrapped-by-quotes? v))
-       (gp-util/unquote-string v)
-
        :else
        (inline-text config (:block/format block) (str v)))]))
 
@@ -2868,7 +2869,7 @@
                                    :colgroup
                                    (repeat number col-elem))))
                               col_groups)
-                        (catch js/Error _e
+                        (catch :default _e
                           []))
         head (when header
                [:thead (tr :th header)])
@@ -3096,7 +3097,7 @@
                (and (seq result) view-f)
                (let [result (try
                               (sci/call-fn view-f result)
-                              (catch js/Error error
+                              (catch :default error
                                 (log/error :custom-view-failed {:error error
                                                                 :result result})
                                 [:div "Custom view failed: "
@@ -3372,7 +3373,7 @@
 
       :else
       "")
-    (catch js/Error e
+    (catch :default e
       (println "Convert to html failed, error: " e)
       "")))
 

+ 54 - 54
src/main/frontend/components/block.css

@@ -177,8 +177,9 @@
   min-width: 22px;
   min-height: 22px;
   padding: 2px;
-  color: initial;
+  color: var(--ls-secondary-text-color);
   user-select: none;
+  opacity: .4;
 
   .control-hide {
     display: none;
@@ -186,40 +187,40 @@
 }
 
 .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;
-        text-align: center;
-        margin: auto 0;
-
-        .indent {
-            opacity: 30%;
-        }
+  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;
+    text-align: center;
+    margin: auto 0;
+
+    .indent {
+      opacity: 30%;
     }
+  }
 }
 
 .block-right-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;
-        text-align: center;
-        margin: auto 0;
-
-        .outdent {
-            margin: 0 12px;
-            opacity: 30%;
-        }
-
-        .more {
-            margin: 0 12px;
-            opacity: 30%;
-        }
+  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;
+    text-align: center;
+    margin: auto 0;
+
+    .outdent {
+      margin: 0 12px;
+      opacity: 30%;
+    }
+
+    .more {
+      margin: 0 12px;
+      opacity: 30%;
     }
+  }
 }
 
 .block-ref {
@@ -279,16 +280,16 @@
   }
 
   &.is-doc {
-      &:before {
-          content: "[[📜";
-          opacity: .7;
-          margin-right: 4px;
-      }
+    &:before {
+      content: "[[📜";
+      opacity: .7;
+      margin-right: 4px;
+    }
 
-      &:after {
-          content: "]]";
-          opacity: .7;
-      }
+    &:after {
+      content: "]]";
+      opacity: .7;
+    }
   }
 }
 
@@ -309,8 +310,7 @@
 
   color: #fff;
 
-  a,
-  .page-reference:not(:hover), {
+  a, .page-reference:not(:hover) {
     color: #aacece;
 
     .bracket {
@@ -633,7 +633,7 @@ a.cloze-revealed {
 }
 
 .page-property-key:hover {
-    background-color: var(--ls-selection-background-color);
+  background-color: var(--ls-selection-background-color);
 }
 
 .block-parents a {
@@ -651,24 +651,24 @@ a.cloze-revealed {
 }
 
 html.is-native-ios {
-    audio {
-        width: 100%;
-        max-width: 350px;
-    }
+  audio {
+    width: 100%;
+    max-width: 350px;
+  }
 }
 
 html:not(.is-mac) {
-    body[data-active-keystroke="Control"] {
-        .block-content {
-            cursor: pointer;
-        }
+  body[data-active-keystroke="Control"] {
+    .block-content {
+      cursor: pointer;
     }
+  }
 }
 
 html.is-mac {
-    body[data-active-keystroke="Meta"] {
-        .block-content {
-            cursor: pointer;
-        }
+  body[data-active-keystroke="Meta"] {
+    .block-content {
+      cursor: pointer;
     }
+  }
 }

+ 24 - 15
src/main/frontend/components/command_palette.css

@@ -37,6 +37,15 @@
       height: var(--palettle-input-height);
     }
 
+    .recent-search {
+      > .wrap {
+        > div:last-child {
+          display: flex !important;
+          justify-items: center;
+        }
+      }
+    }
+
     .cp__palette-input {
       color: var(--ls-secondary-text-color);
     }
@@ -60,25 +69,25 @@
 }
 
 html.is-ios {
-    .cp__palette-main {
-        margin-bottom: 0px;
-    }
+  .cp__palette-main {
+    margin-bottom: 0;
+  }
 }
 
 .cards-review .cp__select {
-    &-main {
-        margin: 0;
-        @screen lg {
-            width: 240px;
-        }
+  &-main {
+    margin: 0;
+    @screen lg {
+      width: 240px;
     }
+  }
 
-    .input-wrap {
-        height: initial;
-    }
+  .input-wrap {
+    height: initial;
+  }
 
-    &-input {
-        padding: 16px;
-        font-size: 16px;
-    }
+  &-input {
+    padding: 16px;
+    font-size: 16px;
+  }
 }

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

@@ -487,7 +487,7 @@
    (fn [state]
      (when-not (:editor/on-paste? @state/state)
        (try (editor-handler/handle-last-input)
-            (catch js/Error _e
+            (catch :default _e
               nil)))
      (state/set-state! :editor/on-paste? false)
      state)}

+ 14 - 13
src/main/frontend/components/encryption.cljs

@@ -54,11 +54,12 @@
 (rum/defc show-password-cp
   [*show-password?]
   [:div.flex.flex-row.items-center
-   [:label {:for "show-password"}
-    (ui/checkbox {:checked? @*show-password?
+   [:label.px-1 {:for "show-password"}
+    (ui/checkbox {:checked?  @*show-password?
                   :on-change (fn [e]
-                               (reset! *show-password? (util/echecked? e)))})]
-   [:span.text-sm.ml-1.opacity-80 "Show password"]])
+                               (reset! *show-password? (util/echecked? e)))
+                  :id        "show-password"})
+    [:span.text-sm.ml-1.opacity-80.select-none.px-1 "Show password"]]])
 
 (rum/defcs ^:large-vars/cleanup-todo input-password-inner < rum/reactive
   (rum/local "" ::password)
@@ -141,7 +142,7 @@
 
     [:div.encryption-password.max-w-2xl.-mb-2
      [:div.cp__file-sync-related-normal-modal
-      [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "lock-access")]]
+      [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "lock-access" {:size 28})]]
 
       [:div.mt-3.text-center.sm:mt-0.sm:text-left
        [:h1#modal-headline.text-2xl.font-bold.text-center
@@ -160,7 +161,7 @@
          [:div.folder-tip.flex.flex-col.items-center
           [:h3
            [:span.flex.space-x-2.leading-none.pb-1
-            (ui/icon "cloud-lock")
+            (ui/icon "cloud-lock" {:size 20})
             [:span GraphName]
             [:span.scale-75 (ui/icon "arrow-right")]
             [:span (ui/icon "folder")]]]
@@ -169,10 +170,10 @@
          [:div.input-hints.text-sm.py-2.px-3.rounded.mb-2.mt-2.flex.items-center
           (if-let [display-str (:fail set-remote-graph-pwd-result)]
             [:<>
-             [:span.scale-125.pr-1.text-red-600 (ui/icon "alert-circle" {:class "text-md mr-1"})]
+             [:span.flex.pr-1.text-red-600 (ui/icon "alert-circle" {:class "text-md mr-1"})]
              [:span.text-red-600 display-str]]
             [:<>
-             [:span.scale-125.pr-1 (ui/icon "bulb" {:class "text-md mr-1"})]
+             [:span.flex.pr-1 (ui/icon "bulb" {:class "text-md mr-1"})]
              [:span "Please enter the password for this graph to continue syncing."]])]])
 
       ;; secure this remote graph
@@ -186,9 +187,9 @@
                     (not (string/blank? @*pw-confirm)))
               (if (or (not (pattern-ok?))
                       (not= @*password @*pw-confirm))
-                [:span.scale-125.pr-1.text-red-600 (ui/icon "alert-circle" {:class "text-md mr-1"})]
-                [:span.scale-125.pr-1.text-green-600 (ui/icon "circle-check" {:class "text-md mr-1"})])
-              [:span.scale-125.pr-1 (ui/icon "bulb" {:class "text-md mr-1"})])
+                [:span.flex.pr-1.text-red-600 (ui/icon "alert-circle" {:class "text-md mr-1"})]
+                [:span.flex.pr-1.text-green-600 (ui/icon "circle-check" {:class "text-md mr-1"})])
+              [:span.flex.pr-1 (ui/icon "bulb" {:class "text-md mr-1"})])
 
             (if (not (string/blank? @*password))
               (if-not (pattern-ok?)
@@ -252,7 +253,7 @@
       (when init-graph-keys
         [:div.init-remote-pw-tips.space-x-4.pt-2.hidden.sm:flex
          [:div.flex-1.flex.items-center
-          [:span.px-3.scale-125 (ui/icon "key")]
+          [:span.px-3.flex (ui/icon "key")]
           [:p.dark:text-gray-100
            [:span "Please make sure you "]
            "remember the password you have set, "
@@ -261,7 +262,7 @@
            [:span "of the password."]]]
 
          [:div.flex-1.flex.items-center
-          [:span.px-3.scale-125 (ui/icon "lock")]
+          [:span.px-3.flex (ui/icon "lock")]
           [:p.dark:text-gray-100
            "If you lose your password, all of your data in the cloud can’t be decrypted. "
            [:span "You will still be able to access the local version of your graph."]]]])]

+ 311 - 114
src/main/frontend/components/file_sync.cljs

@@ -23,10 +23,14 @@
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util.fs :as fs-util]
+            [frontend.storage :as storage]
             [logseq.graph-parser.config :as gp-config]
             [promesa.core :as p]
             [reitit.frontend.easy :as rfe]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [cljs-time.core :as t]
+            [cljs-time.coerce :as tc]
+            [goog.functions :refer [debounce]]))
 
 (declare maybe-onboarding-show)
 (declare open-icloud-graph-clone-picker)
@@ -131,18 +135,18 @@
                 (when-let [GraphUUID (get (async/<! (file-sync-handler/create-graph graph-name)) 2)]
                   (async/<! (fs-sync/sync-start))
                   (state/set-state! [:ui/loading? :graph/create-remote?] false)
-                 ;; update existing repo
-                 (state/set-repos! (map (fn [r]
-                                          (if (= (:url r) repo)
-                                            (assoc r
-                                                   :GraphUUID GraphUUID
-                                                   :GraphName graph-name
-                                                   :remote? true)
-                                            r))
-                                     (state/get-repos))))))))]
+                  ;; update existing repo
+                  (state/set-repos! (map (fn [r]
+                                           (if (= (:url r) repo)
+                                             (assoc r
+                                                    :GraphUUID GraphUUID
+                                                    :GraphName graph-name
+                                                    :remote? true)
+                                             r))
+                                         (state/get-repos))))))))]
 
     [:div.cp__file-sync-related-normal-modal
-     [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "cloud-upload")]]
+     [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "cloud-upload" {:size 20})]]
 
      [:h1.text-xl.font-semibold.opacity-90.text-center.py-2
       "Are you sure you want to create a new remote graph?"]
@@ -161,8 +165,156 @@
       (ui/button "Cancel" :background "gray" :class "opacity-50" :on-click close-fn)
       (ui/button "Create remote graph" :on-click on-confirm)]]))
 
-(rum/defcs ^:large-vars/cleanup-todo indicator < rum/reactive
-  < {:key-fn #(identity "file-sync-indicator")}
+(rum/defc indicator-progress-pie
+  [percentage]
+
+  (let [*el (rum/use-ref nil)]
+    (rum/use-effect!
+     #(when-let [^js el (rum/deref *el)]
+        (set! (.. el -style -backgroundImage)
+              (util/format "conic-gradient(var(--ls-pie-fg-color) %s%, var(--ls-pie-bg-color) %s%)" percentage percentage)))
+     [percentage])
+    [:span.cp__file-sync-indicator-progress-pie {:ref *el}]))
+
+(rum/defc last-synced-cp < rum/reactive
+  []
+  (let [last-synced-at (state/sub [:file-sync/last-synced-at (state/get-current-repo)])
+        last-synced-at (if last-synced-at
+                         (util/time-ago (tc/from-long (* last-synced-at 1000)))
+                         "just now")]
+    [:div.cl
+     [:span.opacity-60 "Last change was"]
+     [:span.pl-1 last-synced-at]]))
+
+(rum/defc sync-now
+  []
+  (ui/button "Sync now"
+             :class "block cursor-pointer"
+             :small? true
+             :on-click #(async/offer! fs-sync/immediately-local->remote-chan true)))
+
+(def *last-calculated-time (atom nil))
+
+(rum/defc ^:large-vars/cleanup-todo indicator-progress-pane
+  [sync-state sync-progress
+   {:keys [idle? syncing? no-active-files? online? history-files? queuing?]}]
+
+  (rum/use-effect!
+   (fn []
+     #(reset! *last-calculated-time nil))
+   [])
+
+  (let [uploading-files        (:current-local->remote-files sync-state)
+        downloading-files      (:current-remote->local-files sync-state)
+        uploading?             (seq uploading-files)
+        downloading?           (seq downloading-files)
+
+        progressing?           (or uploading? downloading?)
+
+        full-upload-files      (:full-local->remote-files sync-state)
+        full-download-files    (:full-remote->local-files sync-state)
+        calc-progress-total    #(cond
+                                  uploading? (count full-upload-files)
+                                  downloading? (count full-download-files)
+                                  :else 0)
+        calc-progress-finished (fn []
+                                 (let [current-sync-files (set
+                                                           (->> (or (seq full-upload-files) (seq full-download-files))
+                                                                (map :path)))]
+                                   (count (filter #(and (= (:percent (second %)) 100)
+                                                        (contains? current-sync-files (first %))) sync-progress))))
+        calc-time-left         (fn [] (let [last-calculated-at (:calculated-at @*last-calculated-time)
+                                            now                (tc/to-epoch (t/now))]
+                                        (if (and last-calculated-at (< (- now last-calculated-at) 10))
+                                          (:result @*last-calculated-time)
+                                          (let [result (file-sync-handler/calculate-time-left sync-state sync-progress)]
+                                            (reset! *last-calculated-time {:calculated-at now
+                                                                           :result        result})
+                                            result))))
+
+        p-total                (if syncing? (calc-progress-total) 0)
+        p-finished             (if syncing? (calc-progress-finished) 0)
+        tip-b&p                (if (and syncing? progressing?)
+                                 [[:span (util/format "%s of %s files" p-finished p-total)]
+                                  [:div.progress-bar [:i {:style
+                                                          {:width (str (if (> p-total 0)
+                                                                         (* (/ p-finished p-total) 100) 0) "%")}}]]]
+                                 [[:span.opacity-60 "all file edits"]
+                                  (last-synced-cp)])
+        *el-ref                (rum/use-ref nil)
+        [list-active?, set-list-active?] (rum/use-state
+                                          (-> (storage/get :ui/file-sync-active-file-list?)
+                                              (#(if (nil? %) true %))))]
+
+    (rum/use-effect!
+     (fn []
+       (when-let [^js outer-class-list
+                  (some-> (rum/deref *el-ref)
+                          (.closest ".menu-links-outer")
+                          (.-classList))]
+         (->> "is-list-active"
+              (#(if list-active?
+                  (.add outer-class-list %)
+                  (.remove outer-class-list %))))
+         (storage/set :ui/file-sync-active-file-list? list-active?)))
+     [list-active?])
+
+    [:div.cp__file-sync-indicator-progress-pane
+     {:ref *el-ref
+      :class (when (and syncing? progressing?) "is-progress-active")}
+     (let [idle-&-no-active? (and idle? no-active-files?)]
+       [:div.a
+        [:div.al
+         [:strong
+          {:class (when idle-&-no-active? "is-no-active")}
+          (cond
+            (not online?) (ui/icon "wifi-off")
+            uploading? (ui/icon "arrow-up")
+            downloading? (ui/icon "arrow-down")
+            :else (ui/icon "thumb-up"))]
+         [:span
+          (cond
+            (not online?) "Currently having connection issues..."
+            idle-&-no-active? "Everything is synced!"
+            syncing? "Currently syncing your graph..."
+            :else "Waiting..."
+            )]]
+        [:div.ar
+         (when queuing? (sync-now))]])
+
+     [:div.b.dark:text-gray-200
+      [:div.bl
+       [:span.flex.items-center
+        (if no-active-files?
+          [:span.opacity-100.pr-1 "Successfully processed"]
+          [:span.opacity-60.pr-1 "Processed"])]
+
+       (first tip-b&p)]
+
+      [:div.br
+       [:small.opacity-50
+        (when syncing?
+          (calc-time-left))]]]
+
+     [:div.c
+      (second tip-b&p)
+      (when (or history-files? (not no-active-files?))
+        [:span.inline-flex.ml-1.active:opacity-50
+         {:on-click #(set-list-active? (not list-active?))}
+         (if list-active?
+           (ui/icon "chevron-up" {:style {:font-size 24}})
+           (ui/icon "chevron-left" {:style {:font-size 24}}))])]]))
+
+(defn- sort-files
+  [progress files]
+  (sort-by (fn [f]
+             (let [percent (or (:percent (get progress f)) 0)]
+               (if (= percent 100) -1 percent)))
+           > files))
+
+(rum/defcs ^:large-vars/cleanup-todo indicator <
+  rum/reactive
+  {:key-fn #(identity "file-sync-indicator")}
   {:will-mount   (fn [state]
                    (let [unsub-fn (file-sync-handler/setup-file-sync-event-listeners)]
                      (assoc state ::unsub-events unsub-fn)))
@@ -170,77 +322,89 @@
                    (apply (::unsub-events state) nil)
                    state)}
   [_state]
-  (let [_                      (state/sub :auth/id-token)
-        current-repo           (state/get-current-repo)
-        creating-remote-graph? (state/sub [:ui/loading? :graph/create-remote?])
-        sync-state             (state/sub [:file-sync/sync-state current-repo])
-        _                      (rum/react file-sync-handler/refresh-file-sync-component)
-        synced-file-graph?     (file-sync-handler/synced-file-graph? current-repo)
-        uploading-files        (:current-local->remote-files sync-state)
-        downloading-files      (:current-remote->local-files sync-state)
-        queuing-files          (:queued-local->remote-files sync-state)
-
-        status                 (:state sync-state)
-        status                 (or (nil? status) (keyword (name status)))
-        off?                   (fs-sync/sync-off? sync-state)
-        full-syncing?          (contains? #{:local->remote-full-sync :remote->local-full-sync} status)
-        syncing?               (or full-syncing? (contains? #{:local->remote :remote->local} status))
-        idle?                  (contains? #{:idle} status)
-        need-password?         (and (contains? #{:need-password} status)
-                                    (not (fs-sync/graph-encrypted?)))
-        queuing?               (and idle? (boolean (seq queuing-files)))
-        no-active-files?       (empty? (concat downloading-files queuing-files uploading-files))
-        create-remote-graph-fn #(when (and current-repo (not (config/demo-graph? current-repo)))
-                                  (let [graph-name
-                                        (js/decodeURI (util/node-path.basename current-repo))
-
-                                        confirm-fn
-                                        (fn [close-fn]
-                                          (create-remote-graph-panel current-repo graph-name close-fn))]
-
-                                    (state/set-modal! confirm-fn {:center? true :close-btn? false})))
-        turn-on                (fn []
-                                 (when-not (file-sync-handler/current-graph-sync-on?)
-                                   (async/go
-                                     (async/<! (p->c (persist-var/-load fs-sync/graphs-txid)))
-                                     (cond
-                                       @*beta-unavailable?
-                                       (state/pub-event! [:file-sync/onboarding-tip :unavailable])
-
-                                       ;; current graph belong to other user, do nothing
-                                       (and (first @fs-sync/graphs-txid)
-                                            (not (fs-sync/check-graph-belong-to-current-user (user-handler/user-uuid)
-                                                                                             (first @fs-sync/graphs-txid))))
-                                       nil
-
-                                       (and synced-file-graph?
-                                            (fs-sync/graph-sync-off? current-repo)
-                                            (second @fs-sync/graphs-txid)
-                                            (async/<! (fs-sync/<check-remote-graph-exists (second @fs-sync/graphs-txid))))
-                                       (fs-sync/sync-start)
-
-                                       ;; remote graph already has been deleted, clear repos first, then create-remote-graph
-                                       synced-file-graph?      ; <check-remote-graph-exists -> false
-                                       (do (state/set-repos!
-                                            (map (fn [r]
-                                                   (if (= (:url r) current-repo)
-                                                     (dissoc r :GraphUUID :GraphName :remote?)
-                                                     r))
-                                              (state/get-repos)))
-                                           (create-remote-graph-fn))
-
-                                       (second @fs-sync/graphs-txid) ; sync not started yet
-                                       nil
-
-                                       :else
-                                       (create-remote-graph-fn)))))]
-
+  (let [_                       (state/sub :auth/id-token)
+        online?                 (state/sub :network/online?)
+        enabled-progress-panel? (util/electron?)
+        current-repo            (state/get-current-repo)
+        creating-remote-graph?  (state/sub [:ui/loading? :graph/create-remote?])
+        sync-state              (state/sub [:file-sync/sync-state current-repo])
+        sync-progress           (state/sub [:file-sync/progress (second @fs-sync/graphs-txid)])
+        _                       (rum/react file-sync-handler/refresh-file-sync-component)
+        synced-file-graph?      (file-sync-handler/synced-file-graph? current-repo)
+        uploading-files         (sort-files sync-progress (:current-local->remote-files sync-state))
+        downloading-files       (sort-files sync-progress (:current-remote->local-files sync-state))
+        queuing-files           (:queued-local->remote-files sync-state)
+        history-files           (:history sync-state)
+        status                  (:state sync-state)
+        status                  (or (nil? status) (keyword (name status)))
+        off?                    (fs-sync/sync-off? sync-state)
+        full-syncing?           (contains? #{:local->remote-full-sync :remote->local-full-sync} status)
+        syncing?                (or full-syncing? (contains? #{:local->remote :remote->local} status))
+        idle?                   (contains? #{:idle} status)
+        need-password?          (and (contains? #{:need-password} status)
+                                     (not (fs-sync/graph-encrypted?)))
+        queuing?                (and idle? (boolean (seq queuing-files)))
+        no-active-files?        (empty? (concat downloading-files queuing-files uploading-files))
+        create-remote-graph-fn  #(when (and current-repo (not (config/demo-graph? current-repo)))
+                                   (let [graph-name
+                                         (js/decodeURI (util/node-path.basename current-repo))
+
+                                         confirm-fn
+                                         (fn [close-fn]
+                                           (create-remote-graph-panel current-repo graph-name close-fn))]
+
+                                     (state/set-modal! confirm-fn {:center? true :close-btn? false})))
+        turn-on                 (->
+                                 (fn []
+                                   (when-not (file-sync-handler/current-graph-sync-on?)
+                                     (async/go
+                                       (async/<! (p->c (persist-var/-load fs-sync/graphs-txid)))
+                                       (cond
+                                         @*beta-unavailable?
+                                         (state/pub-event! [:file-sync/onboarding-tip :unavailable])
+
+                                         ;; current graph belong to other user, do nothing
+                                         (and (first @fs-sync/graphs-txid)
+                                              (not (fs-sync/check-graph-belong-to-current-user (user-handler/user-uuid)
+                                                                                               (first @fs-sync/graphs-txid))))
+                                         nil
+
+                                         (and synced-file-graph?
+                                              (fs-sync/graph-sync-off? current-repo)
+                                              (second @fs-sync/graphs-txid)
+                                              (async/<! (fs-sync/<check-remote-graph-exists (second @fs-sync/graphs-txid))))
+                                         (do
+                                           (prn "sync start")
+                                           (fs-sync/sync-start))
+
+                                         ;; remote graph already has been deleted, clear repos first, then create-remote-graph
+                                         synced-file-graph?  ; <check-remote-graph-exists -> false
+                                         (do (state/set-repos!
+                                              (map (fn [r]
+                                                     (if (= (:url r) current-repo)
+                                                       (dissoc r :GraphUUID :GraphName :remote?)
+                                                       r))
+                                                (state/get-repos)))
+                                             (create-remote-graph-fn))
+
+                                         (second @fs-sync/graphs-txid) ; sync not started yet
+                                         nil
+
+                                         :else
+                                         (create-remote-graph-fn)))))
+                                 (debounce 1500))]
     (if creating-remote-graph?
       (ui/loading "")
       [:div.cp__file-sync-indicator
+       {:class (util/classnames
+                [{:is-enabled-progress-pane enabled-progress-panel?
+                  :has-active-files         (not no-active-files?)}
+                 (str "status-of-" (and (keyword? status) (name status)))])}
        (when (and (not config/publishing?)
                   (user-handler/logged-in?))
+
          (ui/dropdown-with-links
+          ;; trigger
           (fn [{:keys [toggle-fn]}]
             (if (not off?)
               [:a.button.cloud.on
@@ -250,24 +414,30 @@
                                              :queuing queuing?
                                              :idle    (and (not queuing?) idle?)}])}
                [:span.flex.items-center
-                (ui/icon "cloud"
-                         {:style {:fontSize ui/icon-size}})]]
+                (ui/icon "cloud" {:size ui/icon-size})]]
 
               [:a.button.cloud.off
                {:on-click turn-on}
-               (ui/icon "cloud-off" {:style {:fontSize ui/icon-size}})]))
+               (ui/icon "cloud-off" {:size ui/icon-size})]))
 
+          ;; links
           (cond-> []
             synced-file-graph?
             (concat
              (if (and no-active-files? idle?)
-               [{:item [:div.flex.justify-center.w-full.py-2
-                        [:span.opacity-60 "Everything is synced!"]]
-                 :as-link? false}]
-               (if need-password?
+               [(when-not (util/electron?)
+                  {:item     [:div.flex.justify-center.w-full.py-2
+                              [:span.opacity-60 "Everything is synced!"]]
+                   :as-link? false})]
+
+               (cond
+                 need-password?
                  [{:title   [:div.file-item
                              (ui/icon "lock") "Password is required"]
                    :options {:on-click fs-sync/sync-need-password!}}]
+
+                 ;; head of upcoming sync
+                 (not no-active-files?)
                  [{:title   [:div.file-item.is-first ""]
                    :options {:class "is-first-placeholder"}}]))
 
@@ -275,9 +445,18 @@
                                    {:key (str "downloading-" f)}
                                    (js/decodeURIComponent f)]
                            :key   (str "downloading-" f)
-                           :icon  (ui/icon "arrow-narrow-down")}) downloading-files)
+                           :icon  (if enabled-progress-panel?
+                                    (let [progress (get sync-progress f)
+                                          percent (or (:percent progress) 0)]
+                                      (if (and (number? percent)
+                                               (< percent 100))
+                                        (indicator-progress-pie percent)
+                                        (ui/icon "circle-check")))
+                                    (ui/icon "arrow-narrow-down"))
+                           }) downloading-files)
+
              (map (fn [e] (let [icon (case (.-type e)
-                                       "add"    "plus"
+                                       "add" "plus"
                                        "unlink" "minus"
                                        "edit")
                                 path (fs-sync/relative-path e)]
@@ -286,40 +465,60 @@
                                      (js/decodeURIComponent path)]
                              :key   (str "queue-" path)
                              :icon  (ui/icon icon)})) (take 10 queuing-files))
+
              (map (fn [f] {:title [:div.file-item
                                    {:key (str "uploading-" f)}
                                    (js/decodeURIComponent f)]
                            :key   (str "uploading-" f)
-                           :icon  (ui/icon "arrow-up")}) uploading-files)
-
-             (when sync-state
+                           :icon  (if enabled-progress-panel?
+                                    (let [progress (get sync-progress f)
+                                          percent (or (:percent progress) 0)]
+                                      (if (and (number? percent)
+                                               (< percent 100))
+                                        (indicator-progress-pie percent)
+                                        (ui/icon "circle-check")))
+                                    (ui/icon "arrow-up"))
+                           }) uploading-files)
+
+             (when (seq history-files)
                (map-indexed (fn [i f] (:time f)
-                              (let [path       (:path f)
-                                    ext        (string/lower-case (util/get-file-ext path))
+                              (let [path        (:path f)
+                                    ext         (string/lower-case (util/get-file-ext path))
                                     _supported? (gp-config/mldoc-support? ext)
-                                    full-path  (util/node-path.join (config/get-repo-dir current-repo) path)
-                                    page-name  (db/get-file-page full-path)]
+                                    full-path   (util/node-path.join (config/get-repo-dir current-repo) path)
+                                    page-name   (db/get-file-page full-path)]
                                 {:title [:div.files-history.cursor-pointer
-                                         {:key i :class (when (= i 0) "is-first")
+                                         {:key      i :class (when (= i 0) "is-first")
                                           :on-click (fn []
                                                       (if page-name
                                                         (rfe/push-state :page {:name page-name})
                                                         (rfe/push-state :file {:path full-path})))}
                                          [:span.file-sync-item (js/decodeURIComponent (:path f))]
                                          [:div.opacity-50 (ui/humanity-time-ago (:time f) nil)]]}))
-                            (take 10 (:history sync-state))))))
+                            (take 10 history-files)))))
 
-          {:links-header
+          ;; options
+          {:outer-header
            [:<>
-            (when (and synced-file-graph? queuing?)
-              [:div.head-ctls
-               (ui/button "Sync now"
-                          :class "block cursor-pointer"
-                          :small? true
-                          :on-click #(async/offer! fs-sync/immediately-local->remote-chan true))])
-
-                                        ;(when config/dev?
-                                        ;  [:strong.debug-status (str status)])
+            (when (util/electron?)
+              (indicator-progress-pane
+               sync-state sync-progress
+               {:idle?            idle?
+                :syncing?         syncing?
+                :need-password?   need-password?
+                :full-sync?       full-syncing?
+                :online?          online?
+                :queuing?         queuing?
+                :no-active-files? no-active-files?
+                :history-files?   (seq history-files)}))
+
+            (when (and
+                   (not enabled-progress-panel?)
+                   synced-file-graph? queuing?)
+              [:div.head-ctls (sync-now)])
+
+            ;(when config/dev?
+            ;  [:strong.debug-status (str status)])
             ]}))])))
 
 (rum/defc pick-local-graph-for-sync [graph]
@@ -369,9 +568,7 @@
                                                          (nil? (second info))
                                                          (not= (second info) (:GraphUUID graph))))
                                             (if (js/confirm "This directory is not empty, are you sure to sync the remote graph to it? Make sure to back up the directory first.")
-                                              (do
-                                                (state/set-state! :graph/remote-binding? true)
-                                                (p/resolved nil))
+                                              (p/resolved nil)
                                               (throw (js/Error. nil)))))))
 
                             ;; cancel pick a directory
@@ -416,7 +613,7 @@
        [:div.p-4 (ui/loading "Loading...")]
        (for [version version-files]
          (let [version-uuid (get-version-key version)
-               local?      (some? (:relative-path version))]
+               local?       (some? (:relative-path version))]
            [:div.version-list-item {:key version-uuid}
             [:a.item-link.block.fade-link.flex.justify-between
              {:title    version-uuid
@@ -551,7 +748,7 @@
                  :disabled loading?
                  :on-click (fn []
                              (set-loading? true)
-                             (let [result (:user/info @state/state)
+                             (let [result  (:user/info @state/state)
                                    ex-time (:ExpireTime result)]
                                (if (and (number? ex-time)
                                         (< (* ex-time 1000) (js/Date.now)))
@@ -589,7 +786,7 @@
   [close-fn]
 
   [:div.cp__file-sync-related-normal-modal
-   [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "checkup-list")]]
+   [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "checkup-list" {:size 28})]]
 
    [:h1.text-xl.font-semibold.opacity-90.text-center.py-2
     [:span.dark:opacity-80 "Congrats on your first successful sync!"]]
@@ -604,7 +801,7 @@
 
    [:div.cloud-tip.rounded-md.mt-6.py-4
     [:div.items-center.opacity-90.flex.justify-center
-     [:span.pr-2 (ui/icon "bell-ringing" {:class "font-semibold"})]
+     [:span.pr-2.flex (ui/icon "bell-ringing" {:class "font-semibold"})]
      [:strong "Logseq Sync is still in Beta and we're working on a Pro plan!"]]
 
     ;; [:ul.flex.py-6.px-4
@@ -677,5 +874,5 @@
 
           (state/pub-event! [:file-sync/onboarding-tip type])
           (state/set-state! [:file-sync/onboarding-state (keyword type)] true)))
-      (catch js/Error e
+      (catch :default e
         (js/console.warn "[onboarding SKIP] " (name type) e)))))

+ 145 - 14
src/main/frontend/components/file_sync.css

@@ -1,9 +1,5 @@
 .cp__file-sync {
   &-indicator {
-    --ls-color-file-sync-error: #ff0000;
-    --ls-color-file-sync-pending: #ffbb4d;
-    --ls-color-file-sync-idle: #04b404;
-
     a.cloud {
       position: relative;
       opacity: 1 !important;
@@ -68,12 +64,14 @@
       width: 90vw;
       position: fixed;
       right: 5vw;
-      border-radius: 4px;
 
       .head-ctls {
         position: absolute;
-        top: 2px;
-        right: 5px;
+        right: 10px;
+        transform: translateY(8px);
+      }
+
+      .cp__file-sync-indicator-progress-pane {
       }
 
       @screen md {
@@ -101,20 +99,21 @@
           padding: 0 !important;
           user-select: none;
           pointer-events: none;
+          margin-top: 5px;
 
           &:hover {
             background-color: unset !important;
           }
         }
-      }
 
-      i.ti {
-        transform: translate(0);
-        margin-right: 5px;
+        i.ti {
+          transform: translate(0);
+          margin-right: 5px;
+        }
       }
 
       .files-history.is-first, .file-item.is-first {
-        margin-top: 30px;
+        margin-top: 40px;
         position: relative;
 
         &:before {
@@ -142,13 +141,133 @@
       }
     }
 
+    .menu-links-outer {
+      @apply py-0;
+    }
+
     .menu-links-wrapper {
       padding: 4px 0;
-      max-height: 60vh !important;
+      max-height: calc(70vh - 120px) !important;
       overflow-y: auto;
+
+      .title-wrap {
+        flex: 1;
+      }
+    }
+
+    &.is-enabled-progress-pane {
+      .head-ctls {
+        display: none;
+      }
+
+      .menu-links-outer {
+        &.is-list-active {
+          .menu-links-wrapper, .head-ctls {
+            display: block !important;
+          }
+
+          .menu-links-wrapper {
+            overflow-y: auto;
+          }
+        }
+      }
+
+      .menu-links-wrapper {
+        @apply p-0 hidden overflow-hidden;
+
+        > .menu-link:last-child {
+          padding-bottom: 16px;
+        }
+      }
     }
   }
 
+  &-indicator-progress-pane {
+    @apply px-4 pt-4 pb-3 w-full overflow-hidden;
+
+    -webkit-font-smoothing: antialiased;
+    background-color: var(--ls-quaternary-background-color);
+
+    .ti {
+      @apply translate-y-0;
+    }
+
+    > .a {
+      @apply flex justify-between text-gray-200;
+
+      > .al {
+        @apply flex text-base flex-1 w-0.5 items-center space-x-2 font-semibold;
+
+        > strong {
+          @apply flex items-center justify-center font-extralight text-gray-800;
+
+          background-color: var(--ls-color-file-sync-pending);
+          border-radius: 4px;
+          height: 24px;
+          width: 24px;
+
+          &.is-no-active {
+            background-color: var(--ls-color-file-sync-idle);
+          }
+        }
+
+        > span {
+          @apply w-full overflow-hidden overflow-ellipsis pr-2;
+        }
+      }
+
+      > .ar {
+        @apply relative;
+
+        top: -4px;
+      }
+    }
+
+    > .b {
+      @apply flex items-center justify-between pt-2 pb-3 text-sm;
+
+      .bl {
+        @apply flex items-center leading-none pt-2;
+      }
+    }
+
+    > .c {
+      @apply flex items-center text-gray-200 relative;
+
+      top: -3px;
+
+      .cl {
+        @apply text-sm leading-6 flex flex-1 items-center overflow-hidden;
+      }
+
+      .progress-bar {
+        @apply flex flex-1 items-center overflow-hidden relative;
+
+        height: 7px;
+        top: 1px;
+        border-radius: 4px;
+        background-color: var(--ls-secondary-background-color);
+
+        i {
+          transition: width .5s;
+          height: 100%;
+          width: 0;
+          border-radius: 4px;
+          background-color: var(--ls-link-text-color);
+        }
+      }
+    }
+  }
+
+  &-indicator-progress-pie {
+    width: 16px;
+    height: 16px;
+    border-radius: 50%;
+    display: inline-block;
+    margin-right: 2px;
+    margin-bottom: -1px;
+  }
+
   &-page-histories {
     @apply flex;
 
@@ -463,6 +582,18 @@ html[data-theme='light'] {
         background-color: #e1e1e1;
       }
     }
+
+    &-indicator-progress-pane {
+      background-color: var(--ls-tertiary-background-color);
+
+      > .a, > .c {
+        @apply text-gray-700;
+      }
+
+      .progress-bar {
+        background-color: var(--ls-quaternary-background-color);
+      }
+    }
   }
 }
 
@@ -474,4 +605,4 @@ html:not(.is-electron) {
       }
     }
   }
-}
+}

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

@@ -31,7 +31,7 @@
                    (when (mobile-util/native-iphone?)
                      (state/set-left-sidebar-open! false))
                    (route-handler/redirect-to-home!))}
-     (ui/icon "home" {:style {:fontSize ui/icon-size}})]))
+     (ui/icon "home" {:size ui/icon-size})]))
 
 (rum/defc login < rum/reactive
   < {:key-fn #(identity "login-button")}
@@ -55,7 +55,7 @@
     [:button.#left-menu.cp__header-left-menu.button.icon
      {:title "Toggle left menu"
       :on-click on-click}
-     (ui/icon "menu-2" {:style {:fontSize ui/icon-size}})]))
+     (ui/icon "menu-2" {:size ui/icon-size})]))
 
 (rum/defc dropdown-menu < rum/reactive
   < {:key-fn #(identity "repos-dropdown-menu")}
@@ -68,7 +68,7 @@
        [:button.button.icon.toolbar-dots-btn
         {:on-click toggle-fn
          :title "More"}
-        (ui/icon "dots" {:style {:fontSize ui/icon-size}})])
+        (ui/icon "dots" {:size ui/icon-size})])
      (->>
       [(when (state/enable-editing?)
          {:title (t :settings)
@@ -118,12 +118,12 @@
    (ui/with-shortcut :go/backward "bottom"
      [:button.it.navigation.nav-left.button.icon
       {:title "Go back" :on-click #(js/window.history.back)}
-      (ui/icon "arrow-left" {:style {:fontSize ui/icon-size}})])
+      (ui/icon "arrow-left" {:size ui/icon-size})])
 
    (ui/with-shortcut :go/forward "bottom"
      [:button.it.navigation.nav-right.button.icon
       {:title "Go forward" :on-click #(js/window.history.forward)}
-      (ui/icon "arrow-right" {:style {:fontSize ui/icon-size}})])])
+      (ui/icon "arrow-right" {:size ui/icon-size})])])
 
 (rum/defc updater-tips-new-version
   [t]
@@ -186,14 +186,14 @@
                                         (mobile-util/native-iphone?))
                                 (state/set-left-sidebar-open! false))
                               (state/pub-event! [:go/search]))}
-              (ui/icon "search" {:style {:fontSize ui/icon-size}})]))])
+              (ui/icon "search" {:size ui/icon-size})]))])
       (when (mobile-util/native-platform?)
         (if (or (state/home?) custom-home-page?)
           left-menu
           (ui/with-shortcut :go/backward "bottom"
-            [:button.it.navigation.nav-left.button.icon
+            [:button.it.navigation.nav-left.button.icon.opacity-70
              {:title "Go back" :on-click #(js/window.history.back)}
-             (ui/icon "chevron-left" {:style {:fontSize 25}})])))]
+             (ui/icon "chevron-left" {:size 26})])))]
 
      [:div.r.flex
       (when (and sync-enabled?

+ 10 - 10
src/main/frontend/components/header.css

@@ -1,5 +1,6 @@
 .cp__header {
   @apply z-10;
+
   -webkit-app-region: drag;
 
   padding-top: var(--ls-headbar-inner-top-padding);
@@ -17,7 +18,8 @@
   white-space: nowrap;
 
   > .l {
-    @apply pl-4;
+    @apply pl-2;
+    
     width: var(--ls-left-sidebar-width);
     height: 100%;
     align-items: center;
@@ -38,10 +40,6 @@
     transform: translate3d(0, 0, 0);
   }
 
-  .it svg {
-    transform: scale(0.8);
-  }
-
   .button {
     display: flex;
     align-items: center;
@@ -130,10 +128,6 @@
   }
 }
 
-.cp__header .navigation svg {
-  transform: scale(0.7);
-}
-
 .is-electron.is-mac.is-fullscreen .cp__header > .l {
   padding-left: 1rem;
 }
@@ -178,8 +172,14 @@
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
-  max-width: 15ch;
+  max-width: 20ch;
   height: 14px;
+
+  .ui__icon {
+    position: relative;
+    top: 5px;
+    right: -1px;
+  }
 }
 
 .button {

+ 12 - 6
src/main/frontend/components/page.cljs

@@ -985,23 +985,27 @@
              [:a.button.journal
               {:class    (util/classnames [{:active (boolean @*journal?)}])
                :on-click #(reset! *journal? (not @*journal?))}
-              (ui/icon "calendar" {:style {:fontSize ui/icon-size}})])]
+              (ui/icon "calendar" {:size ui/icon-size})])]
 
            [:div.paginates
             [:span.flex.items-center
              {:class (util/classnames [{:is-first (= 1 @*current-page)
                                         :is-last  (= @*current-page total-pages)}])}
              (when has-prev?
-               [:a.py-4.pr-2.fade-link {:on-click #(to-page (dec @*current-page))} (ui/icon "caret-left") (str " " (t :paginates/prev))])
+               [:a.py-4.pr-2.fade-link.flex.items-center
+                {:on-click #(to-page (dec @*current-page))}
+                (ui/icon "caret-left") (str " " (t :paginates/prev))])
              [:span.opacity-60 (str @*current-page "/" total-pages)]
              (when has-next?
-               [:a.py-4.pl-2.fade-link {:on-click #(to-page (inc @*current-page))} (str (t :paginates/next) " ") (ui/icon "caret-right")])]]
+               [:a.py-4.pl-2.fade-link.flex.items-center
+                {:on-click #(to-page (inc @*current-page))} (str (t :paginates/next) " ")
+                (ui/icon "caret-right")])]]
 
            (ui/dropdown-with-links
             (fn [{:keys [toggle-fn]}]
               [:a.button.fade-link
                {:on-click toggle-fn}
-               (ui/icon "dots" {:style {:fontSize ui/icon-size}})])
+               (ui/icon "dots" {:size ui/icon-size})])
             [{:title (t :remove-orphaned-pages)
               :options {:on-click (fn []
                                     (let [orphaned-pages (model/get-orphaned-pages {})
@@ -1075,6 +1079,8 @@
           [:span]
           [:span.flex.items-center
            (when has-prev?
-             [:a.py-4.text-sm.fade-link {:on-click #(to-page (dec @*current-page))} (ui/icon "caret-left") (str " " (t :paginates/prev))])
+             [:a.py-4.text-sm.fade-link.flex.items-center {:on-click #(to-page (dec @*current-page))}
+              (ui/icon "caret-left") (str " " (t :paginates/prev))])
            (when has-next?
-             [:a.py-4.pl-2.text-sm.fade-link {:on-click #(to-page (inc @*current-page))} (str (t :paginates/next) " ") (ui/icon "caret-right")])]]]))]))
+             [:a.py-4.pl-2.text-sm.fade-link.flex.items-center {:on-click #(to-page (inc @*current-page))} (str (t :paginates/next) " ")
+              (ui/icon "caret-right")])]]]))]))

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

@@ -132,13 +132,15 @@
     }
 
     .r {
-      font-size: 14px;
-
+      @apply text-base space-x-2;
+      
       a.button {
         color: var(--ls-primary-text-color);
         margin-top: 1px;
         height: unset;
         padding: 4px;
+        display: flex;
+        align-items: center;
 
         &.active {
           opacity: 1;
@@ -326,9 +328,7 @@ html.is-native-ios {
   .cp__all_pages {
     .actions > .r {
       position: relative;
-      padding: 15px 0;
-      padding-right: 10px;
-      padding-bottom: 25px;
+      padding: 15px 10px 25px 0;
       justify-content: space-between;
 
       .paginates {

+ 15 - 12
src/main/frontend/components/plugins.cljs

@@ -130,14 +130,14 @@
 (rum/defc category-tabs
   [t category on-action]
 
-  [:div.secondary-tabs.categories
+  [:div.secondary-tabs.categories.flex
    (ui/button
-    [:span (ui/icon "puzzle") (t :plugins)]
+    [:span.flex.items-center (ui/icon "puzzle") (t :plugins)]
     :intent "logseq"
     :on-click #(on-action :plugins)
     :class (if (= category :plugins) "active" ""))
    (ui/button
-    [:span (ui/icon "palette") (t :themes)]
+    [:span.flex.items-center (ui/icon "palette") (t :themes)]
     :intent "logseq"
     :on-click #(on-action :themes)
     :class (if (= category :themes) "active" ""))])
@@ -232,8 +232,10 @@
        [:strong (ui/icon "coffee")]
        [:ul.menu-list
         (for [link sponsors]
-          [:li [:a {:href link :target "_blank"}
-                [:span.flex.items-center link (ui/icon "external-link")]]])]])]
+          [:li {:key link}
+           [:a {:href link :target "_blank"}
+            [:span.flex.items-center link (ui/icon "external-link")]]])
+        ]])]
 
    [:div.r.flex.items-center
     (when (and unpacked? (not disabled?))
@@ -438,7 +440,8 @@
          (ui/tippy {:html  [:div (t :plugin/unpacked-tips)]
                     :arrow true}
                    (ui/button
-                    [:span (ui/icon "upload") (t :plugin/load-unpacked)]
+                    [:span.flex.items-center
+                     (ui/icon "upload") (t :plugin/load-unpacked)]
                     :intent "logseq"
                     :class "load-unpacked"
                     :on-click plugin-handler/load-unpacked-plugin))
@@ -534,21 +537,21 @@
           :intent "link"))
 
        (concat (if market?
-                 [{:title   [:span (ui/icon "rotate-clockwise") (t :plugin/refresh-lists)]
+                 [{:title   [:span.flex.items-center (ui/icon "rotate-clockwise") (t :plugin/refresh-lists)]
                    :options {:on-click #(reload-market-fn)}}]
-                 [{:title   [:span (ui/icon "rotate-clockwise") (t :plugin/check-all-updates)]
+                 [{:title   [:span.flex.items-center (ui/icon "rotate-clockwise") (t :plugin/check-all-updates)]
                    :options {:on-click #(plugin-handler/check-enabled-for-updates (not= :plugins category))}}])
 
-               [{:title   [:span (ui/icon "world") (t :settings-page/network-proxy)]
+               [{:title   [:span.flex.items-center (ui/icon "world") (t :settings-page/network-proxy)]
                  :options {:on-click #(state/pub-event! [:go/proxy-settings agent-opts])}}]
 
                (when (state/developer-mode?)
                  [{:hr true}
-                  {:title   [:span (ui/icon "file-code") "Open Preferences"]
+                  {:title   [:span.flex.items-center (ui/icon "file-code") "Open Preferences"]
                    :options {:on-click
                              #(p/let [root (plugin-handler/get-ls-dotdir-root)]
                                 (js/apis.openPath (str root "/preferences.json")))}}
-                  {:title   [:span (ui/icon "bug") "Open " [:code " ~/.logseq"]]
+                  {:title   [:span.flex.items-center (ui/icon "bug") "Open " [:code " ~/.logseq"]]
                    :options {:on-click
                              #(p/let [root (plugin-handler/get-ls-dotdir-root)]
                                 (js/apis.openPath root))}}]))
@@ -852,7 +855,7 @@
    (fn [{:keys [toggle-fn]}]
      [:div.toolbar-plugins-manager
       {:on-click toggle-fn}
-      [:a.button (ui/icon "puzzle")]])
+      [:a.button (ui/icon "puzzle" {:size 20})]])
 
    ;; items
    (for [[_ {:keys [key pinned?] :as opts} pid] items

+ 5 - 0
src/main/frontend/components/plugins.css

@@ -231,6 +231,11 @@
           opacity: .8;
         }
       }
+
+      a {
+        color: var(--ls-primary-text-color);
+        opacity: .7;
+      }
     }
 
     > .l {

+ 82 - 76
src/main/frontend/components/query_table.cljs

@@ -2,6 +2,7 @@
   (:require [frontend.components.svg :as svg]
             [frontend.date :as date]
             [frontend.db :as db]
+            [frontend.db.query-dsl :as query-dsl]
             [frontend.handler.common :as common-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.state :as state]
@@ -13,38 +14,9 @@
             [rum.core :as rum]
             [frontend.modules.outliner.tree :as tree]))
 
-;; TODO: extract to table utils
-(defn- sort-result-by
-  [by-item desc? result]
-  (let [comp (if desc? > <)]
-    (sort-by by-item comp result)))
-
-(rum/defc sortable-title
-  [title key by-item desc? block-id]
-  [:th.whitespace-nowrap
-   [:a {:on-click (fn []
-                    (reset! by-item key)
-                    (swap! desc? not)
-                    (when block-id
-                      (when key
-                        (editor-handler/set-block-property! block-id :query-sort-by (name key)))
-                      (editor-handler/set-block-property! block-id :query-sort-desc @desc?)))}
-    [:div.flex.items-center
-     [:span.mr-1 title]
-     (when (= @by-item key)
-       [:span
-        (if @desc? (svg/caret-down) (svg/caret-up))])]]])
-
-(defn get-keys
-  [result page?]
-  (let [keys (->> (distinct (mapcat keys (map :block/properties result)))
-                  (remove (property/built-in-properties))
-                  (remove #{:template}))
-        keys (if page? (cons :page keys) (cons :block keys))
-        keys (if page? (distinct (concat keys [:created-at :updated-at])) keys)]
-    keys))
-
-(defn attach-clock-property
+;; Util fns
+;; ========
+(defn- attach-clock-property
   [result]
   (let [ks [:block/properties :clock-time]
         result (map (fn [b]
@@ -55,8 +27,8 @@
       (map #(medley/dissoc-in % ks) result)
       result)))
 
-(defn- sort-by-fn [sort-by-item item]
-  (case sort-by-item
+(defn- sort-by-fn [sort-by-column item]
+  (case sort-by-column
     :created-at
     (:block/created-at item)
     :updated-at
@@ -65,72 +37,106 @@
     (:block/content item)
     :page
     (:block/name item)
-    (get-in item [:block/properties sort-by-item])))
+    (get-in item [:block/properties sort-by-column])))
+
+(defn- sort-result [result {:keys [sort-by-column sort-desc?]}]
+  (if (some? sort-by-column)
+    (let [comp (if sort-desc? > <)]
+      (sort-by (fn [item]
+                 (block/normalize-block (sort-by-fn sort-by-column item)))
+               comp
+               result))
+    result))
+
+(defn- get-sort-state
+  "Return current sort direction and column being sorted, respectively
+  :sort-desc? and :sort-by-column. :sort-by-column is nil if no sorting is to be
+  done"
+  [current-block]
+  (let [p-desc? (get-in current-block [:block/properties :query-sort-desc])
+        desc? (if (some? p-desc?) p-desc? true)
+        p-sort-by (keyword (get-in current-block [:block/properties :query-sort-by]))
+        sort-by-column (or (some-> p-sort-by keyword)
+                         (if (query-dsl/query-contains-filter? (:block/content current-block) "sort-by")
+                           nil
+                           :updated-at))]
+    {:sort-desc? desc?
+     :sort-by-column sort-by-column}))
+
+;; Components
+;; ==========
+(rum/defc sortable-title
+  [title column {:keys [sort-by-column sort-desc?]} block-id]
+  [:th.whitespace-nowrap
+   [:a {:on-click (fn []
+                    (editor-handler/set-block-property! block-id :query-sort-by (name column))
+                    (editor-handler/set-block-property! block-id :query-sort-desc (not sort-desc?)))}
+    [:div.flex.items-center
+     [:span.mr-1 title]
+     (when (= sort-by-column column)
+       [:span
+        (if sort-desc? (svg/caret-down) (svg/caret-up))])]]])
+
+(defn get-keys
+  "Get keys for a query table result, which are the columns in a table"
+  [result page?]
+  (let [keys (->> (distinct (mapcat keys (map :block/properties result)))
+                  (remove (property/built-in-properties))
+                  (remove #{:template}))
+        keys (if page? (cons :page keys) (cons :block keys))
+        keys (if page? (distinct (concat keys [:created-at :updated-at])) keys)]
+    keys))
 
-(defn- desc?
-  [*desc? p-desc?]
-  (cond
-    (some? @*desc?)
-    @*desc?
-    (some? p-desc?)
-    p-desc?
-    :else
-    true))
+(defn- get-columns [current-block result {:keys [page?]}]
+  (let [query-properties (some-> (get-in current-block [:block/properties :query-properties] "")
+                                 (common-handler/safe-read-string "Parsing query properties failed"))
+        columns (if (seq query-properties)
+                  query-properties
+                  (get-keys result page?))
+        included-columns #{:created-at :updated-at}]
+    (distinct
+     (if (some included-columns columns)
+       (concat (remove included-columns columns)
+               (filter included-columns columns)
+               included-columns)
+       columns))))
 
+;; Table rows are called items
 (rum/defcs result-table < rum/reactive
-  (rum/local nil ::sort-by-item)
-  (rum/local nil ::desc?)
   (rum/local false ::select?)
   [state config current-block result {:keys [page?]} map-inline page-cp ->elem inline-text]
   (when current-block
     (let [result (tree/filter-top-level-blocks result)
-          p-sort-by (keyword (get-in current-block [:block/properties :query-sort-by]))
-          p-desc? (get-in current-block [:block/properties :query-sort-desc])
           select? (get state ::select?)
-          *sort-by-item (get state ::sort-by-item)
-          *desc? (get state ::desc?)
-          sort-by-item (or @*sort-by-item (some-> p-sort-by keyword) :updated-at)
           ;; remove templates
           result (remove (fn [b] (some? (get-in b [:block/properties :template]))) result)
           result (if page? result (attach-clock-property result))
           clock-time-total (when-not page?
                              (->> (map #(get-in % [:block/properties :clock-time] 0) result)
                                   (apply +)))
-          query-properties (some-> (get-in current-block [:block/properties :query-properties] "")
-                                   (common-handler/safe-read-string "Parsing query properties failed"))
-          keys (if (seq query-properties)
-                 query-properties
-                 (get-keys result page?))
-          included-keys #{:created-at :updated-at}
-          keys (distinct
-                (if (some included-keys keys)
-                  (concat (remove included-keys keys)
-                          (filter included-keys keys)
-                          included-keys)
-                  keys))
-          desc? (desc? *desc? p-desc?)
-          result (sort-result-by (fn [item]
-                                   (block/normalize-block (sort-by-fn sort-by-item item)))
-                                 desc?
-                                 result)]
+          columns (get-columns current-block result {:page? page?})
+          ;; Sort state needs to be in sync between final result and sortable title
+          ;; as user needs to know if there result is sorted
+          sort-state (get-sort-state current-block)
+          result' (sort-result result sort-state)]
       [:div.overflow-x-auto {:on-mouse-down (fn [e] (.stopPropagation e))
                              :style {:width "100%"}
                              :class (when-not page? "query-table")}
        [:table.table-auto
         [:thead
          [:tr.cursor
-          (for [key keys]
-            (let [key-name (if (and (= key :clock-time) (integer? clock-time-total))
+          (for [column columns]
+            (let [title (if (and (= column :clock-time) (integer? clock-time-total))
                              (util/format "clock-time(total: %s)" (clock/minutes->days:hours:minutes
                                                                    clock-time-total))
-                             (name key))]
-              (sortable-title key-name key *sort-by-item *desc? (:block/uuid current-block))))]]
+                             (name column))]
+              (sortable-title title column sort-state (:block/uuid current-block))))]]
         [:tbody
-         (for [item result]
+         (for [item result']
            (let [format (:block/format item)]
              [:tr.cursor
-              (for [key keys]
-                (let [value (case key
+              (for [column columns]
+                (let [value (case column
                               :page
                               [:string (or (:block/original-name item)
                                            (:block/name item))]
@@ -154,7 +160,7 @@
                               [:string (when-let [updated-at (:block/updated-at item)]
                                          (date/int->local-time-2 updated-at))]
 
-                              [:string (get-in item [:block/properties key])])]
+                              [:string (get-in item [:block/properties column])])]
                   [:td.whitespace-nowrap {:on-mouse-down (fn [] (reset! select? false))
                                           :on-mouse-move (fn [] (reset! select? true))
                                           :on-mouse-up (fn []

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

@@ -164,7 +164,7 @@
                           {:title        [:span.flex.items-center.whitespace-nowrap short-repo-name
                                           (when remote? [:span.pl-1
                                                          {:title (str "<" GraphName "> #" GraphUUID)}
-                                                         (ui/icon "cloud")])]
+                                                         (ui/icon "cloud" {:size 18})])]
                            :hover-detail repo-path ;; show full path on hover
                            :options      {:on-click (fn [e]
                                                       (if (gobj/get e "shiftKey")
@@ -228,7 +228,9 @@
                                              (check-multiple-windows? state)
                                              (toggle-fn))
                                  :title    repo-path}       ;; show full path on hover
-                                (ui/icon "database mr-2" {:style {:font-size 16} :id "database-icon"})
+                                [:span.flex.pr-2.relative
+                                 {:style {:top 1}}
+                                 (ui/icon "database" {:size 16 :id "database-icon"})]
                                 [:div.graphs
                                  [:span#repo-switch.block.pr-2.whitespace-nowrap
                                   [:span [:span#repo-name.font-medium

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

@@ -308,12 +308,12 @@
 (rum/defc recent-search-and-pages
   [in-page-search?]
   [:div.recent-search
-   [:div.px-4.py-2.text-sm.opacity-70.flex.flex-row.justify-between.align-items
+   [:div.wrap.px-4.py-2.text-sm.opacity-70.flex.flex-row.justify-between.align-items.mx-1.sm:mx-0
     [:div "Recent search:"]
     (ui/with-shortcut :go/search-in-page "bottom"
       [:div.flex-row.flex.align-items
-       [:div.mr-2 "Search blocks in page:"]
-       [:div {:style {:margin-top 3}}
+       [:div.mr-3.flex "Search blocks in page:"]
+       [:div.flex.items-center
         (ui/toggle in-page-search?
                    (fn [_value]
                      (state/set-search-mode! (if in-page-search? :global :page)))
@@ -324,7 +324,7 @@
                   :interactive     true
                   :arrow           true
                   :theme       "monospace"}
-                 [:a.inline-block.fade-link
+                 [:a.flex.fade-link.items-center
                   {:style {:margin-left 12}
                    :on-click #(state/toggle! :ui/command-palette-open?)}
                   (ui/icon "command" {:style {:font-size 20}})])])]

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

@@ -517,6 +517,7 @@
               (when-let [e (and protocol host port (str protocol "://" host ":" port))]
                 [:strong.pr-1 e])
               (ui/icon "edit")]
+             :small? true
              :on-click #(state/set-sub-modal!
                          (fn [_] (plugins/user-proxy-settings-panel agent-opts))
                          {:id :https-proxy-panel :center? true})))
@@ -715,7 +716,7 @@
                       (not (file-sync-handler/synced-file-graph? current-repo)))
                  [:git "git" (t :settings-page/tab-version-control) (ui/icon "history" {:style {:font-size 20}})])
                [:advanced "advanced" (t :settings-page/tab-advanced) (ui/icon "bulb" {:style {:font-size 20}})]
-               [:features "features" (t :settings-page/tab-features) (ui/icon "app-feature" {:style {:font-size 20}
+               [:features "features" (t :settings-page/tab-features) (ui/icon "app-feature" {:style {:font-size 18}
                                                                                              :extension? true})]
                (when plugins-of-settings
                  [:plugins-setting "plugins" (t :settings-of-plugins) (ui/icon "puzzle")])]]

+ 12 - 19
src/main/frontend/components/settings.css

@@ -15,7 +15,7 @@
 
   &-inner {
     @apply flex flex-col md:flex-row;
-    
+
     > aside {
       border-right: 0 solid var(--ls-quaternary-background-color);
       border-bottom: 1px solid var(--ls-quaternary-background-color);
@@ -41,10 +41,8 @@
             color: var(--ls-primary-text-color);
 
             > i {
-              width: 20px;
-              height: 20px;
               overflow: hidden;
-              opacity: .6;
+              opacity: .9;
             }
 
             > strong {
@@ -290,31 +288,26 @@
   }
 }
 
-/* Styles for the category icon on the left of settings-modal */
-.cp__settings-inner > aside ul > li > a > i {
-  margin-right: 4px;
-}
-
 html.is-native-android,
 html.is-native-iphone,
 html.is-native-iphone-without-notch {
 
-    .cp__settings-inner {
-        > article {
-            padding-bottom: 0px;
-        }
+  .cp__settings-inner {
+    > article {
+      padding-bottom: 0;
+    }
 
-        .panel-wrap {
-            padding-bottom: 0px;
-        }
+    .panel-wrap {
+      padding-bottom: 0;
     }
+  }
 }
 
 svg.git {
-    margin-left: -4px;
-    transform: scale(0.9);
+  margin-left: -4px;
+  transform: scale(0.9);
 }
 
 svg.cmd {
-    margin-left: -1px;
+  margin-left: -1px;
 }

+ 23 - 22
src/main/frontend/components/sidebar.cljs

@@ -72,23 +72,23 @@
   [name icon recent?]
   (let [original-name (db-model/get-page-original-name name)
         whiteboard-page? (db-model/whiteboard-page? name)]
-    [:a {:on-click (fn [e]
-                     (let [name (util/safe-page-name-sanity-lc name)
-                           source-page (db-model/get-alias-source-page (state/get-current-repo) name)
-                           name (if (empty? source-page) name (:block/name source-page))]
-                       (if (and (gobj/get e "shiftKey") (not whiteboard-page?))
-                         (when-let [page-entity (if (empty? source-page) (db/entity [:block/name name]) source-page)]
-                           (state/sidebar-add-block!
-                            (state/get-current-repo)
-                            (:db/id page-entity)
-                            :page))
-                         (if whiteboard-page?
-                           (route-handler/redirect-to-whiteboard! name)
-                           (route-handler/redirect-to-page! name {:click-from-recent? recent?})))))}
-     [:span.page-icon (if whiteboard-page?
-                        [:span.tie.tie-whiteboard]
-                        icon)]
-     (pdf-assets/fix-local-asset-filename original-name)]))
+    [:a.flex.items-center
+     {:on-click
+      (fn [e]
+        (let [name        (util/safe-page-name-sanity-lc name)
+              source-page (db-model/get-alias-source-page (state/get-current-repo) name)
+              name        (if (empty? source-page) name (:block/name source-page))]
+          (if (and (gobj/get e "shiftKey") (not whiteboard-page?))
+            (when-let [page-entity (if (empty? source-page) (db/entity [:block/name name]) source-page)]
+              (state/sidebar-add-block!
+               (state/get-current-repo)
+               (:db/id page-entity)
+               :page))
+            (if whiteboard-page?
+              (route-handler/redirect-to-whiteboard! name)
+              (route-handler/redirect-to-page! name {:click-from-recent? recent?})))))}
+     [:span.page-icon (if whiteboard-page? [:span.tie.tie-whiteboard] icon)]
+     [:span.page-title (pdf-assets/fix-local-asset-filename original-name)]]))
 
 (defn get-page-icon [page-entity]
   (let [default-icon (ui/icon "page" {:extension? true})
@@ -132,8 +132,8 @@
   [t]
   (nav-content-item
    [:a.flex.items-center.text-sm.font-medium.rounded-md.wrap-th
-    (ui/icon "star mr-1" {:style {:font-size 16}})
-    [:span.flex-1.ml-1 (string/upper-case (t :left-side-bar/nav-favorites))]]
+    (ui/icon "star" {:size 16})
+    [:span.flex-1.ml-2 (string/upper-case (t :left-side-bar/nav-favorites))]]
 
    {:class "favorites"
     :edit-fn
@@ -156,8 +156,8 @@
   [t]
   (nav-content-item
    [:a.flex.items-center.text-sm.font-medium.rounded-md.wrap-th
-    (ui/icon "history mr-2" {:style {:font-size 16}})
-    [:span.flex-1
+    (ui/icon "history" {:size 16})
+    [:span.flex-1.ml-2
      (string/upper-case (t :left-side-bar/nav-recent-pages))]]
 
    {:class "recent"}
@@ -279,7 +279,7 @@
         [:div.fake-bar.absolute
          [:button
           {:on-click state/toggle-left-sidebar!}
-          (ui/icon "menu-2" {:style {:fontSize ui/icon-size}})]])
+          (ui/icon "menu-2" {:size ui/icon-size})]])
 
       [:nav.px-4.flex.flex-col.gap-1
        {:aria-label "Navigation menu"}
@@ -349,6 +349,7 @@
            [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md.new-page-link
             {:on-click #((close-sidebar-on-mobile!)
                          (state/pub-event! [:go/search]))}
+            ;; TODO: check following line
             (ui/icon "circle-plus mr-3" {:style {:font-size 20}})
             [:span.flex-1 (t :right-side-bar/new-page)]]))]]]))
 

+ 23 - 17
src/main/frontend/components/sidebar.css

@@ -45,12 +45,12 @@
 }
 
 #main-container {
-    /* Hack: (overflow-y) to fix sticky header not working */
-    /* To reproduce: quick creating blocks */
-    overflow-y: hidden;
-    position: relative;
-    height: 100%;
-    transition: padding-left .3s;
+  /* Hack: (overflow-y) to fix sticky header not working */
+  /* To reproduce: quick creating blocks */
+  overflow-y: hidden;
+  position: relative;
+  height: 100%;
+  transition: padding-left .3s;
 
   &.is-left-sidebar-open {
     padding-left: 0;
@@ -117,13 +117,14 @@
   }
 
   .page-icon {
-    @apply mr-1 align-baseline;
+    @apply flex items-center mr-1 align-baseline;
+
     width: 16px;
-    height: 16px;
+    height: 18px;
     text-align: center;
     display: inline-block;
     line-height: 1em;
-    color: var(--ls-icon-color);
+    opacity: .6;
   }
 
   a.item {
@@ -135,7 +136,7 @@
     > :is(.ti, .tie) {
       font-size: 16px;
       margin-right: 8px;
-      opacity: .6;
+      opacity: .9;
       position: relative;
     }
 
@@ -233,13 +234,21 @@
         a {
           width: 100%;
           padding: 2px 24px;
-          display: block;
-          text-overflow: ellipsis;
-          overflow: hidden;
-          white-space: nowrap;
           color: var(--ls-primary-text-color);
           transition: background-color .3s;
 
+          .page-title {
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            flex-grow: 1;
+          }
+
+          .page-icon {
+            display: flex;
+            align-items: center;
+          }
+
           &:hover {
             background-color: var(--ls-quaternary-background-color);
           }
@@ -409,9 +418,6 @@
 }
 
 .ls-left-sidebar-open {
-  .cp__header > .r {
-    display: none;
-  }
 
   @screen sm {
     .cp__header > .r {

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

@@ -66,7 +66,7 @@
     (->> (tf/parse (tf/formatters :date-time-no-ms) s)
         (t/to-default-time-zone)
         (tf/unparse (tf/formatter "MMM do, yyyy")))
-    (catch js/Error _e
+    (catch :default _e
       nil)))
 
 (def custom-formatter-2 (tf/formatter "yyyy-MM-dd-HH-mm-ss"))
@@ -112,7 +112,7 @@
 (defn journal-name-s [s]
   (try
     (journal-name (tf/parse (tf/formatter "yyyy-MM-dd") s))
-    (catch js/Error _e
+    (catch :default _e
       (log/info :parse-journal-date {:message  "Unable to parse date to journal name, skipping."
                                      :date-str s})
       nil)))
@@ -168,7 +168,7 @@
    (fn [formatter]
      (try
        (tf/parse (tf/formatter formatter) s)
-       (catch js/Error _e
+       (catch :default _e
          false)))
    (journal-title-formatters)))
 

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

@@ -160,7 +160,7 @@
           _ (swap! conns assoc db-name db-conn)
           _ (when stored
               (let [stored-db (try (string->db stored)
-                                   (catch js/Error _e
+                                   (catch :default _e
                                      (js/console.warn "Invalid graph cache")
                                      (d/empty-db db-schema/schema)))
                     attached-db (d/db-with stored-db

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

@@ -48,6 +48,7 @@
     :block/marker
     :block/priority
     :block/properties
+    :block/properties-text-values
     :block/pre-block?
     :block/scheduled
     :block/deadline

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

@@ -5,7 +5,7 @@
   [conn id]
   (try
     (d/pull @conn '[*] id)
-    (catch js/Error _e nil)))
+    (catch :default _e nil)))
 
 ;; key [:block/children parent-id]
 

+ 39 - 40
src/main/frontend/db/query_dsl.cljs

@@ -38,11 +38,7 @@
 ;; project (block, TBD)
 
 ;; Sort by (field, asc/desc):
-
-;; created_at
-;; last_modified_at
-
-;; (sort-by last_modified_at asc)
+;; (sort-by created-at asc)
 
 ;; (between -7d +7d)
 
@@ -151,13 +147,16 @@
 
       :else
       (->> clauses
-           (map (fn [result]
-                  (if (list? result)
-                    result
-                    (let [result (if (vector? (ffirst result))
-                                   (apply concat result)
-                                   result)]
-                      (cons 'and (seq result))))))
+           (mapcat (fn [result]
+                     (cond
+                       ;; rule like (task ?b #{"NOW"})
+                       (list? result)
+                       [result]
+                       ;; datalog clause like [[?b :block/uuid]]
+                       (vector? result)
+                       result
+                       :else
+                       [(cons 'and (seq result))])))
            (apply list fe)))
 
     :else
@@ -238,9 +237,14 @@
 (defn parse-property-value
   "Parses non-string property values or any page-ref like values"
   [v]
-  (if-some [res (text/parse-non-string-property-value v)]
-    res
-    (text/split-page-refs-without-brackets v)))
+  (let [result (if-some [res (text/parse-non-string-property-value v)]
+                 res
+                 (if (string/starts-with? v "#")
+                   (subs v 1)
+                   (or (page-ref/get-page-name v) v)))]
+    (if (string? result)
+      (string/trim result)
+      result)))
 
 (defn- build-property-two-arg
   [e]
@@ -320,33 +324,24 @@
   (when-let [num (second e)]
     (when (integer? num)
       (reset! sample num)
-      {:query [['?p :block/uuid]]})))
+      ;; blank b/c this post-process filter doesn't effect query
+      {})))
 
 (defn- build-sort-by
   [e sort-by_]
-  (let [[k order] (rest e)
-             order (if (and order (contains? #{:asc :desc}
-                                             (keyword (string/lower-case (name order)))))
-                     (keyword (string/lower-case (name order)))
-                     :desc)
-             k (-> (string/lower-case (name k))
-                   (string/replace "_" "-"))
-             get-value (cond
-                         (= k "created-at")
-                         :block/created-at
-
-                         (= k "updated-at")
-                         :block/updated-at
-
-                         :else
-                         #(get-in % [:block/properties k]))
-             comp (if (= order :desc) >= <=)]
-         (reset! sort-by_
-                 (fn [result]
-                   (->> result
-                        flatten
-                        (sort-by get-value comp))))
-         nil))
+  (let [[k order*] (map keyword (rest e))
+        order (if (contains? #{:asc :desc} order*)
+                order*
+                :desc)
+        comp (if (= order :desc) >= <=)]
+    (reset! sort-by_
+            (fn sort-results [result]
+              ;; first because there is one binding result in query-wrapper
+              (sort-by #(-> % first (get-in [:block/properties k]))
+                       comp
+                       result)))
+    ;; blank b/c this post-process filter doesn't effect query
+    {}))
 
 (defn- build-page
   [e]
@@ -392,7 +387,7 @@ Some bindings in this fn:
          page-ref? (page-ref/page-ref? e)]
      (when (or (and page-ref?
                     (not (contains? #{'page-property 'page-tags} (:current-filter env))))
-               (contains? #{'between 'property 'todo 'task 'priority 'sort-by 'page} fe)
+               (contains? #{'between 'property 'todo 'task 'priority 'page} fe)
                (and (not page-ref?) (string? e)))
        (reset! blocks? true))
      (cond
@@ -587,6 +582,10 @@ Some bindings in this fn:
                             (when sort-by
                               {:transform-fn sort-by})))))))
 
+(defn query-contains-filter?
+  [query filter-name]
+  (string/includes? query (str "(" filter-name)))
+
 (comment
   ;; {{query (and (page-property foo bar) [[hello]])}}
 

+ 2 - 2
src/main/frontend/db/query_react.cljs

@@ -72,12 +72,12 @@
         (if-let [f (sci/eval-string (pr-str result-transform))]
           (try
             (sci/call-fn f result)
-            (catch js/Error e
+            (catch :default e
               (log/error :sci/call-error e)
               result))
           result)
         result))
-    (catch js/Error e
+    (catch :default e
       (log/error :query/failed e))))
 
 (defn- resolve-query

+ 2 - 2
src/main/frontend/db/react.cljs

@@ -348,7 +348,7 @@
                        (if (and custom? (not custom-query?))
                          (async/put! (state/get-reactive-custom-queries-chan) [f query])
                          (f)))
-                     (catch js/Error e
+                     (catch :default e
                        (js/console.error e)))))))))))))
 
 (defn set-key-value
@@ -378,7 +378,7 @@
             (do
               (async/<! (async/timeout 2000))
               (async/put! chan [f query])))
-          (catch js/Error error
+          (catch :default error
             (let [type :custom-query/failed]
               (js/console.error (str type "\n" query))
               (js/console.error error)))))

+ 2 - 2
src/main/frontend/db/utils.cljs

@@ -66,7 +66,7 @@
        (d/pull db
                selector
                eid)
-       (catch js/Error _e
+       (catch :default _e
          nil)))))
 
 (defn pull-many
@@ -78,7 +78,7 @@
    (when-let [db (conn/get-db repo)]
      (try
        (d/pull-many db selector eids)
-       (catch js/Error e
+       (catch :default e
          (js/console.error e))))))
 
 (defn transact!

+ 21 - 1
src/main/frontend/dicts.cljc

@@ -2113,6 +2113,7 @@
            :help/key-commands "Nøkkelkommandoer"
            :help/working-with-lists " (arbeide med lister)"
            :help/select-nfs-browser " Vennligst bruk en annen nettleser (f.eks. siste Chrome) som støtter NFS for å åpne en lokal mappe."
+           :help/forum-community "Forum-samfunn"
            :undo "Angre"
            :redo "Gjør om"
            :general "Generell"
@@ -2142,6 +2143,8 @@
            :right-side-bar/all-pages "Alle sider"
            :right-side-bar/flashcards "Flashcards"
            :right-side-bar/new-page "Ny side"
+           :right-side-bar/separator "Høyre sidestolpe størrelsesendring"
+           :right-side-bar/show-journals "Vis dagbøker"
            :left-side-bar/journals "Dagbøker"
            :left-side-bar/new-page "Ny side"
            :left-side-bar/nav-favorites "Favoritter"
@@ -2252,6 +2255,12 @@
            :settings-page/tab-shortcuts "Snarveier"
            :settings-page/tab-version-control "Versjonskontroll"
            :settings-page/tab-advanced "Avansert"
+           :settings-page/custom-global-configuration "Egendefinert global konfigurasjon"
+           :settings-page/edit-global-config-edn "Rediger global config.edn"
+           :settings-page/enable-flashcards "Flashcards"
+           :settings-page/export-theme "Eksporter tema"
+           :settings-page/sync "Synkronisering"
+           :settings-page/tab-features "Funksjoner"
            :logseq "Logseq"
            :on "PÅ"
            :more-options "Flere valg"
@@ -2394,12 +2403,23 @@
            :graph/save "Lagrer..."
            :graph/save-error "Lagring feilet"
            :graph/save-success "Lagring vellykket"
+           :graph/all-graphs "Alle grafer"
+           :graph/local-graphs "Lokale grafer"
+           :graph/remote-graphs "Fjerngrafer"
            :page/copy-page-url "Kopier side URL"
            :page/open-backup-directory "Åpne mappe med sidens sikkerhetskopier"
            :plugin/not-installed "Ikke installert"
            :settings-page/edit-export-css "Rediger export.css"
            :settings-page/network-proxy "Nettverksproxy"
-           :settings-page/plugin-system "System for utvidelser"}
+           :settings-page/plugin-system "System for utvidelser"
+           :discourse-title "Vårt forum!"
+           :importing "Import"
+           :asset/copy "Kopier bilde"
+           :asset/delete "Slett bilde"
+           :asset/maximize "Maksimer bilde"
+           :asset/open-in-browser "Åpne bilde i nettleser"
+           :asset/show-in-folder "Vis bilde i mappe"
+           :linked-references/filter-search "Søk i lenkede referanser"}
 
    :pt-BR {:on-boarding/demo-graph "Esse é um grafo de demonstração, mudanças não serão salvas enquanto uma pasta local não for aberta."
            :on-boarding/add-graph "Adicionar grafo"

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

@@ -68,6 +68,6 @@
 
           :else
           pos))
-      (catch js/Error e
+      (catch :default e
         (log/error :diff/find-position {:error e})
         (count markup)))))

+ 1 - 1
src/main/frontend/extensions/excalidraw.cljs

@@ -26,7 +26,7 @@
   (when-not (string/blank? text)
     (try
       (js/JSON.parse text)
-      (catch js/Error e
+      (catch :default e
         (println "from json error:")
         (js/console.dir e)
         (notification/show!

+ 3 - 3
src/main/frontend/extensions/graph/pixi.cljs

@@ -176,13 +176,13 @@
           simulation                                                                (layout! nodes-js links-js)]
       (doseq [node nodes-js]
         (try (.addNode graph (.-id node) node)
-          (catch js/Error e
+          (catch :default e
             (js/console.error e))))
       (doseq [link links-js]
         (let [source (.-id (.-source link))
               target (.-id (.-target link))]
           (try (.addEdge graph source target link)
-            (catch js/Error e
+            (catch :default e
               (js/console.error e)))))
       (when-let [container-ref (:ref state)]
         (let [pixi-graph (new (.-PixiGraph Pixi-Graph)
@@ -199,6 +199,6 @@
             (register-handlers-fn pixi-graph))
           (set-up-listeners! pixi-graph)
           (.on simulation "tick" (tick! pixi-graph graph nodes-js links-js)))))
-    (catch js/Error e
+    (catch :default e
       (js/console.error e)))
   state)

+ 1 - 1
src/main/frontend/extensions/latex.cljs

@@ -24,7 +24,7 @@
                               :throwOnError false
                               :strict false}))
 
-      (catch js/Error e
+      (catch :default e
         (js/console.error e)))))
 
 (defn- load-and-render!

+ 2 - 2
src/main/frontend/extensions/pdf/utils.cljs

@@ -173,13 +173,13 @@
   []
   (try
     (js-invoke js/window.lsPdfViewer "nextPage")
-    (catch js/Error _e nil)))
+    (catch :default _e nil)))
 
 (defn prev-page
   []
   (try
     (js-invoke js/window.lsPdfViewer "previousPage")
-    (catch js/Error _e nil)))
+    (catch :default _e nil)))
 
 (comment
  (fix-selection-text-breakline "this is a\ntest paragraph")

+ 1 - 1
src/main/frontend/extensions/sci.cljs

@@ -17,7 +17,7 @@
                                    'isNaN js/isNaN
                                    'log js/console.log
                                    'pprint util/pp-str}})
-    (catch js/Error e
+    (catch :default e
       (println "Query: sci eval failed:")
       (js/console.error e))))
 

+ 1 - 1
src/main/frontend/extensions/srs.cljs

@@ -558,7 +558,7 @@
               count (count result)]
           (reset! cards-total count)
           count)))
-    (catch js/Error e
+    (catch :default e
       (js/console.error e) 0)))
 
 (declare cards)

+ 3 - 1
src/main/frontend/format/block.cljs

@@ -11,8 +11,9 @@
             [frontend.state :as state]
             [logseq.graph-parser.block :as gp-block]
             [logseq.graph-parser.config :as gp-config]
+            [logseq.graph-parser.property :as gp-property]
             [logseq.graph-parser.mldoc :as gp-mldoc]
-            [logseq.graph-parser.property :as gp-property]))
+            [lambdaisland.glogi :as log]))
 
 (defn extract-blocks
   "Wrapper around logseq.graph-parser.block/extract-blocks that adds in system state
@@ -27,6 +28,7 @@ and handles unexpected failure."
                               :db (db/get-db (state/get-current-repo))
                               :date-formatter (state/get-date-formatter)})
     (catch :default e
+      (log/error :exception e)
       (Sentry/captureException e)
       (notification/show! "An unexpected error occurred during block extraction." :error)
       [])))

+ 6 - 6
src/main/frontend/format/mldoc.cljs

@@ -2,7 +2,6 @@
   "Mldoc code needed by app but not graph-parser"
   (:require [clojure.string :as string]
             [frontend.format.protocol :as protocol]
-            [frontend.state :as state]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
             ["mldoc" :as mldoc :refer [Mldoc]]
@@ -40,20 +39,21 @@
                "Heading"} type))
 
 (defn opml->edn
-  [content]
+  [config content]
   (try
     (if (string/blank? content)
       {}
       (let [[headers blocks] (-> content (parse-opml) (gp-util/json->clj))]
-        [headers (gp-mldoc/collect-page-properties blocks gp-mldoc/parse-property (state/get-config))]))
-    (catch js/Error e
+        [headers (gp-mldoc/collect-page-properties blocks config)]))
+    (catch :default e
       (log/error :edn/convert-failed e)
       [])))
 
 (defn ->edn
-  "Wrapper around gp-mldoc/->edn which provides config state"
+  "Alias to gp-mldoc/->edn but could serve as a wrapper e.g. handle
+  gp-mldoc/default-config"
   [content config]
-  (gp-mldoc/->edn content config (state/get-config)))
+  (gp-mldoc/->edn content config))
 
 (defrecord MldocMode []
   protocol/Format

+ 356 - 264
src/main/frontend/fs/sync.cljs

@@ -27,7 +27,8 @@
             [frontend.encrypt :as encrypt]
             [medley.core :refer [dedupe-by]]
             [rum.core :as rum]
-            [promesa.core :as p]))
+            [promesa.core :as p]
+            [lambdaisland.glogi :as log]))
 
 ;;; ### Commentary
 ;; file-sync related local files/dirs:
@@ -143,6 +144,7 @@
 (s/def ::event #{:created-local-version-file
                  :finished-local->remote
                  :finished-remote->local
+                 :start
                  :pause
                  :resume
                  :exception-decrypt-failed
@@ -152,6 +154,10 @@
 
 (s/def ::sync-event (s/keys :req-un [::event ::data]))
 
+(defonce download-batch-size 100)
+(defonce upload-batch-size 20)
+(def ^:private current-sm-graph-uuid (atom nil))
+
 ;;; ### configs in config.edn
 ;; - :file-sync/ignore-files
 
@@ -178,13 +184,14 @@
 (def graphs-txid (persist-var/persist-var nil "graphs-txid"))
 
 (declare assert-local-txid<=remote-txid)
-(defn update-graphs-txid!
+(defn <update-graphs-txid!
   [latest-txid graph-uuid user-uuid repo]
   {:pre [(int? latest-txid) (>= latest-txid 0)]}
-  (persist-var/-reset-value! graphs-txid [user-uuid graph-uuid latest-txid] repo)
-  (p/let [_ (persist-var/persist-save graphs-txid)]
-    (state/pub-event! [:graph/refresh]))
-  (when (state/developer-mode?) (assert-local-txid<=remote-txid)))
+  (-> (p/let [_ (persist-var/-reset-value! graphs-txid [user-uuid graph-uuid latest-txid] repo)
+              _ (persist-var/persist-save graphs-txid)]
+        (state/pub-event! [:graph/refresh])
+        (when (state/developer-mode?) (assert-local-txid<=remote-txid)))
+      p->c))
 
 (defn clear-graphs-txid! [repo]
   (persist-var/-reset-value! graphs-txid nil repo)
@@ -592,10 +599,7 @@
   FileTxn
   (-checksum [this] (.-checksum this)))
 
-
-
-
-(defn- sort-file-metatdata-fn
+(defn- sort-file-metadata-fn
   ":recent-days-range > :favorite-pages > small-size pages > ...
   :recent-days-range : [<min-inst-ms> <max-inst-ms>]
 "
@@ -605,7 +609,8 @@
   (let [favorite-pages* (set favorite-pages)]
     (fn [^FileMetadata item]
       (let [path (relative-path item)
-            journal? (string/starts-with? path "journals/")
+            journal? (string/starts-with? path
+                                          (str (config/get-journals-directory) "/"))
             journal-day
             (when journal?
               (try
@@ -623,11 +628,18 @@
                    (second recent-days-range)))
           journal-day
 
+          (string/includes? path "logseq/")
+          9999
+
+          (string/includes? path "content.")
+          10000
+
           (contains? favorite-pages* path)
           (count path)
 
           :else
           (- (.-size item)))))))
+
 ;;; ### APIs
 ;; `RSAPI` call apis through rsapi package, supports operations on files
 
@@ -752,10 +764,8 @@
   (<update-local-files [this graph-uuid base-path filepaths]
     (println "update-local-files" graph-uuid base-path filepaths)
     (go
-      (let [token (<! (<get-token this))
-            r (<! (<retry-rsapi
-                   #(p->c (ipc/ipc "update-local-files" graph-uuid base-path filepaths token))))]
-        r)))
+      (let [token (<! (<get-token this))]
+        (<! (p->c (ipc/ipc "update-local-files" graph-uuid base-path filepaths token))))))
   (<download-version-files [this graph-uuid base-path filepaths]
     (go
       (let [token (<! (<get-token this))
@@ -810,13 +820,15 @@
     (set! graph-uuid' graph-uuid)
     (set! private-key secret-key)
     (set! public-key' public-key)
-    (p->c (.setEnv mobile-util/file-sync (clj->js {:env (if prod? "prod" "dev")
+    (p->c (.setEnv mobile-util/file-sync (clj->js {:graphUUID graph-uuid
+                                                   :env (if prod? "prod" "dev")
                                                    :secretKey secret-key
                                                    :publicKey public-key}))))
 
-  (<get-local-all-files-meta [_ _graph-uuid base-path]
+  (<get-local-all-files-meta [_ graph-uuid base-path]
     (go
-      (let [r (<! (p->c (.getLocalAllFilesMeta mobile-util/file-sync (clj->js {:basePath base-path}))))]
+      (let [r (<! (p->c (.getLocalAllFilesMeta mobile-util/file-sync (clj->js {:graphUUID graph-uuid
+                                                                               :basePath base-path}))))]
         (if (instance? ExceptionInfo r)
           r
           (->> (.-result r)
@@ -826,10 +838,11 @@
                                       (get metadata "encryptedFname") (get metadata "mtime") false nil)))
                set)))))
 
-  (<get-local-files-meta [_ _graph-uuid base-path filepaths]
+  (<get-local-files-meta [_ graph-uuid base-path filepaths]
     (go
       (let [r (<! (p->c (.getLocalFilesMeta mobile-util/file-sync
-                                            (clj->js {:basePath base-path
+                                            (clj->js {:graphUUID graph-uuid
+                                                      :basePath base-path
                                                       :filePaths filepaths}))))]
         (assert (not (instance? ExceptionInfo r)) "get-local-files-meta shouldn't return exception")
         (->> (.-result r)
@@ -839,21 +852,20 @@
                                     (get metadata "encryptedFname") (get metadata "mtime") false nil)))
              set))))
 
-  (<rename-local-file [_ _graph-uuid base-path from to]
+  (<rename-local-file [_ graph-uuid base-path from to]
     (p->c (.renameLocalFile mobile-util/file-sync
-                            (clj->js {:basePath base-path
+                            (clj->js {:graphUUID graph-uuid
+                                      :basePath base-path
                                       :from from
                                       :to to}))))
 
   (<update-local-files [this graph-uuid base-path filepaths]
     (go
-      (let [token (<! (<get-token this))
-            r (<! (<retry-rsapi
-                   #(p->c (.updateLocalFiles mobile-util/file-sync (clj->js {:graphUUID graph-uuid
-                                                                             :basePath base-path
-                                                                             :filePaths filepaths
-                                                                             :token token})))))]
-        r)))
+      (let [token (<! (<get-token this))]
+        (<! (p->c (.updateLocalFiles mobile-util/file-sync (clj->js {:graphUUID graph-uuid
+                                                                     :basePath base-path
+                                                                     :filePaths filepaths
+                                                                     :token token})))))))
 
   (<download-version-files [this graph-uuid base-path filepaths]
     (go
@@ -866,10 +878,11 @@
                                                                           :token token})))))]
         r)))
 
-  (<delete-local-files [_ _graph-uuid base-path filepaths]
+  (<delete-local-files [_ graph-uuid base-path filepaths]
     (go
       (let [r (<! (<retry-rsapi #(p->c (.deleteLocalFiles mobile-util/file-sync
-                                                          (clj->js {:basePath base-path
+                                                          (clj->js {:graphUUID graph-uuid
+                                                                    :basePath base-path
                                                                     :filePaths filepaths})))))]
         r)))
 
@@ -899,16 +912,18 @@
          r
          (get (js->clj r) "txid")))))
 
-  (<encrypt-fnames [_ _graph-uuid fnames]
+  (<encrypt-fnames [_ graph-uuid fnames]
     (go
       (let [r (<! (p->c (.encryptFnames mobile-util/file-sync
-                                        (clj->js {:filePaths fnames}))))]
+                                        (clj->js {:graphUUID graph-uuid
+                                                  :filePaths fnames}))))]
         (if (instance? ExceptionInfo r)
           (.-cause r)
           (get (js->clj r) "value")))))
-  (<decrypt-fnames [_ _graph-uuid fnames]
+  (<decrypt-fnames [_ graph-uuid fnames]
     (go (let [r (<! (p->c (.decryptFnames mobile-util/file-sync
-                                          (clj->js {:filePaths fnames}))))]
+                                          (clj->js {:graphUUID graph-uuid
+                                                    :filePaths fnames}))))]
           (if (instance? ExceptionInfo r)
             (ex-info "decrypt-failed" {:fnames fnames} (ex-cause r))
             (get (js->clj r) "value"))))))
@@ -1099,6 +1114,7 @@
                             (map
                              #(hash-map :checksum (:checksum %)
                                         :encrypted-path (remove-user-graph-uuid-prefix (:Key %))
+                                        :size (:Size %)
                                         :last-modified (:LastModified %))
                              objs))
                      (when-not (empty? next-continuation-token)
@@ -1113,7 +1129,7 @@
               (let [encrypted-path->path-map (zipmap encrypted-path-list* path-list-or-exp)]
                 (set
                  (mapv
-                  #(->FileMetadata nil
+                  #(->FileMetadata (:size %)
                                    (:checksum %)
                                    (get encrypted-path->path-map (:encrypted-path %))
                                    (:encrypted-path %)
@@ -1444,7 +1460,7 @@
               r)))))))
 
 (defn apply-filetxns-partitions
-  "won't call update-graphs-txid! when *txid is nil"
+  "won't call <update-graphs-txid! when *txid is nil"
   [*sync-state user-uuid graph-uuid base-path filetxns-partitions repo *txid *stopped *paused]
   (assert (some? *sync-state))
 
@@ -1476,7 +1492,7 @@
               ;; update local-txid
               (when *txid
                 (reset! *txid latest-txid)
-                (update-graphs-txid! latest-txid graph-uuid user-uuid repo))
+                (<! (<update-graphs-txid! latest-txid graph-uuid user-uuid repo)))
               (recur (next filetxns-partitions*)))))))))
 
 (defmulti need-sync-remote? (fn [v] (cond
@@ -1542,6 +1558,17 @@
            :path path
            :checksum checksum}))
 
+  ILookup
+  (-lookup [o k] (-lookup o k nil))
+  (-lookup [_ k not-found]
+    (case k
+      :type type
+      :dir  dir
+      :path path
+      :stat stat
+      :checksum checksum
+      not-found))
+
   IPrintWithWriter
   (-pr-writer [_ w _opts]
     (write-all w (str {:type type :base-path dir :path path :size (:size stat) :checksum checksum}))))
@@ -1947,7 +1974,9 @@
   {:post [(s/valid? ::sync-state %)]}
   {:current-syncing-graph-uuid  nil
    :state                       ::starting
+   :full-local->remote-files    #{}
    :current-local->remote-files #{}
+   :full-remote->local-files    #{}
    :current-remote->local-files #{}
    :queued-local->remote-files  #{}
    :recent-remote->local-files  #{}
@@ -2006,6 +2035,16 @@
   {:post [(s/valid? ::sync-state %)]}
   (update sync-state :recent-remote->local-files set/difference items))
 
+(defn sync-state-reset-full-local->remote-files
+  [sync-state events]
+  {:post [(s/valid? ::sync-state %)]}
+  (assoc sync-state :full-local->remote-files events))
+
+(defn sync-state-reset-full-remote->local-files
+  [sync-state events]
+  {:post [(s/valid? ::sync-state %)]}
+  (assoc sync-state :full-remote->local-files events))
+
 (defn- add-history-items
   [history paths now]
   (sequence
@@ -2072,20 +2111,26 @@
     (go
       (let [partitioned-filetxns
             (sequence (filepath+checksum-coll->partitioned-filetxns
-                       10 graph-uuid user-uuid)
+                       download-batch-size graph-uuid user-uuid)
                       relative-filepath+checksum-coll)
             r
             (if (empty? (flatten partitioned-filetxns))
               {:succ true}
-              (<! (apply-filetxns-partitions
-                   *sync-state user-uuid graph-uuid base-path partitioned-filetxns repo
-                   nil *stopped *paused)))]
+              (do
+                (put-sync-event! {:event :start
+                                  :data  {:type :full-remote->local
+                                          :graph-uuid graph-uuid
+                                          :full-sync? true
+                                          :epoch      (tc/to-epoch (t/now))}})
+                (<! (apply-filetxns-partitions
+                    *sync-state user-uuid graph-uuid base-path partitioned-filetxns repo
+                    nil *stopped *paused))))]
         (cond
           (instance? ExceptionInfo r) {:unknown r}
           @*stopped                   {:stop true}
           @*paused                    {:pause true}
           :else
-          (do (update-graphs-txid! latest-txid graph-uuid user-uuid repo)
+          (do (<! (<update-graphs-txid! latest-txid graph-uuid user-uuid repo))
               (reset! *txid latest-txid)
               {:succ true})))))
 
@@ -2103,12 +2148,17 @@
                         {:need-remote->local-full-sync true})
 
                     (when (pos-int? latest-txid)
-                      (let [partitioned-filetxns (transduce (diffs->partitioned-filetxns 10)
+                      (let [partitioned-filetxns (transduce (diffs->partitioned-filetxns download-batch-size)
                                                             (completing (fn [r i] (conj r (reverse i)))) ;reverse
                                                             '()
                                                             (reverse diff-txns))]
+                        (put-sync-event! {:event :start
+                                          :data  {:type :remote->local
+                                                  :graph-uuid graph-uuid
+                                                  :full-sync? false
+                                                  :epoch      (tc/to-epoch (t/now))}})
                         (if (empty? (flatten partitioned-filetxns))
-                          (do (update-graphs-txid! latest-txid graph-uuid user-uuid repo)
+                          (do (<! (<update-graphs-txid! latest-txid graph-uuid user-uuid repo))
                               (reset! *txid latest-txid)
                               {:succ true})
                           (<! (apply-filetxns-partitions
@@ -2140,9 +2190,10 @@
                 recent-10-days-range  ((juxt #(tc/to-long (t/minus % (t/days 10))) #(tc/to-long %)) (t/today))
                 sorted-diff-remote-files
                 (sort-by
-                 (sort-file-metatdata-fn :recent-days-range recent-10-days-range) > diff-remote-files)
+                 (sort-file-metadata-fn :recent-days-range recent-10-days-range) > diff-remote-files)
                 latest-txid           (:TXId (<! (<get-remote-graph remoteapi nil graph-uuid)))]
             (println "[full-sync(remote->local)]" (count sorted-diff-remote-files) "files need to sync")
+            (swap! *sync-state #(sync-state-reset-full-remote->local-files % sorted-diff-remote-files))
             (<! (.sync-files-remote->local!
                  this (map (juxt relative-path -checksum)
                            sorted-diff-remote-files)
@@ -2240,188 +2291,193 @@
                          ^:mutable rate *txid ^:mutable remote->local-syncer stop-chan *stopped *paused
                          ;; control chans
                          private-immediately-local->remote-chan private-recent-edited-chan]
-    Object
-    (filter-file-change-events-fn [_]
-      (fn [^FileChangeEvent e]
-        (go (and (instance? FileChangeEvent e)
-                 (if-let [mtime (:mtime (.-stat e))]
-                   ;; if mtime is not nil, it should be after (- now 1min)
-                   ;; ignore events too early
-                   (> (* 1000 mtime) (tc/to-long (t/minus (t/now) (t/minutes 1))))
-                   true)
-                 (or (string/starts-with? (.-dir e) base-path)
-                     (string/starts-with? (str "file://" (.-dir e)) base-path)) ; valid path prefix
-                 (not (ignored? e))     ;not ignored
-                 ;; download files will also trigger file-change-events, ignore them
-                 (let [r (not (contains? (:recent-remote->local-files @*sync-state)
-                                         (<! (<file-change-event=>recent-remote->local-file-item
-                                              graph-uuid e))))]
-                   (when (and (true? r)
-                              (seq (:recent-remote->local-files @*sync-state)))
-                     (println :debug (:recent-remote->local-files @*sync-state) e))
-                   r)))))
-
-    (set-remote->local-syncer! [_ s] (set! remote->local-syncer s))
-
-    ILocal->RemoteSync
-    (setup-local->remote! [_]
-      (async/tap immediately-local->remote-mult private-immediately-local->remote-chan)
-      (async/tap recent-edited-mult private-recent-edited-chan))
-
-    (stop-local->remote! [_]
-      (async/untap immediately-local->remote-mult private-immediately-local->remote-chan)
-      (async/untap recent-edited-mult private-recent-edited-chan)
-      (async/close! stop-chan)
-      (vreset! *stopped true))
-
-    (<ratelimit [this from-chan]
-      (let [<fast-filter-e-fn (.filter-file-change-events-fn this)]
-        (util/<ratelimit
-         from-chan rate
-         :filter-fn
-         (fn [e]
-           (go
-             (and (rsapi-ready? rsapi graph-uuid)
-                  (<! (<fast-filter-e-fn e))
-                  (do
-                    (swap! *sync-state sync-state--add-queued-local->remote-files e)
-                    (let [v (<! (<filter-local-changes-pred e base-path graph-uuid))]
-                      (when-not v
-                        (swap! *sync-state sync-state--remove-queued-local->remote-files e))
-                      v)))))
-         :flush-fn #(swap! *sync-state sync-state-reset-queued-local->remote-files)
-         :stop-ch stop-chan
-         :distinct-coll? true
-         :flush-now-ch private-immediately-local->remote-chan
-         :refresh-timeout-ch private-recent-edited-chan)))
-
-    (<sync-local->remote! [_ es]
-      (if (empty? es)
-        (go {:succ true})
-        (let [type         (.-type ^FileChangeEvent (first es))
-              es->paths-xf (comp
-                            (map #(relative-path %))
-                            (remove ignored?))]
-          (go
-            (let [es*   (<! (<filter-checksum-not-consistent graph-uuid es))
-                  _     (when (not= (count es*) (count es))
-                          (println :debug :filter-checksum-changed
-                                   (mapv relative-path (set/difference (set es) (set es*)))))
-                  es**  (filter-too-huge-files es*)
-                  _     (when (not= (count es**) (count es*))
-                          (println :debug :filter-too-huge-files
-                                   (mapv relative-path (set/difference (set es*) (set es**)))))
-                  paths (sequence es->paths-xf es**)
-                  _     (println :sync-local->remote type paths)
-                  r     (if (empty? paths)
-                          (go @*txid)
-                          (case type
-                            ("add" "change")
-                            (<with-pause (<update-remote-files rsapi graph-uuid base-path paths @*txid) *paused)
-
-                            "unlink"
-                            (<with-pause (<delete-remote-files rsapi graph-uuid base-path paths @*txid) *paused)))
-                  _               (swap! *sync-state sync-state--add-current-local->remote-files paths)
-                  r*              (<! r)
-                  [succ? paused?] ((juxt number? :pause) r*)
-                  _               (swap! *sync-state sync-state--remove-current-local->remote-files paths succ?)]
-              (cond
-                (need-sync-remote? r*)
-                (do (println :need-sync-remote r*)
-                    {:need-sync-remote true})
-
-                (need-reset-local-txid? r*) ;; TODO: this cond shouldn't be true,
-                ;; but some potential bugs cause local-txid > remote-txid
-                (let [remote-txid (:TXId (<! (<get-remote-graph remoteapi nil graph-uuid)))]
-                  (update-graphs-txid! remote-txid graph-uuid user-uuid repo)
-                  (reset! *txid remote-txid)
-                  {:succ true})
-
-                (graph-has-been-deleted? r*)
-                (do (println :graph-has-been-deleted r*)
-                    {:graph-has-been-deleted true})
-
-                paused?
-                {:pause true}
-
-                succ?                   ; succ
+  Object
+  (filter-file-change-events-fn [_]
+    (fn [^FileChangeEvent e]
+      (go (and (instance? FileChangeEvent e)
+               (if-let [mtime (:mtime (.-stat e))]
+                 ;; if mtime is not nil, it should be after (- now 1min)
+                 ;; ignore events too early
+                 (> (* 1000 mtime) (tc/to-long (t/minus (t/now) (t/minutes 1))))
+                 true)
+               (or (string/starts-with? (.-dir e) base-path)
+                   (string/starts-with? (str "file://" (.-dir e)) base-path)) ; valid path prefix
+               (not (ignored? e))     ;not ignored
+               ;; download files will also trigger file-change-events, ignore them
+               (not (contains? (:recent-remote->local-files @*sync-state)
+                               (<! (<file-change-event=>recent-remote->local-file-item
+                                    graph-uuid e))))))))
+
+  (set-remote->local-syncer! [_ s] (set! remote->local-syncer s))
+
+  ILocal->RemoteSync
+  (setup-local->remote! [_]
+    (async/tap immediately-local->remote-mult private-immediately-local->remote-chan)
+    (async/tap recent-edited-mult private-recent-edited-chan))
+
+  (stop-local->remote! [_]
+    (async/untap immediately-local->remote-mult private-immediately-local->remote-chan)
+    (async/untap recent-edited-mult private-recent-edited-chan)
+    (async/close! stop-chan)
+    (vreset! *stopped true))
+
+  (<ratelimit [this from-chan]
+    (let [<fast-filter-e-fn (.filter-file-change-events-fn this)]
+      (util/<ratelimit
+       from-chan rate
+       :filter-fn
+       (fn [e]
+         (go
+           (and (rsapi-ready? rsapi graph-uuid)
+                (<! (<fast-filter-e-fn e))
                 (do
-                  (println "sync-local->remote! update txid" r*)
-                  ;; persist txid
-                  (update-graphs-txid! r* graph-uuid user-uuid repo)
-                  (reset! *txid r*)
-                  {:succ true})
+                  (swap! *sync-state sync-state--add-queued-local->remote-files e)
+                  (let [v (<! (<filter-local-changes-pred e base-path graph-uuid))]
+                    (when-not v
+                      (swap! *sync-state sync-state--remove-queued-local->remote-files e))
+                    v)))))
+       :flush-fn #(swap! *sync-state sync-state-reset-queued-local->remote-files)
+       :stop-ch stop-chan
+       :distinct-coll? true
+       :flush-now-ch private-immediately-local->remote-chan
+       :refresh-timeout-ch private-recent-edited-chan)))
+
+  (<sync-local->remote! [_ es]
+    (if (empty? es)
+      (go {:succ true})
+      (let [type         (.-type ^FileChangeEvent (first es))
+            es->paths-xf (comp
+                          (map #(relative-path %))
+                          (remove ignored?))]
+        (go
+          (let [es*   (<! (<filter-checksum-not-consistent graph-uuid es))
+                _     (when (not= (count es*) (count es))
+                        (println :debug :filter-checksum-changed
+                                 (mapv relative-path (set/difference (set es) (set es*)))))
+                es**  (filter-too-huge-files es*)
+                _     (when (not= (count es**) (count es*))
+                        (println :debug :filter-too-huge-files
+                                 (mapv relative-path (set/difference (set es*) (set es**)))))
+                paths (sequence es->paths-xf es**)
+                _     (println :sync-local->remote type paths)
+                r     (if (empty? paths)
+                        (go @*txid)
+                        (case type
+                          ("add" "change")
+                          (<with-pause (<update-remote-files rsapi graph-uuid base-path paths @*txid) *paused)
+
+                          "unlink"
+                          (<with-pause (<delete-remote-files rsapi graph-uuid base-path paths @*txid) *paused)))
+                _               (swap! *sync-state sync-state--add-current-local->remote-files paths)
+                r*              (<! r)
+                [succ? paused?] ((juxt number? :pause) r*)
+                _               (swap! *sync-state sync-state--remove-current-local->remote-files paths succ?)]
+            (cond
+              (need-sync-remote? r*)
+              (do (println :need-sync-remote r*)
+                  {:need-sync-remote true})
+
+              (need-reset-local-txid? r*) ;; TODO: this cond shouldn't be true,
+              ;; but some potential bugs cause local-txid > remote-txid
+              (let [remote-txid (:TXId (<! (<get-remote-graph remoteapi nil graph-uuid)))]
+                (<! (<update-graphs-txid! remote-txid graph-uuid user-uuid repo))
+                (reset! *txid remote-txid)
+                {:succ true})
+
+              (graph-has-been-deleted? r*)
+              (do (println :graph-has-been-deleted r*)
+                  {:graph-has-been-deleted true})
+
+              paused?
+              {:pause true}
+
+              succ?                   ; succ
+              (do
+                (println "sync-local->remote! update txid" r*)
+                ;; persist txid
+                (<! (<update-graphs-txid! r* graph-uuid user-uuid repo))
+                (reset! *txid r*)
+                {:succ true})
 
-                :else
-                (do
-                  (println "sync-local->remote unknown:" r*)
-                  {:unknown r*})))))))
-
-    (<sync-local->remote-all-files! [this]
-      (go
-        (let [remote-all-files-meta-c      (<get-remote-all-files-meta remoteapi graph-uuid)
-              local-all-files-meta-c       (<get-local-all-files-meta rsapi graph-uuid base-path)
-              deletion-logs-c              (<get-deletion-logs remoteapi graph-uuid @*txid)
-              remote-all-files-meta-or-exp (<! remote-all-files-meta-c)
-              deletion-logs                (<! deletion-logs-c)]
-          (if (or (storage-exceed-limit? remote-all-files-meta-or-exp)
-                  (sync-stop-when-api-flying? remote-all-files-meta-or-exp)
-                  (decrypt-exp? remote-all-files-meta-or-exp))
-            (do (put-sync-event! {:event :exception-decrypt-failed
-                                  :data  {:graph-uuid graph-uuid
-                                          :exp        remote-all-files-meta-or-exp
-                                          :epoch      (tc/to-epoch (t/now))}})
-                {:stop true})
-            (let [remote-all-files-meta remote-all-files-meta-or-exp
-                  local-all-files-meta  (<! local-all-files-meta-c)
-                  {local-all-files-meta :keep delete-local-files :delete}
-                  (filter-local-files-in-deletion-logs local-all-files-meta deletion-logs)
-                  diff-local-files      (diff-file-metadata-sets local-all-files-meta remote-all-files-meta)
-                  change-events
-                  (sequence
-                   (comp
-                    ;; convert to FileChangeEvent
-                    (map #(->FileChangeEvent "change" base-path (.get-normalized-path ^FileMetadata %)
-                                             {:size (:size %)} (:etag %)))
-                    (remove ignored?))
-                   diff-local-files)
-                  change-events-partitions
-                  (sequence
-                   ;; partition FileChangeEvents
-                   (partition-file-change-events 10)
-                   (distinct-file-change-events change-events))]
-              (println "[full-sync(local->remote)]"
-                       (count (flatten change-events-partitions)) "files need to sync and"
-                       (count delete-local-files) "local files need to delete")
-              ;; 1. delete local files
-              (loop [[f & fs] delete-local-files]
-                (when f
-                  (let [relative-p (relative-path f)]
-                    (when-not (<! (<local-file-not-exist? graph-uuid rsapi base-path relative-p))
-                      (let [fake-recent-remote->local-file-item {:remote->local-type :delete
-                                                                 :checksum nil
-                                                                 :path relative-p}]
-                        (swap! *sync-state sync-state--add-recent-remote->local-files
-                               [fake-recent-remote->local-file-item])
-                        (<! (<delete-local-files rsapi graph-uuid base-path [(relative-path f)]))
-                        (go (<! (timeout 5000))
-                            (swap! *sync-state sync-state--remove-recent-remote->local-files
-                                   [fake-recent-remote->local-file-item])))))
-                  (recur fs)))
-
-              ;; 2. upload local files
-              (loop [es-partitions change-events-partitions]
-                (if @*stopped
-                  {:stop true}
-                  (if (empty? es-partitions)
-                    {:succ true}
-                    (let [{:keys [succ need-sync-remote graph-has-been-deleted unknown] :as r}
-                          (<! (<sync-local->remote! this (first es-partitions)))]
-                      (s/assert ::sync-local->remote!-result r)
-                      (cond
-                        succ
-                        (recur (next es-partitions))
-                        (or need-sync-remote graph-has-been-deleted unknown) r)))))))))))
+              :else
+              (do
+                (println "sync-local->remote unknown:" r*)
+                {:unknown r*})))))))
+
+  (<sync-local->remote-all-files! [this]
+    (go
+      (let [remote-all-files-meta-c      (<get-remote-all-files-meta remoteapi graph-uuid)
+            local-all-files-meta-c       (<get-local-all-files-meta rsapi graph-uuid base-path)
+            deletion-logs-c              (<get-deletion-logs remoteapi graph-uuid @*txid)
+            remote-all-files-meta-or-exp (<! remote-all-files-meta-c)
+            deletion-logs                (<! deletion-logs-c)]
+        (if (or (storage-exceed-limit? remote-all-files-meta-or-exp)
+                (sync-stop-when-api-flying? remote-all-files-meta-or-exp)
+                (decrypt-exp? remote-all-files-meta-or-exp))
+          (do (put-sync-event! {:event :exception-decrypt-failed
+                                :data  {:graph-uuid graph-uuid
+                                        :exp        remote-all-files-meta-or-exp
+                                        :epoch      (tc/to-epoch (t/now))}})
+              {:stop true})
+          (let [remote-all-files-meta remote-all-files-meta-or-exp
+                local-all-files-meta  (<! local-all-files-meta-c)
+                {local-all-files-meta :keep delete-local-files :delete}
+                (filter-local-files-in-deletion-logs local-all-files-meta deletion-logs)
+                recent-10-days-range  ((juxt #(tc/to-long (t/minus % (t/days 10))) #(tc/to-long %)) (t/today))
+                diff-local-files      (->> (diff-file-metadata-sets local-all-files-meta remote-all-files-meta)
+                                           (sort-by (sort-file-metadata-fn :recent-days-range recent-10-days-range) >))
+                change-events
+                (sequence
+                 (comp
+                  ;; convert to FileChangeEvent
+                  (map #(->FileChangeEvent "change" base-path (.get-normalized-path ^FileMetadata %)
+                                           {:size (:size %)} (:etag %)))
+                  (remove ignored?))
+                 diff-local-files)
+                distinct-change-events (distinct-file-change-events change-events)
+                _ (swap! *sync-state #(sync-state-reset-full-local->remote-files % distinct-change-events))
+                change-events-partitions
+                (sequence
+                 ;; partition FileChangeEvents
+                 (partition-file-change-events upload-batch-size)
+                 distinct-change-events)]
+            (println "[full-sync(local->remote)]"
+                     (count (flatten change-events-partitions)) "files need to sync and"
+                     (count delete-local-files) "local files need to delete")
+            (put-sync-event! {:event :start
+                              :data  {:type :full-local->remote
+                                      :graph-uuid graph-uuid
+                                      :full-sync? true
+                                      :epoch      (tc/to-epoch (t/now))}})
+            ;; 1. delete local files
+            (loop [[f & fs] delete-local-files]
+              (when f
+                (let [relative-p (relative-path f)]
+                  (when-not (<! (<local-file-not-exist? graph-uuid rsapi base-path relative-p))
+                    (let [fake-recent-remote->local-file-item {:remote->local-type :delete
+                                                               :checksum nil
+                                                               :path relative-p}]
+                      (swap! *sync-state sync-state--add-recent-remote->local-files
+                             [fake-recent-remote->local-file-item])
+                      (<! (<delete-local-files rsapi graph-uuid base-path [(relative-path f)]))
+                      (go (<! (timeout 5000))
+                          (swap! *sync-state sync-state--remove-recent-remote->local-files
+                                 [fake-recent-remote->local-file-item])))))
+                (recur fs)))
+
+            ;; 2. upload local files
+            (loop [es-partitions change-events-partitions]
+              (if @*stopped
+                {:stop true}
+                (if (empty? es-partitions)
+                  {:succ true}
+                  (let [{:keys [succ need-sync-remote graph-has-been-deleted unknown] :as r}
+                        (<! (<sync-local->remote! this (first es-partitions)))]
+                    (s/assert ::sync-local->remote!-result r)
+                    (cond
+                      succ
+                      (recur (next es-partitions))
+                      (or need-sync-remote graph-has-been-deleted unknown) r)))))))))))
 
 ;;; ### put all stuff together
 
@@ -2454,7 +2510,7 @@
         ::local->remote-full-sync
         (<! (.full-sync this))
         ::remote->local-full-sync
-        (<! (.remote->local-full-sync this nil))
+        (<! (.remote->local-full-sync this args))
         ::pause
         (<! (.pause this))
         ::stop
@@ -2541,8 +2597,8 @@
 
   (pause [this]
     (put-sync-event! {:event :pause
-                      :data {:graph-uuid graph-uuid
-                             :epoch (tc/to-epoch (t/now))}})
+                      :data  {:graph-uuid graph-uuid
+                              :epoch      (tc/to-epoch (t/now))}})
     (go-loop []
       (let [{:keys [resume]} (<! ops-chan)]
         (if resume
@@ -2563,9 +2619,9 @@
               ;; if resume-state = nil, try a remote->local to sync recent diffs
               (offer! private-remote->local-sync-chan true))
             (put-sync-event! {:event :resume
-                              :data {:graph-uuid graph-uuid
-                                     :resume-state resume-state
-                                     :epoch (tc/to-epoch (t/now))}})
+                              :data  {:graph-uuid   graph-uuid
+                                      :resume-state resume-state
+                                      :epoch        (tc/to-epoch (t/now))}})
             (<! (.schedule this ::idle nil :resume)))
           (recur)))))
 
@@ -2615,11 +2671,11 @@
           unknown
           (do
             (put-sync-event! {:event :local->remote-full-sync-failed
-                              :data {:graph-uuid graph-uuid
-                                     :epoch (tc/to-epoch (t/now))}})
+                              :data  {:graph-uuid graph-uuid
+                                      :epoch      (tc/to-epoch (t/now))}})
             (.schedule this ::idle nil nil))))))
 
-  (remote->local-full-sync [this _next-state]
+  (remote->local-full-sync [this _]
     (go
       (let [{:keys [succ unknown stop pause]}
             (<! (<sync-remote->local-all-files! remote->local-syncer))]
@@ -2638,9 +2694,11 @@
           unknown
           (do
             (put-sync-event! {:event :remote->local-full-sync-failed
-                              :data {:graph-uuid graph-uuid
-                                     :epoch (tc/to-epoch (t/now))}})
-            (.schedule this ::idle nil nil))))))
+                              :data  {:graph-uuid graph-uuid
+                                      :exp        unknown
+                                      :epoch      (tc/to-epoch (t/now))}})
+            ;; if any exception occurred, re-exec remote->local-full-sync
+            (.schedule this ::remote->local-full-sync nil nil))))))
 
   (remote->local [this _next-state {remote-val :remote}]
     (go
@@ -2678,8 +2736,14 @@
     (assert (some? local-changes) local-changes)
     (go
       (let [distincted-local-changes (distinct-file-change-events local-changes)
+            _ (swap! *sync-state #(sync-state-reset-full-local->remote-files % distincted-local-changes))
             change-events-partitions
-            (sequence (partition-file-change-events 10) distincted-local-changes)
+            (sequence (partition-file-change-events upload-batch-size) distincted-local-changes)
+            _ (put-sync-event! {:event :start
+                                :data  {:type       :local->remote
+                                        :graph-uuid graph-uuid
+                                        :full-sync? false
+                                        :epoch      (tc/to-epoch (t/now))}})
             {:keys [succ need-sync-remote graph-has-been-deleted unknown stop pause]}
             (loop [es-partitions change-events-partitions]
               (cond
@@ -2741,9 +2805,11 @@
         (debug/pprint ["stop sync-manager, graph-uuid" graph-uuid "base-path" base-path])
         (swap! *sync-state sync-state--update-state ::stop)
         (loop []
-          (when (not= ::stop state)
-            (<! (timeout 100))
-            (recur))))))
+          (if (not= ::stop state)
+            (do
+              (<! (timeout 100))
+              (recur))
+            (reset! current-sm-graph-uuid nil))))))
 
   IStopped?
   (-stopped? [_]
@@ -2769,21 +2835,25 @@
     (->SyncManager graph-uuid base-path *sync-state local->remote-syncer remote->local-syncer remoteapi-with-stop
                    nil *txid nil nil nil *stopped? *paused? nil (chan 1) (chan 1) (chan 1) (chan 1) (chan 1))))
 
-(def ^:private current-sm-graph-uuid (atom nil))
-
 (defn sync-manager-singleton
   [user-uuid graph-uuid base-path repo txid *sync-state]
   (when-not @current-sm-graph-uuid
     (reset! current-sm-graph-uuid graph-uuid)
     (sync-manager user-uuid graph-uuid base-path repo txid *sync-state)))
 
+(defn clear-graph-progress!
+  [graph-uuid]
+  (state/set-state! [:file-sync/progress graph-uuid] {}))
+
 (defn <sync-stop []
   (go
     (when-let [sm ^SyncManager (state/get-file-sync-manager)]
       (println "[SyncManager" (:graph-uuid sm) "]" "stopping")
       (<! (-stop! sm))
+
       (println "[SyncManager" (:graph-uuid sm) "]" "stopped")
-      (state/set-file-sync-manager nil))
+      (state/set-file-sync-manager nil)
+      (clear-graph-progress! (:graph-uuid sm)))
     (reset! current-sm-graph-uuid nil)))
 
 (defn sync-need-password!
@@ -2840,11 +2910,14 @@
 
 (declare network-online-cursor)
 
+;; Prevent starting of multiple sync managers
+(def *sync-starting? (atom {}))
 (defn sync-start []
   (let [*sync-state                 (atom (sync-state))
         current-user-uuid           (user/user-uuid)
         repo                        (state/get-current-repo)]
     (go
+      (<! (<sync-stop))
       (when (and (graph-sync-off? repo) @network-online-cursor)
         (<! (p->c (persist-var/-load graphs-txid)))
         (let [[user-uuid graph-uuid txid] @graphs-txid]
@@ -2852,25 +2925,35 @@
                      (user/logged-in?)
                      repo
                      (not (config/demo-graph? repo)))
-            (when-some [sm (sync-manager-singleton current-user-uuid graph-uuid
-                                                   (config/get-repo-dir repo) repo
-                                                   txid *sync-state)]
-              (when (check-graph-belong-to-current-user current-user-uuid user-uuid)
-                (if-not (<! (<check-remote-graph-exists graph-uuid)) ; remote graph has been deleted
-                  (clear-graphs-txid! repo)
-                  (do
-                    (state/set-file-sync-state repo @*sync-state)
-                    (state/set-file-sync-manager sm)
-
-                    ;; update global state when *sync-state changes
-                    (add-watch *sync-state ::update-global-state
-                               (fn [_ _ _ n]
-                                 (state/set-file-sync-state repo n)))
-
-                    (.start sm)
-
-                    (offer! remote->local-full-sync-chan true)
-                    (offer! full-sync-chan true)))))))))))
+            (try
+              (when-not (get @*sync-starting? graph-uuid)
+                (swap! *sync-starting? assoc graph-uuid true)
+                (clear-graph-progress! graph-uuid)
+
+                (when-some [sm (sync-manager-singleton current-user-uuid graph-uuid
+                                                       (config/get-repo-dir repo) repo
+                                                       txid *sync-state)]
+                  (when (check-graph-belong-to-current-user current-user-uuid user-uuid)
+                    (if-not (<! (<check-remote-graph-exists graph-uuid)) ; remote graph has been deleted
+                      (clear-graphs-txid! repo)
+                      (do
+                        (state/set-file-sync-state repo @*sync-state)
+                        (state/set-file-sync-manager sm)
+
+                        ;; update global state when *sync-state changes
+                        (add-watch *sync-state ::update-global-state
+                                   (fn [_ _ _ n]
+                                     (state/set-file-sync-state repo n)))
+
+                        (.start sm)
+
+                        (offer! remote->local-full-sync-chan true)
+                        (offer! full-sync-chan true)
+                        (swap! *sync-starting? assoc graph-uuid false))))))
+              (catch :default e
+                (prn "Sync start error: ")
+                (log/error :exception e)
+                (swap! *sync-starting? assoc graph-uuid false)))))))))
 
 ;;; ### some add-watches
 
@@ -2927,6 +3010,15 @@
   (<get-local-all-files-meta rsapi graph-uuid
                              (config/get-repo-dir (state/get-current-repo)))
   (def base-path (config/get-repo-dir (state/get-current-repo)))
+
+  ;; upload
+  (def full-upload-files (:full-local->remote-files (state/sub [:file-sync/sync-state (state/get-current-repo)])))
+
+  ;; queued
+  (:queued-local->remote-files (state/sub [:file-sync/sync-state (state/get-current-repo)]))
+
+  ;; download
+  (:current-remote->local-files (state/sub [:file-sync/sync-state (state/get-current-repo)]))
   )
 
 

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

@@ -24,7 +24,7 @@
     (doseq [block-id (block-ref/get-all-block-ref-ids content)]
       (when-let [block (try
                          (model/get-block-by-uuid block-id)
-                         (catch js/Error _e
+                         (catch :default _e
                            nil))]
         (let [id-property (:id (:block/properties block))]
           (when-not (= (str id-property) (str block-id))

+ 2 - 0
src/main/frontend/handler.cljs

@@ -26,6 +26,7 @@
             [frontend.handler.user :as user-handler]
             [frontend.handler.repo-config :as repo-config-handler]
             [frontend.handler.global-config :as global-config-handler]
+            [frontend.handler.metadata :as metadata-handler]
             [frontend.idb :as idb]
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.instrumentation.core :as instrument]
@@ -233,6 +234,7 @@
   (persist-var/load-vars)
   (user-handler/restore-tokens-from-localstorage)
   (user-handler/refresh-tokens-loop)
+  (metadata-handler/run-set-page-metadata-job!)
   (js/setTimeout instrument! (* 60 1000)))
 
 (defn stop! []

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

@@ -234,7 +234,7 @@
 
             :else
             nil))
-        (catch js/Error e
+        (catch :default e
           (js/console.error e))
         (finally
           (reset! *show-left-menu? false)

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

@@ -294,7 +294,7 @@
                           :else
                           content)]
         new-content)
-      (catch js/Error _e
+      (catch :default _e
         content))
     content))
 
@@ -634,8 +634,7 @@
 (defn properties-block
   [properties format page]
   (let [content (property/insert-properties format "" properties)
-        refs (gp-block/get-page-refs-from-properties format
-                                                     properties
+        refs (gp-block/get-page-refs-from-properties properties
                                                      (db/get-db (state/get-current-repo))
                                                      (state/get-date-formatter)
                                                      (state/get-config))]
@@ -1294,7 +1293,7 @@
                       (not= (string/trim db-content-without-heading)
                             (string/trim value)))
                  (save-block-aux! db-block value opts))))
-           (catch js/Error error
+           (catch :default error
              (log/error :save-block-failed error))))))))
 
 (defn- clean-content!
@@ -1611,7 +1610,7 @@
               @commands/*initial-commands)
          (and last-command
               (commands/get-matched-commands last-command)))))
-    (catch js/Error e
+    (catch :default e
       (js/console.error e)
       nil)))
 
@@ -1631,7 +1630,7 @@
               (commands/get-matched-commands
                last-command
                (commands/block-commands-map))))))
-    (catch js/Error _error
+    (catch :default _error
       nil)))
 
 (defn auto-complete?
@@ -2741,11 +2740,11 @@
             (autopair input-id "(" format nil))
 
         ;; If you type `xyz`, the last backtick should close the first and not add another autopair
-        ;; If you type several backticks in a row, each one should autopair to accommodate multiline code (```)        
+        ;; If you type several backticks in a row, each one should autopair to accommodate multiline code (```)
         (contains? (set (keys autopair-map)) key)
         (let [curr (get-current-input-char input)
                   prev (util/nth-safe value (dec pos))]
-            (util/stop e) 
+            (util/stop e)
             (if (and (= key "`") (= "`" curr) (not= "`" prev))
               (cursor/move-cursor-forward input)
               (autopair input-id key format nil)))

+ 1 - 1
src/main/frontend/handler/editor/lifecycle.cljs

@@ -20,7 +20,7 @@
 
     ;; try to close all opened dropdown menu
     (when-let [close-fns (vals (sub :modal/dropdowns))]
-      (try (doseq [f close-fns] (f)) (catch js/Error _e ())))
+      (try (doseq [f close-fns] (f)) (catch :default _e ())))
 
     (when-let [element (gdom/getElement id)]
       (.focus element)

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

@@ -155,14 +155,13 @@
           (repo-handler/persist-db! current-repo persist-db-noti-m)
           (repo-handler/broadcast-persist-db! graph))))
      (repo-handler/restore-and-setup-repo! graph)
-     (graph-switch graph))))
+     (graph-switch graph)
+     state/set-state! :sync-graph/init? false)))
 
 (defmethod handle :graph/switch [[_ graph opts]]
-  (if (or @outliner-file/*writes-finished?
-          (:graph/remote-binding? @state/state))
-    (do
-      (state/set-state! :graph/remote-binding? false)
-      (graph-switch-on-persisted graph opts))
+  (if (or (not (false? (get @outliner-file/*writes-finished? graph)))
+          (:sync-graph/init? @state/state))
+    (graph-switch-on-persisted graph opts)
     (notification/show!
      "Please wait seconds until all changes are saved for the current graph."
      :warning)))
@@ -671,9 +670,9 @@
                        {:content (str "The directory " dir " has been back, you can edit your graph now.")
                         :status :success
                         :clear? true}])
-    (state/update-state! :file/unlinked-dirs (fn [dirs] (disj dirs dir))))
-  (when (= dir (config/get-repo-dir repo))
-    (fs/watch-dir! dir)))
+    (state/update-state! :file/unlinked-dirs (fn [dirs] (disj dirs dir)))
+    (when (= dir (config/get-repo-dir repo))
+      (fs/watch-dir! dir))))
 
 (defmethod handle :file/alter [[_ repo path content]]
   (p/let [_ (file-handler/alter-file repo path content {:from-disk? true})]
@@ -686,7 +685,7 @@
       (let [payload (async/<! chan)]
         (try
           (handle payload)
-          (catch js/Error error
+          (catch :default error
             (let [type :handle-system-events/failed]
               (js/console.error (str type) (clj->js payload) "\n" error)
               (state/pub-event! [:instrument {:type    type

+ 5 - 3
src/main/frontend/handler/external.cljs

@@ -11,6 +11,7 @@
             [frontend.db :as db]
             [frontend.format.mldoc :as mldoc]
             [frontend.format.block :as block]
+            [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.date-time-util :as date-time-util]
             [frontend.handler.page :as page]
@@ -79,7 +80,8 @@
   [data finished-ok-handler]
   #_:clj-kondo/ignore
   (when-let [repo (state/get-current-repo)]
-    (let [[headers parsed-blocks] (mldoc/opml->edn data)
+    (let [config (gp-mldoc/default-config :markdown)
+          [headers parsed-blocks] (mldoc/opml->edn config data)
           parsed-blocks (->>
                          (block/extract-blocks parsed-blocks "" :markdown {})
                          (mapv editor/wrap-parse-block))
@@ -116,7 +118,7 @@
     (try (page/create! title {:redirect?  false
                               :format     page-format
                               :uuid       uuid})
-         (catch js/Error e
+         (catch :default e
            (notification/show! (str "Error happens when creating page " title ":\n"
                                     e
                                     "\nSkipped and continue the remaining import.") :error)))
@@ -128,7 +130,7 @@
                                        {:target-block first-child
                                         :sibling?     true
                                         :keep-uuid?   true})
-             (catch js/Error e
+             (catch :default e
                (notification/show! (str "Error happens when creating block content of page " title "\n"
                                         e
                                         "\nSkipped and continue the remaining import.") :error))))))

+ 52 - 14
src/main/frontend/handler/file_sync.cljs

@@ -10,7 +10,9 @@
             [frontend.handler.notification :as notification]
             [frontend.state :as state]
             [frontend.handler.user :as user]
-            [frontend.fs :as fs]))
+            [frontend.fs :as fs]
+            [cljs-time.coerce :as tc]
+            [cljs-time.core :as t]))
 
 (def *beta-unavailable? (volatile! false))
 
@@ -42,7 +44,7 @@
       (if (and (not (instance? ExceptionInfo r))
                (string? r))
         (let [tx-info [0 r (user/user-uuid) (state/get-current-repo)]]
-          (apply sync/update-graphs-txid! tx-info)
+          (<! (apply sync/<update-graphs-txid! tx-info))
           (swap! refresh-file-sync-component not) tx-info)
         (do
           (state/set-state! [:ui/loading? :graph/create-remote?] false)
@@ -90,10 +92,13 @@
   (state/set-state! :file-sync/remote-graphs {:loading false :graphs nil}))
 
 (defn init-graph [graph-uuid]
-  (let [repo (state/get-current-repo)
-        user-uuid (user/user-uuid)]
-    (sync/update-graphs-txid! 0 graph-uuid user-uuid repo)
-    (swap! refresh-file-sync-component not)))
+  (go
+    (let [repo (state/get-current-repo)
+          user-uuid (user/user-uuid)]
+      (state/set-state! :sync-graph/init? true)
+      (<! (sync/<update-graphs-txid! 0 graph-uuid user-uuid repo))
+      (swap! refresh-file-sync-component not)
+      (state/pub-event! [:graph/switch repo {:persist? false}]))))
 
 (defn download-version-file
   ([graph-uuid file-uuid version-uuid]
@@ -173,7 +178,6 @@
           " to "
           (config/get-string-repo-dir (config/get-local-dir local)))
      :warning)
-
     (init-graph (:GraphUUID graph))
     (state/close-modal!)))
 
@@ -181,20 +185,54 @@
   []
   (let [c     (async/chan 1)
         p     sync/sync-events-publication
-        topic :finished-local->remote]
-
-    (async/sub p topic c)
+        topics [:finished-local->remote :finished-remote->local :start]]
+    (doseq [topic topics]
+      (async/sub p topic c))
 
     (async/go-loop []
-      (let [{:keys [data]} (async/<! c)]
+      (let [{:keys [event data]} (async/<! c)]
+        (case event
+          (list :finished-local->remote :finished-remote->local)
+          (do
+            (sync/clear-graph-progress! (second @sync/graphs-txid))
+            (state/set-state! :file-sync/start {})
+            (state/set-state! [:file-sync/last-synced-at (state/get-current-repo)]
+                              (:epoch data)))
+
+          :start
+          (state/set-state! :file-sync/start data)
+
+          nil)
+
         (when (and (:file-change-events data)
                    (= :page (state/get-current-route)))
-          (state/pub-event!
-           [:file-sync/maybe-onboarding-show :sync-history])))
+          (state/pub-event! [:file-sync/maybe-onboarding-show :sync-history])))
       (recur))
 
-    #(async/unsub p topic c)))
+    #(doseq [topic topics]
+       (async/unsub p topic c))))
 
 (defn reset-user-state! []
   (vreset! *beta-unavailable? false)
   (state/set-state! :file-sync/onboarding-state nil))
+
+(defn calculate-time-left
+  "This assumes that the network speed is stable which could be wrong sometimes."
+  [sync-state progressing]
+  (let [start-time (get-in @state/state [:file-sync/start :epoch])
+        now (tc/to-epoch (t/now))
+        diff-seconds (- now start-time)
+        finished (reduce + (map (comp :progress second) progressing))
+        local->remote-files (:full-local->remote-files sync-state)
+        remote->local-files (:full-remote->local-files sync-state)
+        total (if (seq remote->local-files)
+                (reduce + (map (fn [m] (or (:size m) 0)) remote->local-files))
+                (reduce + (map #(:size (.-stat %)) local->remote-files)))
+        mins (int (/ (* (/ total finished) diff-seconds) 60))]
+    (if (or (zero? total) (zero? finished))
+      "waiting"
+      (cond
+        (zero? mins) "soon"
+        (= mins 1) "1 min left"
+        (> mins 30) "calculating..."
+        :else (str mins " mins left")))))

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

@@ -49,7 +49,7 @@
              (fn [error]
                (println "Can't read local image file: ")
                (js/console.dir error))))))
-      (catch js/Error _e
+      (catch :default _e
         nil))))
 
 (defn request-presigned-url

+ 10 - 1
src/main/frontend/handler/metadata.cljs

@@ -23,7 +23,7 @@
         (let [metadata-str (or file-content default-metadata-str)
               metadata (try
                          (reader/read-string metadata-str)
-                         (catch js/Error e
+                         (catch :default e
                            (println "Parsing metadata.edn failed: ")
                            (js/console.dir e)
                            {}))
@@ -84,3 +84,12 @@
 (defn update-properties!
   [properties-tx]
   (set-metadata! :block/properties #(handler-properties! % properties-tx)))
+
+(defn run-set-page-metadata-job!
+  []
+  (js/setInterval
+   (fn []
+     (when-let [repo (state/get-current-repo)]
+       (when (state/input-idle? repo :diff 3000)
+         (set-pages-metadata! repo))))
+   (* 1000 60 10)))

+ 2 - 3
src/main/frontend/handler/page.cljs

@@ -77,8 +77,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 format
-                                                      properties
+         refs (gp-block/get-page-refs-from-properties properties
                                                       (db/get-db (state/get-current-repo))
                                                       (state/get-date-formatter)
                                                       (state/get-config))]
@@ -700,7 +699,7 @@
   (let [properties (db/get-page-properties page-name)
         properties-str (get properties :filters "{}")]
     (try (reader/read-string properties-str)
-         (catch js/Error e
+         (catch :default e
            (log/error :syntax/filters e)))))
 
 (defn save-filter!

+ 4 - 4
src/main/frontend/handler/plugin.cljs

@@ -35,7 +35,7 @@
   [type & args]
   (try
     (apply js-invoke (aget js/window.logseq "api") type args)
-    (catch js/Error e (js/console.error e))))
+    (catch :default e (js/console.error e))))
 
 ;; state handlers
 (defonce central-endpoint "https://raw.githubusercontent.com/logseq/marketplace/master/")
@@ -153,7 +153,7 @@
   [id]
   (try
     (js/LSPluginCore.ensurePlugin (name id))
-    (catch js/Error _e
+    (catch :default _e
       nil)))
 
 (defn open-updates-downloading
@@ -431,7 +431,7 @@
                             matched)))
                       content)]
         (format/to-html content :markdown (gp-mldoc/default-config :markdown))))
-    (catch js/Error e
+    (catch :default e
       (log/error :parse-user-md-exception e)
       content)))
 
@@ -472,7 +472,7 @@
                    (bean/->js (normalize-keyword-for-json payload))
                    payload)
                  (if (keyword? plugin-id) (name plugin-id) plugin-id))
-      (catch js/Error e
+      (catch :default e
         (js/console.error "[Hook Plugin Err]" e)))))
 
 (defn hook-plugin-app

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

@@ -163,7 +163,7 @@
                            (remove nil?))]
              (when (seq metadata)
                (db/transact! repo metadata {:new-graph? true}))))))
-     (catch js/Error e
+     (catch :default e
        (log/error :exception e)))))
 
 (defn update-pages-metadata!
@@ -359,8 +359,9 @@
                         (when (= (state/get-current-repo) url)
                           (state/set-current-repo! (:url (first (state/get-repos)))))))]
     (when (or (config/local-db? url) (= url "local"))
-      (p/let [_ (idb/clear-local-db! url)] ; clear file handles
-        (delete-db-f)))))
+      (-> (p/let [_ (idb/clear-local-db! url)] ; clear file handles
+            )
+          (p/finally delete-db-f)))))
 
 (defn start-repo-db-if-not-exists!
   [repo]

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

@@ -166,7 +166,7 @@
                         (try
                           (js/eval scripts)
                           (execed)
-                          (catch js/Error e
+                          (catch :default e
                             (js/console.error "[custom js]" e)))))))))))))))
 
 (defn toggle-wide-mode!

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