Переглянути джерело

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

Peng Xiao 3 роки тому
батько
коміт
1e607907f3
45 змінених файлів з 670 додано та 577 видалено
  1. 4 0
      deps/db/src/logseq/db/schema.cljs
  2. 15 10
      deps/graph-parser/src/logseq/graph_parser/block.cljs
  3. 1 1
      deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs
  4. 14 3
      deps/graph-parser/src/logseq/graph_parser/util.cljs
  5. 29 0
      docs/accessibility.md
  6. 4 0
      docs/dev-practices.md
  7. 12 0
      e2e-tests/accessibility.spec.ts
  8. 4 4
      ios/App/App.xcodeproj/project.pbxproj
  9. 1 0
      package.json
  10. 1 1
      resources/css/common.css
  11. 240 213
      src/main/frontend/components/block.cljs
  12. 6 7
      src/main/frontend/components/export.cljs
  13. 0 80
      src/main/frontend/components/external.cljs
  14. 8 4
      src/main/frontend/components/header.cljs
  15. 1 1
      src/main/frontend/components/header.css
  16. 5 5
      src/main/frontend/components/onboarding.cljs
  17. 0 1
      src/main/frontend/components/onboarding/index.css
  18. 51 55
      src/main/frontend/components/onboarding/setups.cljs
  19. 2 2
      src/main/frontend/components/reference.cljs
  20. 2 1
      src/main/frontend/components/right_sidebar.cljs
  21. 14 7
      src/main/frontend/components/sidebar.cljs
  22. 15 0
      src/main/frontend/components/sidebar.css
  23. 9 5
      src/main/frontend/components/theme.cljs
  24. 1 0
      src/main/frontend/dicts.cljc
  25. 3 5
      src/main/frontend/fs/capacitor_fs.cljs
  26. 2 4
      src/main/frontend/fs/nfs.cljs
  27. 3 5
      src/main/frontend/fs/node.cljs
  28. 8 7
      src/main/frontend/handler.cljs
  29. 4 3
      src/main/frontend/handler/block.cljs
  30. 6 1
      src/main/frontend/handler/editor.cljs
  31. 1 0
      src/main/frontend/handler/events.cljs
  32. 17 6
      src/main/frontend/handler/export.cljs
  33. 54 27
      src/main/frontend/handler/external.cljs
  34. 20 44
      src/main/frontend/handler/file.cljs
  35. 8 3
      src/main/frontend/handler/page.cljs
  36. 0 6
      src/main/frontend/handler/ui.cljs
  37. 6 15
      src/main/frontend/modules/file/core.cljs
  38. 6 5
      src/main/frontend/modules/outliner/file.cljs
  39. 8 4
      src/main/frontend/modules/outliner/pipeline.cljs
  40. 7 14
      src/main/frontend/state.cljs
  41. 2 0
      src/main/frontend/ui.cljs
  42. 10 9
      src/main/frontend/util.cljc
  43. 36 13
      src/test/frontend/handler/page_test.cljs
  44. 18 6
      templates/config.edn
  45. 12 0
      yarn.lock

+ 4 - 0
deps/db/src/logseq/db/schema.cljs

@@ -119,6 +119,8 @@
     :block/heading-level
     :block/type
     :block/properties
+    :block/properties-order
+    :block/invalid-properties
     :block/created-at
     :block/updated-at
     :block/warning
@@ -135,5 +137,7 @@
     :block/format
     :block/content
     :block/properties
+    :block/properties-order
+    :block/invalid-properties
     :block/alias
     :block/tags})

+ 15 - 10
deps/graph-parser/src/logseq/graph_parser/block.cljs

@@ -199,22 +199,19 @@
          (remove string/blank?)
          distinct)))
 
-(defn- invalid-property-key?
-  [s]
-  (string/includes? s "`"))
-
 (defn extract-properties
   [format properties user-config]
   (when (seq properties)
     (let [properties (seq properties)
           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))
                                              (string/replace " " "-")
                                              (string/replace "_" "-")
                                              (string/replace #"[\"|^|(|)|{|}]+" ""))]
-                                   (when-not (invalid-property-key? k)
+                                   (if (gp-util/valid-edn-keyword? (str ":" k))
                                      (let [k (if (contains? #{"custom_id" "custom-id"} k)
                                                "id"
                                                k)
@@ -235,10 +232,14 @@
                                                (set [v])
                                                v)
                                            v (if (coll? v) (set v) v)]
-                                       [k v])))))
-                          (remove #(nil? (second %))))]
-      {:properties (into {} properties)
+                                       [k v])
+                                     (do (swap! *invalid-properties conj k)
+                                         nil)))))
+                          (remove #(nil? (second %))))
+          properties' (into {} properties)]
+      {:properties properties'
        :properties-order (map first properties)
+       :invalid-properties @*invalid-properties
        :page-refs page-refs})))
 
 (defn- paragraph-timestamp-block?
@@ -533,7 +534,7 @@
                  (cons
                   (merge
                    (let [content (utf8/substring encoded-content 0 first-block-start-pos)
-                         {:keys [properties properties-order]} pre-block-properties
+                         {:keys [properties properties-order 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)
                                             (map :block/original-name))
@@ -542,6 +543,7 @@
                                 :level 1
                                 :properties properties
                                 :properties-order (vec properties-order)
+                                :invalid-properties invalid-properties
                                 :refs property-refs
                                 :pre-block? true
                                 :unordered true
@@ -578,7 +580,10 @@
                 (assoc :properties (:properties properties))
 
                 (seq (:properties-order properties))
-                (assoc :properties-order (vec (:properties-order properties))))
+                (assoc :properties-order (vec (:properties-order properties)))
+
+                (seq (:invalid-properties properties))
+                (assoc :invalid-properties (:invalid-properties properties)))
         block (if (get-in block [:properties :collapsed])
                 (-> (assoc block :collapsed? true)
                     (update :properties (fn [m] (dissoc m :collapsed)))

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

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

@@ -1,10 +1,11 @@
 (ns logseq.graph-parser.util
   "Util fns shared between graph-parser and rest of app. Util fns only rely on
   clojure standard libraries."
-  (:require [clojure.walk :as walk]
+  (:require [cljs.reader :as reader]
+            [clojure.edn :as edn]
             [clojure.string :as string]
-            [logseq.graph-parser.log :as log]
-            [cljs.reader :as reader]))
+            [clojure.walk :as walk]
+            [logseq.graph-parser.log :as log]))
 
 (defn path-normalize
   "Normalize file path (for reading paths from FS, not required by writting)"
@@ -150,6 +151,16 @@
   (when file
     (normalize-format (keyword (string/lower-case (last (string/split file #"\.")))))))
 
+(defn valid-edn-keyword?
+  [k]
+  (try
+    (let [s (str k)]
+      (and (= \: (first s))
+           (edn/read-string (str "{" s " nil}"))))
+    true
+    (catch :default _
+      false)))
+
 (defn safe-read-string
   [content]
   (try

+ 29 - 0
docs/accessibility.md

@@ -0,0 +1,29 @@
+- Accessibility is a vague term, which is why it is usually misunderstood. It is not just about people with with specific disabilities. You can read more about [what is accessibility](https://developer.mozilla.org/en-US/docs/Learn/Accessibility/What_is_accessibility#so_what_is_accessibility) and [diverse abilities and barriers](https://www.w3.org/WAI/people-use-web/abilities-barriers/).
+- ## Web Content Accessibility Guidelines
+	- [WCAG](https://www.w3.org/WAI/standards-guidelines/wcag/) (Web Content Accessibility Guidelines) is the international standard for web content accessibility, developed by [W3C](https://www.w3.org/). Logseq is a web application, so conforming with WCAG should be our first priority. In general, there is no simple way to determine if a website is accessible or not, but WCAG can help us make the tool usable by as many people as possible.
+- ## Levels of conformance
+	- There are three levels of conformance defined by WCAG
+		- Level **A** is the minimum level.
+		- Level **AA** includes all Level A and AA requirements.
+		- Level **AAA** includes all Level A, AA, and AAA requirements.
+	- Many organizations strive to meet Level AA. The reason behind this decision, is that in some cases AAA standard is too strict. That does't mean that triple-A issues should be disregarded. On the contrary, all of them should be handled if possible.
+	- We can also provide alternative options in order to conform with AAA standards. For instance, our default themes can aim for AA, but we can provide a high-contrast theme that aims for AAA. Providing [alternative versions](https://www.w3.org/WAI/GL/2007/05/alternate-versions.html) with different levels of conformance is permitted according to WCAG, if there is an accessible way to reach those alternatives.
+- ## Simple development guidelines
+	- Use semantically correct markup whenever possible. Every time you are about to decide which html tag you are going to use, choose the one that behaves the way you want it. For example, let's say you want to create an element that looks like plain text, but triggers an action on click. Usually, the best approach would be to create a `<button>` and make it look like a `<span>` using css. If you use a `span`, you will also have to override other html attributes like `tabindex` and `role` to make the element behave like a button. This is almost always a bad sign, and should be avoided. If you use the appropriate html element, the browser will be able to properly handle it.
+	- Do not skip headings. People who use screen readers and a keyboard to navigate through the app, use the headings structure to quickly jump to areas of interest. Skipping headings to visually conform with the design, makes this hard for them. If you want to create a heading tha looks like an `<h4>` but is in terms of document structure an `<h2>`, use the latter and make it look like an `<h4>`.
+	- A more [in-depth guide about HTML and accessibility](https://developer.mozilla.org/en-US/docs/Learn/Accessibility/HTML).
+- ## Advanced concepts
+	- Focus management is extremely important for [keyboard navigation](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_WCAG/Keyboard). Focusable elements can help people with motor disabilities navigate. [Focus Order](https://www.w3.org/TR/UNDERSTANDING-WCAG20/navigation-mechanisms-focus-order.html) plays an important role in this.
+- ## Automated testing
+	- There is a [huge list of tools](https://www.w3.org/WAI/ER/tools/) that can help us test our application. Most of them use [axe-core](https://github.com/dequelabs/axe-core) internally. There are [browser extensions](https://www.deque.com/axe/browser-extensions/) based on axe, a [VSCode Linter Plugin](https://marketplace.visualstudio.com/items?itemName=deque-systems.vscode-axe-linter) and also [multiple community projects](https://github.com/dequelabs/axe-core/blob/develop/doc/projects.md#community-projects).
+	- Basic accessibility testing could be integrated into our CI, by using the appropriate axe package (e.g. [@axe-core/playwright](https://github.com/dequelabs/axe-core-npm/blob/develop/packages/playwright/README.md))
+- ## Manual testing
+	- In theory, all of the cases described by WCAG should be testable. In practice, there are issues that can't be replicated by code. Also, manual accessibility testing would help us have a better understanding of the difficulties that certain people might encounter. Even if the all the individual cases pass the tests, the overall navigation might be nonsensical.
+	- ### Manual accessibility testing musts
+		- Keyboard-only navigation
+		- Screen reader testing and compatibility
+		- Zooming up to 200%
+		- Manually testing contrast issues that can't be automated
+	- ### Screen readers
+		- Apple and Android mobile devices have build-in screen readers.
+		- For desktop, [NVDA](https://help.gnome.org/users/orca/stable/index.html.en) is the most popular choice. For linux, [Orca](https://help.gnome.org/users/orca/stable/index.html.en) is a good option.

+ 4 - 0
docs/dev-practices.md

@@ -130,6 +130,10 @@ To write a test that uses a datascript db:
 
 For examples of these tests, see `frontend.db.query-dsl-test` and `frontend.db.model-test`.
 
+## Accessibility
+
+Please refer to our [accessibility guidelines](accessibility.md).
+
 ## Logging
 
 For logging, we use https://github.com/lambdaisland/glogi. When in development,

+ 12 - 0
e2e-tests/accessibility.spec.ts

@@ -0,0 +1,12 @@
+import { injectAxe, checkA11y, getViolations, reportViolations } from 'axe-playwright'
+import { test } from './fixtures'
+import { createRandomPage } from './utils'
+
+
+test('check a11y for the whole page', async ({ page }) => {
+    await injectAxe(page)
+    await createRandomPage(page)
+    await checkA11y(page, null, {
+        detailedReport: true,
+    })
+})

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

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

+ 1 - 0
package.json

@@ -8,6 +8,7 @@
         "@playwright/test": "^1.24.2",
         "@tailwindcss/ui": "0.7.2",
         "@types/gulp": "^4.0.7",
+        "axe-playwright": "^1.1.11",
         "cross-env": "^7.0.3",
         "cssnano": "^4.1.10",
         "del": "^6.0.0",

+ 1 - 1
resources/css/common.css

@@ -1040,7 +1040,7 @@ hr {
 
 .cp__header-logo,
 .fade-link {
-  opacity: 0.6;
+  opacity: 0.8;
   transition: 0.3s;
   color: var(--ls-primary-text-color);
 }

+ 240 - 213
src/main/frontend/components/block.cljs

@@ -71,6 +71,7 @@
             [promesa.core :as p]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]
+            [clojure.set :as set]
             [shadow.loader :as loader]))
 
 (defn safe-read-string
@@ -237,7 +238,7 @@
                                                   (editor-handler/delete-asset-of-block!
                                                    {:block-id    block-id
                                                     :local?      local?
-                                                    :delete-local? (first sub-selected)
+                                                    :delete-local? (and sub-selected (first sub-selected))
                                                     :repo        (state/get-current-repo)
                                                     :href        src
                                                     :title       title
@@ -424,6 +425,27 @@
 
 (declare page-reference)
 
+(defn open-page-ref
+  [e page-name redirect-page-name page-name-in-block contents-page?]
+  (util/stop e)
+  (cond
+    (gobj/get e "shiftKey")
+    (when-let [page-entity (db/entity [:block/name redirect-page-name])]
+      (state/sidebar-add-block!
+       (state/get-current-repo)
+       (:db/id page-entity)
+       :page))
+
+    (not= redirect-page-name page-name)
+    (route-handler/redirect-to-page! redirect-page-name)
+
+    :else
+    (state/pub-event! [:page/create page-name-in-block]))
+  (when (and contents-page?
+             (util/mobile?)
+             (state/get-left-sidebar-open?))
+    (ui-handler/close-left-sidebar!)))
+
 (rum/defc page-inner
   "The inner div of page reference component
 
@@ -1861,41 +1883,53 @@
        :else
        (inline-text config (:block/format block) (str v)))]))
 
+(def hidden-editable-page-properties
+  "Properties that are hidden in the pre-block (page property)"
+  #{:title :filters :icon})
+
+(assert (set/subset? hidden-editable-page-properties (gp-property/editable-built-in-properties))
+        "Hidden editable page properties must be valid editable properties")
+
+(defn- add-aliases-to-properties
+  [properties block]
+  (let [repo (state/get-current-repo)
+        aliases (db/get-page-alias-names repo
+                                         (:block/name (db/pull (:db/id (:block/page block)))))]
+    (if (seq aliases)
+      (if (:alias properties)
+        (update properties :alias (fn [c]
+                                    (util/distinct-by string/lower-case (concat c aliases))))
+        (assoc properties :alias aliases))
+      properties)))
+
 (rum/defc properties-cp
-  [config block]
-  (let [properties (walk/keywordize-keys (:block/properties block))
-        properties-order (:block/properties-order block)
-        properties (apply dissoc properties (property/hidden-properties))
-        properties-order (remove (property/hidden-properties) properties-order)
-        pre-block? (:block/pre-block? block)
-        properties (if pre-block?
-                     (let [repo (state/get-current-repo)
-                           properties (dissoc properties :title :filters)
-                           aliases (db/get-page-alias-names repo
-                                                            (:block/name (db/pull (:db/id (:block/page block)))))]
-                       (if (seq aliases)
-                         (if (:alias properties)
-                           (update properties :alias (fn [c]
-                                                       (util/distinct-by string/lower-case (concat c aliases))))
-                           (assoc properties :alias aliases))
-                         properties))
-                     properties)
-        properties-order (if pre-block?
-                           (remove #{:title :filters} properties-order)
-                           properties-order)
-        properties (if (seq properties-order)
-                     (map (fn [k] [k (get properties k)]) properties-order)
-                     properties)]
+  [config {:block/keys [pre-block?] :as block}]
+  (let [dissoc-keys (fn [m keys] (apply dissoc m keys))
+        properties (cond-> (update-keys (:block/properties block) keyword)
+                           true
+                           (dissoc-keys (property/hidden-properties))
+                           pre-block?
+                           (dissoc-keys hidden-editable-page-properties)
+                           pre-block?
+                           (add-aliases-to-properties block))]
     (cond
       (seq properties)
-      [:div.block-properties
-       {:class (when pre-block? "page-properties")
-        :title (if pre-block?
-                 "Click to edit this page's properties"
-                 "Click to edit this block's properties")}
-       (for [[k v] properties]
-         (rum/with-key (property-cp config block k v)
-           (str (:block/uuid block) "-" k)))]
+      (let [properties-order (cond->> (:block/properties-order block)
+                                      true
+                                      (remove (property/hidden-properties))
+                                      pre-block?
+                                      (remove hidden-editable-page-properties))
+            ordered-properties (if (seq properties-order)
+                                 (map (fn [k] [k (get properties k)]) properties-order)
+                                 properties)]
+        [:div.block-properties
+         {:class (when pre-block? "page-properties")
+          :title (if pre-block?
+                   "Click to edit this page's properties"
+                   "Click to edit this block's properties")}
+         (for [[k v] ordered-properties]
+           (rum/with-key (property-cp config block k v)
+             (str (:block/uuid block) "-" k)))])
 
       (and pre-block? properties)
       [:span.opacity-50 "Properties"]
@@ -1903,6 +1937,16 @@
       :else
       nil)))
 
+(rum/defc invalid-properties-cp
+  [invalid-properties]
+  (when (seq invalid-properties)
+    [:div.invalid-properties.mb-2
+     [:div.warning {:title "Invalid properties"}
+      "Invalid property keys: "
+      (for [p invalid-properties]
+        [:button.p-1.mr-2 p])]
+     [:code "Property key begins with a non-numeric character and can contain alphanumeric characters and . * + ! - _ ? $ % & = < >. If -, + or . are the first character, the second character (if any) must be non-numeric."]]))
+
 (rum/defcs timestamp-cp < rum/reactive
   (rum/local false ::show?)
   (rum/local {} ::pos)
@@ -2109,6 +2153,9 @@
         (when-let [scheduled-ast (block-handler/get-scheduled-ast block)]
           (timestamp-cp block "SCHEDULED" scheduled-ast)))
 
+      (when-let [invalid-properties (:block/invalid-properties block)]
+        (invalid-properties-cp invalid-properties))
+
       (when (and (seq properties)
                  (let [hidden? (property/properties-hidden? properties)]
                    (not hidden?))
@@ -3067,192 +3114,172 @@
 
 (defn ^:large-vars/cleanup-todo markup-element-cp
   [{:keys [html-export?] :as config} item]
-  (let [format (or (:block/format config)
-                   :markdown)]
-    (try
-      (match item
-        ["Drawer" name lines]
-        (when (or (not= name "logbook")
-                  (and
-                   (= name "logbook")
-                   (state/enable-timetracking?)
-                   (or  (get-in (state/get-config) [:logbook/settings :enabled-in-all-blocks])
-                        (when (get-in (state/get-config)
-                                      [:logbook/settings :enabled-in-timestamped-blocks] true)
-                          (or (:block/scheduled (:block config))
-                              (:block/deadline (:block config)))))))
-          [:div
-           [:div.text-sm
-            [:div.drawer {:data-drawer-name name}
-             (ui/foldable
-              [:div.opacity-50.font-medium.logbook
-               (util/format ":%s:" (string/upper-case name))]
-              [:div.opacity-50.font-medium
-               (if (= name "logbook")
-                 (logbook-cp lines)
-                 (apply str lines))
-               [:div ":END:"]]
-              {:default-collapsed? true
-               :title-trigger? true})]]])
-
-        ["Properties" m]
-        [:div.properties
-         (for [[k v] (dissoc m :roam_alias :roam_tags)]
-           (when (and (not (and (= k :macros) (empty? v))) ; empty macros
-                      (not (= k :title))
-                      (not (= k :filters)))
-             [:div.property
-              [:span.font-medium.mr-1 (str (name k) ": ")]
-              (if (coll? v)
-                (let [vals (for [item v]
-                             (if (coll? v)
-                               (let [config (when (= k :alias)
-                                              (assoc config :block/alias? true))]
-                                 (page-cp config {:block/name item}))
-                               (inline-text format item)))]
-                  (interpose [:span ", "] vals))
-                (inline-text format v))]))]
-
-             ;; for file-level property in orgmode: #+key: value
-             ;; only display caption. https://orgmode.org/manual/Captions.html.
-        ["Directive" key value]
-        [:div.file-level-property
-         (when (contains? #{"caption"} (string/lower-case key))
-           [:span.font-medium
-            [:span.font-bold (string/upper-case key)]
-            (str ": " value)])]
-
-        ["Paragraph" l]
-             ;; TODO: speedup
-        (if (util/safe-re-find #"\"Export_Snippet\" \"embed\"" (str l))
-          (->elem :div (map-inline config l))
-          (->elem :div.is-paragraph (map-inline config l)))
-
-        ["Horizontal_Rule"]
-        (when-not (:slide? config)
-          [:hr])
-        ["Heading" h]
-        (block-container config h)
-        ["List" l]
-        (let [lists (divide-lists l)]
-          (if (= 1 (count lists))
-            (let [l (first lists)]
-              (->elem
-               (list-element l)
-               (map #(list-item config %) l)))
-            [:div.list-group
-             (for [l lists]
-               (->elem
-                (list-element l)
-                (map #(list-item config %) l)))]))
-        ["Table" t]
-        (table config t)
-        ["Math" s]
-        (if html-export?
-          (latex/html-export s true true)
-          (latex/latex (str (d/squuid)) s true true))
-        ["Example" l]
-        [:pre.pre-wrap-white-space
-         (join-lines l)]
-        ["Quote" l]
-        (->elem
-         :blockquote
-         (markup-elements-cp config l))
-        ["Raw_Html" content]
-        (when (not html-export?)
-          [:div.raw_html {:dangerouslySetInnerHTML
-                          {:__html content}}])
-        ["Export" "html" _options content]
-        (when (not html-export?)
-          [:div.export_html {:dangerouslySetInnerHTML
-                             {:__html content}}])
-        ["Hiccup" content]
-        (ui/catch-error
-         [:div.warning {:title "Invalid hiccup"}
-          content]
-         (-> (safe-read-string content)
-             (security/remove-javascript-links-in-href)))
-
-        ["Export" "latex" _options content]
-        (if html-export?
-          (latex/html-export content true false)
-          (latex/latex (str (d/squuid)) content true false))
-
-        ["Custom" "query" _options _result content]
-        (try
-          (let [query (reader/read-string content)]
-            (custom-query config query))
-          (catch :default e
-            (log/error :read-string-error e)
-            (ui/block-error "Invalid query:" {:content content})))
-
-        ["Custom" "note" _options result _content]
-        (admonition config "note" result)
-
-        ["Custom" "tip" _options result _content]
-        (admonition config "tip" result)
-
-        ["Custom" "important" _options result _content]
-        (admonition config "important" result)
-
-        ["Custom" "caution" _options result _content]
-        (admonition config "caution" result)
-
-        ["Custom" "warning" _options result _content]
-        (admonition config "warning" result)
-
-        ["Custom" "pinned" _options result _content]
-        (admonition config "pinned" result)
-
-        ["Custom" "center" _options l _content]
-        (->elem
-         :div.text-center
-         (markup-elements-cp config l))
-
-        ["Custom" name _options l _content]
-        (->elem
-         :div
-         {:class name}
-         (markup-elements-cp config l))
+  (try
+    (match item
+      ["Drawer" name lines]
+      (when (or (not= name "logbook")
+                (and
+                 (= name "logbook")
+                 (state/enable-timetracking?)
+                 (or  (get-in (state/get-config) [:logbook/settings :enabled-in-all-blocks])
+                      (when (get-in (state/get-config)
+                                    [:logbook/settings :enabled-in-timestamped-blocks] true)
+                        (or (:block/scheduled (:block config))
+                            (:block/deadline (:block config)))))))
+        [:div
+         [:div.text-sm
+          [:div.drawer {:data-drawer-name name}
+           (ui/foldable
+            [:div.opacity-50.font-medium.logbook
+             (util/format ":%s:" (string/upper-case name))]
+            [:div.opacity-50.font-medium
+             (if (= name "logbook")
+               (logbook-cp lines)
+               (apply str lines))
+             [:div ":END:"]]
+            {:default-collapsed? true
+             :title-trigger? true})]]])
+
+      ;; for file-level property in orgmode: #+key: value
+      ;; only display caption. https://orgmode.org/manual/Captions.html.
+      ["Directive" key value]
+      [:div.file-level-property
+       (when (contains? #{"caption"} (string/lower-case key))
+         [:span.font-medium
+          [:span.font-bold (string/upper-case key)]
+          (str ": " value)])]
+
+      ["Paragraph" l]
+      ;; TODO: speedup
+      (if (util/safe-re-find #"\"Export_Snippet\" \"embed\"" (str l))
+        (->elem :div (map-inline config l))
+        (->elem :div.is-paragraph (map-inline config l)))
+
+      ["Horizontal_Rule"]
+      (when-not (:slide? config)
+        [:hr])
+      ["Heading" h]
+      (block-container config h)
+      ["List" l]
+      (let [lists (divide-lists l)]
+        (if (= 1 (count lists))
+          (let [l (first lists)]
+            (->elem
+             (list-element l)
+             (map #(list-item config %) l)))
+          [:div.list-group
+           (for [l lists]
+             (->elem
+              (list-element l)
+              (map #(list-item config %) l)))]))
+      ["Table" t]
+      (table config t)
+      ["Math" s]
+      (if html-export?
+        (latex/html-export s true true)
+        (latex/latex (str (d/squuid)) s true true))
+      ["Example" l]
+      [:pre.pre-wrap-white-space
+       (join-lines l)]
+      ["Quote" l]
+      (->elem
+       :blockquote
+       (markup-elements-cp config l))
+      ["Raw_Html" content]
+      (when (not html-export?)
+        [:div.raw_html {:dangerouslySetInnerHTML
+                        {:__html content}}])
+      ["Export" "html" _options content]
+      (when (not html-export?)
+        [:div.export_html {:dangerouslySetInnerHTML
+                           {:__html content}}])
+      ["Hiccup" content]
+      (ui/catch-error
+       [:div.warning {:title "Invalid hiccup"}
+        content]
+       (-> (safe-read-string content)
+           (security/remove-javascript-links-in-href)))
+
+      ["Export" "latex" _options content]
+      (if html-export?
+        (latex/html-export content true false)
+        (latex/latex (str (d/squuid)) content true false))
+
+      ["Custom" "query" _options _result content]
+      (try
+        (let [query (reader/read-string content)]
+          (custom-query config query))
+        (catch :default e
+          (log/error :read-string-error e)
+          (ui/block-error "Invalid query:" {:content content})))
+
+      ["Custom" "note" _options result _content]
+      (admonition config "note" result)
+
+      ["Custom" "tip" _options result _content]
+      (admonition config "tip" result)
+
+      ["Custom" "important" _options result _content]
+      (admonition config "important" result)
+
+      ["Custom" "caution" _options result _content]
+      (admonition config "caution" result)
+
+      ["Custom" "warning" _options result _content]
+      (admonition config "warning" result)
+
+      ["Custom" "pinned" _options result _content]
+      (admonition config "pinned" result)
+
+      ["Custom" "center" _options l _content]
+      (->elem
+       :div.text-center
+       (markup-elements-cp config l))
 
-        ["Latex_Fragment" l]
-        [:p.latex-fragment
-         (inline config ["Latex_Fragment" l])]
+      ["Custom" name _options l _content]
+      (->elem
+       :div
+       {:class name}
+       (markup-elements-cp config l))
 
-        ["Latex_Environment" name option content]
-        (let [content (latex-environment-content name option content)]
-          (if html-export?
-            (latex/html-export content true true)
-            (latex/latex (str (d/squuid)) content true true)))
+      ["Latex_Fragment" l]
+      [:p.latex-fragment
+       (inline config ["Latex_Fragment" l])]
 
-        ["Displayed_Math" content]
+      ["Latex_Environment" name option content]
+      (let [content (latex-environment-content name option content)]
         (if html-export?
           (latex/html-export content true true)
-          (latex/latex (str (d/squuid)) content true true))
-
-        ["Footnote_Definition" name definition]
-        (let [id (util/url-encode name)]
-          [:div.footdef
-           [:div.footpara
-            (conj
-             (markup-element-cp config ["Paragraph" definition])
-             [:a.ml-1 {:id (str "fn." id)
-                       :style {:font-size 14}
-                       :class "footnum"
-                       :on-click #(route-handler/jump-to-anchor! (str "fnr." id))}
-              [:sup.fn (str name "↩︎")]])]])
-
-        ["Src" options]
-        [:div.cp__fenced-code-block
-         (if-let [opts (plugin-handler/hook-fenced-code-by-type (util/safe-lower-case (:language options)))]
-           (plugins/hook-ui-fenced-code (string/join "" (:lines options)) opts)
-           (src-cp config options html-export?))]
+          (latex/latex (str (d/squuid)) content true true)))
+
+      ["Displayed_Math" content]
+      (if html-export?
+        (latex/html-export content true true)
+        (latex/latex (str (d/squuid)) content true true))
+
+      ["Footnote_Definition" name definition]
+      (let [id (util/url-encode name)]
+        [:div.footdef
+         [:div.footpara
+          (conj
+           (markup-element-cp config ["Paragraph" definition])
+           [:a.ml-1 {:id (str "fn." id)
+                     :style {:font-size 14}
+                     :class "footnum"
+                     :on-click #(route-handler/jump-to-anchor! (str "fnr." id))}
+            [:sup.fn (str name "↩︎")]])]])
+
+      ["Src" options]
+      [:div.cp__fenced-code-block
+       (if-let [opts (plugin-handler/hook-fenced-code-by-type (util/safe-lower-case (:language options)))]
+         (plugins/hook-ui-fenced-code (string/join "" (:lines options)) opts)
+         (src-cp config options html-export?))]
 
-        :else
-        "")
-      (catch js/Error e
-        (println "Convert to html failed, error: " e)
-        ""))))
+      :else
+      "")
+    (catch js/Error e
+      (println "Convert to html failed, error: " e)
+      "")))
 
 (defn markup-elements-cp
   [config col]

+ 6 - 7
src/main/frontend/components/export.cljs

@@ -12,8 +12,13 @@
   (when-let [current-repo (state/get-current-repo)]
     [:div.export
      [:h1.title "Export"]
-
      [:ul.mr-1
+      [:li.mb-4
+       [:a.font-medium {:on-click #(export/export-repo-as-edn-v2! current-repo)}
+        (t :export-edn)]]
+      [:li.mb-4
+       [:a.font-medium {:on-click #(export/export-repo-as-json-v2! current-repo)}
+        (t :export-json)]]
       (when (util/electron?)
         [:li.mb-4
          [:a.font-medium {:on-click #(export/export-repo-as-html! current-repo)}
@@ -25,12 +30,6 @@
         [:li.mb-4
          [:a.font-medium {:on-click #(export/export-repo-as-opml! current-repo)}
           (t :export-opml)]])
-      [:li.mb-4
-       [:a.font-medium {:on-click #(export/export-repo-as-edn-v2! current-repo)}
-        (t :export-edn)]]
-      [:li.mb-4
-       [:a.font-medium {:on-click #(export/export-repo-as-json-v2! current-repo)}
-        (t :export-json)]]
       (when-not (mobile-util/native-platform?)
        [:li.mb-4
         [:a.font-medium {:on-click #(export/export-repo-as-roam-json! current-repo)}

+ 0 - 80
src/main/frontend/components/external.cljs

@@ -1,80 +0,0 @@
-;; deprecated by the onboarding import panel frontend.components.onboarding.setups
-
-(ns frontend.components.external
-  (:require [rum.core :as rum]
-            [goog.object :as gobj]
-            [clojure.string :as string]
-            [frontend.handler.notification :as notification]
-            [frontend.handler.external :as external-handler]
-            [frontend.ui :as ui]
-            [reitit.frontend.easy :as rfe]))
-
-(defonce *roam-importing? (atom nil))
-(defonce *opml-importing? (atom nil))
-(defonce *opml-imported-pages (atom nil))
-(rum/defc import-cp < rum/reactive
-  []
-  (let [roam-importing? (rum/react *roam-importing?)
-        opml-importing? (rum/react *opml-importing?)]
-    [:div#import
-     [:h1.title "Import JSON from Roam Research"]
-
-     [:input
-      {:id "import-roam"
-       :type "file"
-       :on-change (fn [e]
-                    (let [file (first (array-seq (.-files (.-target e))))
-                          file-name (gobj/get file "name")]
-                      (if (string/ends-with? file-name ".json")
-                        (do
-                          (reset! *roam-importing? true)
-                          (let [reader (js/FileReader.)]
-                            (set! (.-onload reader)
-                                  (fn [e]
-                                    (let [text (.. e -target -result)]
-                                      (external-handler/import-from-roam-json! text
-                                                                               #(reset! *roam-importing? false)))))
-                            (.readAsText reader file)))
-                        (notification/show! "Please choose a JSON file."
-                                            :error))))}]
-
-     [:hr]
-
-     [:div.mt-4
-      (case roam-importing?
-        true (ui/loading "Loading")
-        false [:b "Importing finished!"]
-        nil)]
-     ;;
-     [:h1.title "Import OPML"]
-
-     [:input
-      {:id "import-opml"
-       :type "file"
-       :on-change (fn [e]
-                    (let [file (first (array-seq (.-files (.-target e))))
-                          file-name (gobj/get file "name")]
-                      (if (string/ends-with? file-name ".opml")
-                        (do
-                          (reset! *opml-importing? true)
-                          (let [reader (js/FileReader.)]
-                            (set! (.-onload reader)
-                                  (fn [e]
-                                    (let [text (.. e -target -result)]
-                                      (external-handler/import-from-opml! text
-                                                                          (fn [pages]
-                                                                            (reset! *opml-imported-pages pages)
-                                                                            (reset! *opml-importing? false))))))
-                            (.readAsText reader file)))
-                        (notification/show! "Please choose a OPML file."
-                                            :error))))}]
-     [:div.mt-4
-      (case opml-importing?
-        true (ui/loading "Loading")
-        false [:div
-               [:b "Importing finished!"]
-               [:tr
-                (mapv (fn [page-name] [:tb
-                                       [:a {:href (rfe/href :page {:name page-name})} page-name]])
-                      @*opml-imported-pages)]]
-        nil)]]))

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

@@ -26,7 +26,8 @@
 (rum/defc home-button []
   (ui/with-shortcut :go/home "left"
     [:button.button.icon.inline
-     {:on-click #(do
+     {:title "Home"
+      :on-click #(do
                    (when (mobile-util/native-iphone?)
                      (state/set-left-sidebar-open! false))
                    (route-handler/redirect-to-home!))}
@@ -131,7 +132,8 @@
   [{:keys [on-click]}]
   (ui/with-shortcut :ui/toggle-left-sidebar "bottom"
     [:button.#left-menu.cp__header-left-menu.button.icon
-     {:on-click on-click}
+     {:title "Toggle left menu"
+      :on-click on-click}
       (ui/icon "menu-2" {:style {:fontSize ui/icon-size}})]))
 
 (rum/defc dropdown-menu < rum/reactive
@@ -142,7 +144,8 @@
     (ui/dropdown-with-links
      (fn [{:keys [toggle-fn]}]
        [:button.button.icon
-        {:on-click toggle-fn}
+        {:title "More"
+         :on-click toggle-fn}
         (ui/icon "dots" {:style {:fontSize ui/icon-size}})])
      (->>
       [(when (state/enable-editing?)
@@ -249,7 +252,8 @@
          (when current-repo ;; this is for the Search button
            (ui/with-shortcut :go/search "right"
              [:button.button.icon#search-button
-              {:on-click #(do (when (or (mobile-util/native-android?)
+              {:title "Search"
+               :on-click #(do (when (or (mobile-util/native-android?)
                                         (mobile-util/native-iphone?))
                                 (state/set-left-sidebar-open! false))
                               (state/pub-event! [:go/search]))}

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

@@ -46,7 +46,7 @@
     display: flex;
     align-items: center;
     justify-content: center;
-    opacity: .5;
+    opacity: .8;
 
     .ti, .tie {
       font-size: 20px;

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

@@ -13,8 +13,8 @@
   []
   [:div.help.cp__sidebar-help-docs
    (let [discourse-with-icon [:div.flex-row.inline-flex.items-center
-                            [:span.mr-1 (t :help/forum-community)]
-                            (ui/icon "message-circle" {:style {:font-size 20}})]
+                              [:span.mr-1 (t :help/forum-community)]
+                              (ui/icon "message-circle" {:style {:font-size 20}})]
          list
          [{:title "Usage"
            :children [[[:a
@@ -25,7 +25,7 @@
                       [(t :help/docs) "https://docs.logseq.com/"]
                       [(t :help/start) "https://docs.logseq.com/#/page/tutorial"]
                       ["FAQ" "https://docs.logseq.com/#/page/faq"]]}
-          
+
           {:title "Community"
            :children [[(t :help/awesome-logseq) "https://github.com/logseq/awesome-logseq"]
                       [(t :help/blog) "https://blog.logseq.com"]
@@ -36,7 +36,7 @@
                       [(t :help/bug) "https://github.com/logseq/logseq/issues/new?labels=from:in-app&template=bug_report.yaml"]
                       [(t :help/feature) "https://discuss.logseq.com/c/feature-requests/"]
                       [(t :help/changelog) "https://docs.logseq.com/#/page/changelog"]]}
-          
+
           {:title "About"
            :children [[(t :help/about) "https://logseq.com/blog/about"]]}
 
@@ -44,7 +44,7 @@
            :children [[(t :help/privacy) "https://logseq.com/blog/privacy-policy"]
                       [(t :help/terms) "https://logseq.com/blog/terms"]]}]]
 
-          
+
 
      (map (fn [sublist]
             [[:p.mt-4.mb-1 [:b (:title sublist)]]

+ 0 - 1
src/main/frontend/components/onboarding/index.css

@@ -314,7 +314,6 @@ body[data-page=import] {
 
       small {
         font-size: 11px;
-        text-align: center;
       }
 
       &:hover {

+ 51 - 55
src/main/frontend/components/onboarding/setups.cljs

@@ -2,9 +2,11 @@
   (:require [frontend.state :as state]
             [rum.core :as rum]
             [frontend.ui :as ui]
+            [frontend.context.i18n :refer [t]]
             [frontend.components.svg :as svg]
             [frontend.handler.page :as page-handler]
             [frontend.handler.route :as route-handler]
+            [frontend.handler.ui :as ui-handler]
             [frontend.util :as util]
             [frontend.handler.web.nfs :as nfs]
             [frontend.mobile.util :as mobile-util]
@@ -126,15 +128,13 @@
               [:strong.uppercase title]
               [:small.opacity-50 label]]]))]]])))
 
-(defonce *roam-importing? (atom nil))
-(defonce *lsq-importing? (atom nil))
-(defonce *opml-importing? (atom nil))
 (defonce *opml-imported-pages (atom nil))
 
 (defn- finished-cb
   []
+  (route-handler/redirect-to-home!)
   (notification/show! "Import finished!" :success)
-  (route-handler/redirect-to-home!))
+  (ui-handler/re-render-root!))
 
 (defn- roam-import-handler
   [e]
@@ -142,13 +142,16 @@
         file-name (gobj/get file "name")]
     (if (string/ends-with? file-name ".json")
       (do
-        (reset! *roam-importing? true)
+        (state/set-state! :graph/importing :roam-json)
         (let [reader (js/FileReader.)]
           (set! (.-onload reader)
                 (fn [e]
                   (let [text (.. e -target -result)]
-                    (external-handler/import-from-roam-json! text
-                                                             #(do (reset! *roam-importing? false) (finished-cb))))))
+                    (external-handler/import-from-roam-json!
+                     text
+                     #(do
+                        (state/set-state! :graph/importing nil)
+                        (finished-cb))))))
           (.readAsText reader file)))
       (notification/show! "Please choose a JSON file."
                           :error))))
@@ -157,32 +160,27 @@
   [e]
   (let [file      (first (array-seq (.-files (.-target e))))
         file-name (some-> (gobj/get file "name")
-                          (string/lower-case))]
-    (cond (string/ends-with? file-name ".edn")
-          (do
-            (reset! *lsq-importing? true)
-            (let [reader (js/FileReader.)]
-              (set! (.-onload reader)
-                    (fn [e]
-                      (let [text (.. e -target -result)]
-                        (external-handler/import-from-edn! text
-                                                           #(do (reset! *lsq-importing? false) (finished-cb))))))
-              (.readAsText reader file)))
-
-          (string/ends-with? file-name ".json")
-          (do
-            (reset! *lsq-importing? true)
-            (let [reader (js/FileReader.)]
-              (set! (.-onload reader)
-                    (fn [e]
-                      (let [text (.. e -target -result)]
-                        (external-handler/import-from-json! text
-                                                            #(do (reset! *lsq-importing? false) (finished-cb))))))
-              (.readAsText reader file)))
-
-          :else
-          (notification/show! "Please choose an EDN or a JSON file."
-                              :error))))
+                          (string/lower-case))
+        edn? (string/ends-with? file-name ".edn")
+        json? (string/ends-with? file-name ".json")]
+    (if (or edn? json?)
+      (do
+        (state/set-state! :graph/importing :logseq)
+        (let [reader (js/FileReader.)
+              import-f (if edn?
+                         external-handler/import-from-edn!
+                         external-handler/import-from-json!)]
+          (set! (.-onload reader)
+                (fn [e]
+                  (let [text (.. e -target -result)]
+                    (import-f
+                     text
+                     #(do
+                        (state/set-state! :graph/importing nil)
+                        (finished-cb))))))
+          (.readAsText reader file)))
+      (notification/show! "Please choose an EDN or a JSON file."
+                          :error))))
 
 (defn- opml-import-handler
   [e]
@@ -190,7 +188,7 @@
         file-name (gobj/get file "name")]
     (if (string/ends-with? file-name ".opml")
       (do
-        (reset! *opml-importing? true)
+        (state/set-state! :graph/importing :opml)
         (let [reader (js/FileReader.)]
           (set! (.-onload reader)
                 (fn [e]
@@ -198,7 +196,7 @@
                     (external-handler/import-from-opml! text
                                                         (fn [pages]
                                                           (reset! *opml-imported-pages pages)
-                                                          (reset! *opml-importing? false)
+                                                          (state/set-state! :graph/importing nil)
                                                           (finished-cb))))))
           (.readAsText reader file)))
       (notification/show! "Please choose a OPML file."
@@ -206,11 +204,18 @@
 
 (rum/defc importer < rum/reactive
   [{:keys [query-params]}]
-  (let [roam-importing? (rum/react *roam-importing?)
-        lsq-importing?  (rum/react *lsq-importing?)
-        opml-importing? (rum/react *opml-importing?)
-        importing?      (or roam-importing? lsq-importing? opml-importing?)]
-
+  (if (state/sub :graph/importing)
+    (let [{:keys [total current-idx current-page]} (state/sub :graph/importing-state)
+          left-label [:div.flex.flex-row.font-bold
+                      (t :importing)
+                      [:div.hidden.md:flex.flex-row
+                       [:span.mr-1 ": "]
+                       [:div.text-ellipsis-wrapper {:style {:max-width 300}}
+                        current-page]]]
+          width (js/Math.round (* (.toFixed (/ current-idx total) 2) 100))
+          process (when (and total current-idx)
+                    (str current-idx "/" total))]
+      (ui/progress-bar-with-label width left-label process))
     (setups-container
      :importer
      [:article.flex.flex-col.items-center.importer.py-16.px-8
@@ -219,39 +224,30 @@
        [:h2 "If they are in a JSON, EDN or Markdown format Logseq can work with them."]]
       [:section.d.md:flex
        [:label.action-input.flex.items-center.mx-2.my-2
-        {:disabled importing?}
         [:span.as-flex-center [:i (svg/roam-research 28)]]
         [:div.flex.flex-col
-         (if roam-importing?
-           (ui/loading "Importing ...")
-           [[:strong "RoamResearch"]
-            [:small "Import a JSON Export of your Roam graph"]])]
+         [[:strong "RoamResearch"]
+          [:small "Import a JSON Export of your Roam graph"]]]
         [:input.absolute.hidden
          {:id        "import-roam"
           :type      "file"
           :on-change roam-import-handler}]]
 
        [:label.action-input.flex.items-center.mx-2.my-2
-        {:disabled importing?}
         [:span.as-flex-center [:i (svg/logo 28)]]
         [:span.flex.flex-col
-         (if lsq-importing?
-           (ui/loading "Importing ...")
-           [[:strong "EDN / JSON"]
-            [:small "Import an EDN or a JSON Export of your Logseq graph"]])]
+         [[:strong "EDN / JSON"]
+          [:small "Import an EDN or a JSON Export of your Logseq graph"]]]
         [:input.absolute.hidden
          {:id        "import-lsq"
           :type      "file"
           :on-change lsq-import-handler}]]
 
        [:label.action-input.flex.items-center.mx-2.my-2
-        {:disabled importing?}
         [:span.as-flex-center (ui/icon "sitemap" {:style {:fontSize "26px"}})]
         [:span.flex.flex-col
-         (if opml-importing?
-           (ui/loading "Importing ...")
-           [[:strong "OPML"]
-            [:small " Import OPML files"]])]
+         [[:strong "OPML"]
+          [:small " Import OPML files"]]]
 
         [:input.absolute.hidden
          {:id        "import-opml"

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

@@ -127,7 +127,7 @@
         *collapsed? (atom nil)]
     (ui/foldable
      [:div.flex.flex-row.flex-1.justify-between.items-center
-      [:h2.font-bold.opacity-50 (str
+      [:h2.font-bold.opacity-80 (str
                                  (when (seq filters)
                                    (str filter-n " of "))
                                  total
@@ -275,7 +275,7 @@
         [:div.references.mt-6.flex-1.flex-row
          [:div.content.flex-1
           (ui/foldable
-           [:h2.font-bold {:style {:opacity "0.3"}}
+           [:h2.font-bold.opacity-80
             (if @n-ref
               (str @n-ref " Unlinked Reference" (when (> @n-ref 1)
                                                   "s"))

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

@@ -25,7 +25,8 @@
   (when-not (util/mobile?)
     (ui/with-shortcut :ui/toggle-right-sidebar "left"
       [:button.button.icon.fade-link.toggle-right-sidebar
-       {:on-click ui-handler/toggle-right-sidebar!}
+       {:title "Toggle right sidebar"
+        :on-click ui-handler/toggle-right-sidebar!}
        (ui/icon "layout-sidebar-right" {:style {:fontSize "20px"}})])))
 
 (rum/defc block-cp < rum/reactive

+ 14 - 7
src/main/frontend/components/sidebar.cljs

@@ -231,7 +231,7 @@
                                [".favorites .bd" ".recent .bd" ".dropdown-wrapper" ".nav-header"])
                      (close-modal-fn)))}
      [:div.flex.flex-col.pb-4.wrap.gap-4
-      [:nav.px-4.flex.flex-col.gap-1 {:aria-label "Sidebar"}
+      [:nav.px-4.flex.flex-col.gap-1 {:aria-label "Navigation menu"}
        (repo/repos-dropdown)
 
        [:div.nav-header.flex.gap-1.flex-col
@@ -283,8 +283,7 @@
       (when (and left-sidebar-open? (not config/publishing?)) (recent-pages t))
 
       (when-not (mobile-util/native-platform?)
-        [:nav.px-2 {:aria-label "Sidebar"
-                    :class      "new-page"}
+        [:footer.px-2 {:class "new-page"}
          (when-not config/publishing?
            [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md.new-page-link
             {:on-click (fn []
@@ -346,7 +345,9 @@
                     :route-match route-match})
 
      [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row
-      {:data-is-margin-less-pages margin-less-pages?}
+      
+      {:tabindex "-1"
+       :data-is-margin-less-pages margin-less-pages?}
 
       (when (util/electron?)
         (find-in-page/search))
@@ -593,7 +594,8 @@
         default-home (get-default-home-if-valid)
         logged? (user-handler/logged-in?)
         show-action-bar? (state/sub :mobile/show-action-bar?)
-        show-recording-bar? (state/sub :mobile/show-recording-bar?)]
+        show-recording-bar? (state/sub :mobile/show-recording-bar?)
+        preferred-language (state/sub [:preferred-language])]
     (theme/container
      {:t             t
       :theme         theme
@@ -606,15 +608,20 @@
       :settings-open? settings-open?
       :sidebar-blocks-len (count right-sidebar-blocks)
       :system-theme? system-theme?
+      :preferred-language preferred-language
       :on-click      (fn [e]
                        (editor-handler/unhighlight-blocks!)
                        (util/fix-open-external-with-shift! e))}
 
-     [:div.theme-inner
+     [:main.theme-inner
       {:class (util/classnames [{:ls-left-sidebar-open left-sidebar-open?
                                  :ls-right-sidebar-open sidebar-open?
                                  :ls-wide-mode wide-mode?}])}
-
+      [:button#skip-to-main
+       {:on-key-up (fn [e]
+                        (when (= (.-key e) "Enter")
+                          (ui/focus-element (ui/main-node))))}
+       "Skip to main content"]
       [:div.#app-container
        [:div#left-container
         {:class (if (state/sub :ui/sidebar-open?) "overflow-hidden" "w-full")}

+ 15 - 0
src/main/frontend/components/sidebar.css

@@ -25,6 +25,21 @@
   flex: 0 0 100%;
 }
 
+#skip-to-main {
+  @apply fixed p-2 rounded;
+
+  left: 50%;
+  transform: translate(-50%, 0);
+  background-color: var(--ls-secondary-background-color);
+  top: -100px;
+  z-index: 10000;
+  transition: top 0.3s;
+
+  &:focus {
+    top: 20px;
+  }
+}
+
 #left-container {
   @apply flex flex-1 flex-col relative h-screen;
 }

+ 9 - 5
src/main/frontend/components/theme.cljs

@@ -13,7 +13,7 @@
 
 (rum/defc container
   [{:keys [route theme on-click current-repo nfs-granted? db-restoring?
-           settings-open? sidebar-open? system-theme? sidebar-blocks-len]} child]
+           settings-open? sidebar-open? system-theme? sidebar-blocks-len preferred-language]} child]
   (let [mounted-fn (use-mounted)
         [restored-sidebar? set-restored-sidebar?] (rum/use-state false)]
 
@@ -28,6 +28,10 @@
         (plugin-handler/hook-plugin-app :theme-mode-changed {:mode theme} nil))
      [theme])
 
+    (rum/use-effect!
+     #(let [doc js/document.documentElement]
+        (.setAttribute doc "lang" preferred-language)))
+
     (rum/use-effect!
      #(when (and restored-sidebar?
                  (mounted-fn))
@@ -68,10 +72,10 @@
                     config/publishing?
                     ;; other graphs exists
                     (seq repos))
-            (route-handler/redirect! {:to :repo-add})
-            (do
-              (ui-handler/restore-right-sidebar-state!)
-              (set-restored-sidebar? true))))))
+             (route-handler/redirect! {:to :repo-add})
+             (do
+               (ui-handler/restore-right-sidebar-state!)
+               (set-restored-sidebar? true))))))
      [db-restoring?])
 
     (rum/use-effect!

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

@@ -260,6 +260,7 @@
         :developer-mode-alert "You need to restart the app to enable the plugin system. Do you want to restart it now?"
         :relaunch-confirm-to-work "Should relaunch app to make it work. Do you want to restart it now?"
         :import "Import"
+        :importing "Importing"
         :join-community "Join the community"
         :sponsor-us "Sponsor Us"
         :discourse-title "Our forum!"

+ 3 - 5
src/main/frontend/fs/capacitor_fs.cljs

@@ -185,15 +185,13 @@
             repo-dir (config/get-local-dir repo)
             ext (util/get-file-ext path)
             db-content (or old-content (db/get-file repo path) "")
-            contents-matched? (contents-matched? disk-content db-content)
-            pending-writes (state/get-write-chan-length)]
+            contents-matched? (contents-matched? disk-content db-content)]
       (cond
         (and
          (not= stat :not-found)   ; file on the disk was deleted
          (not contents-matched?)
-         (not (contains? #{"excalidraw" "tldr" "edn" "css"} ext))
-         (not (string/includes? path "/.recycle/"))
-         (zero? pending-writes))
+         (not (contains? #{"excalidraw" "edn" "css"} ext))
+         (not (string/includes? path "/.recycle/")))
         (p/let [disk-content (encrypt/decrypt disk-content)]
           (state/pub-event! [:file/not-matched-from-disk path disk-content content]))
 

+ 2 - 4
src/main/frontend/fs/nfs.cljs

@@ -163,7 +163,6 @@
           (if file-handle
             (-> (p/let [local-file (.getFile file-handle)
                         local-content (.text local-file)
-                        pending-writes (state/get-write-chan-length)
                         ext (string/lower-case (util/get-file-ext path))
                         db-content (db/get-file repo path)
                         contents-matched? (contents-matched? local-content (or db-content ""))]
@@ -172,9 +171,8 @@
                          (not (string/blank? db-content))
                          (not (:skip-compare? opts))
                          (not contents-matched?)
-                         (not (contains? #{"excalidraw" "tldr" "edn" "css"} ext))
-                         (not (string/includes? path "/.recycle/"))
-                         (zero? pending-writes))
+                         (not (contains? #{"excalidraw" "edn" "css"} ext))
+                         (not (string/includes? path "/.recycle/")))
                       (p/let [local-content (encrypt/decrypt local-content)]
                         (state/pub-event! [:file/not-matched-from-disk path local-content content]))
                       (p/let [_ (verify-permission repo file-handle true)

+ 3 - 5
src/main/frontend/fs/node.cljs

@@ -53,15 +53,13 @@
             disk-content (or disk-content "")
             ext (string/lower-case (util/get-file-ext path))
             db-content (or old-content (db/get-file repo path) "")
-            contents-matched? (contents-matched? disk-content db-content)
-            pending-writes (state/get-write-chan-length)]
+            contents-matched? (contents-matched? disk-content db-content)]
       (cond
         (and
          (not= stat :not-found)         ; file on the disk was deleted
          (not contents-matched?)
-         (not (contains? #{"excalidraw" "tldr" "edn" "css"} ext))
-         (not (string/includes? path "/.recycle/"))
-         (zero? pending-writes))
+         (not (contains? #{"excalidraw" "edn" "css"} ext))
+         (not (string/includes? path "/.recycle/")))
         (p/let [disk-content (encrypt/decrypt disk-content)]
           (state/pub-event! [:file/not-matched-from-disk path disk-content content]))
 

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

@@ -171,9 +171,10 @@
                       (string/includes? % "logseq_local_/")) nfs-dbs))
       (do (notification/show! ["DB version is not compatible, please clear cache then re-add your graph back."
                                (ui/button
-                                (t :settings-page/clear-cache)
-                                :class    "text-sm p-1"
-                                :on-click clear-cache!)] :error false)
+                                 (t :settings-page/clear-cache)
+                                 :class    "ui__modal-enter"
+                                 :class    "text-sm p-1"
+                                 :on-click clear-cache!)] :error false)
           {:url config/local-repo
            :example? true})
 
@@ -217,12 +218,12 @@
 
     (p/let [repos (get-repos)]
       (state/set-repos! repos)
-      (restore-and-setup! repos db-schema)
-      (when (mobile-util/native-platform?)
-        (p/do! (mobile-util/hide-splash))))
+      (restore-and-setup! repos db-schema))
+    (when (mobile-util/native-platform?)
+      (p/do! (mobile-util/hide-splash)))
 
     (db/run-batch-txs!)
-    (file-handler/run-writes-chan!)
+
     (when config/dev?
       (enable-datalog-console))
     (when (util/electron?)

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

@@ -287,7 +287,8 @@
           get-parents (fn [block]
                         (loop [block block
                                result [block]]
-                          (if-let [parent (id->block (:db/id (:block/parent block)))]
-                            (recur parent (conj result parent))
-                            result)))]
+                          (let [parent (id->block (:db/id (:block/parent block)))]
+                            (if (and parent (not= (:db/id parent) (:db/id block)))
+                              (recur parent (conj result parent))
+                              result))))]
       (distinct (mapcat get-parents filtered-ref-blocks)))))

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

@@ -1953,7 +1953,12 @@
   [tree-vec format {:keys [target-block keep-uuid?] :as opts}]
   (let [blocks (block-tree->blocks tree-vec format keep-uuid?)
         page-id (:db/id (:block/page target-block))
-        blocks (gp-block/with-parent-and-left page-id blocks)]
+        blocks (gp-block/with-parent-and-left page-id blocks)
+        block-refs (->> (mapcat :block/refs blocks)
+                        (set)
+                        (filter (fn [ref] (and (vector? ref) (= :block/uuid (first ref))))))]
+    (when (seq block-refs)
+      (db/transact! (map (fn [[_ id]] {:block/uuid id}) block-refs)))
     (paste-blocks
      blocks
      opts)))

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

@@ -476,6 +476,7 @@
        (ui/button
          (t :yes)
          :autoFocus "on"
+         :class "ui__modal-enter"
          :large? true
          :on-click (fn []
                      (state/close-modal!)

+ 17 - 6
src/main/frontend/handler/export.cljs

@@ -444,6 +444,15 @@
        x))
    vec-tree))
 
+(defn- safe-keywordize
+  [block]
+  (update block :block/properties
+          (fn [properties]
+            (when (seq properties)
+              (->> (filter (fn [[k _v]]
+                             (gp-util/valid-edn-keyword? k)) properties)
+                   (into {}))))))
+
 (defn- blocks [db]
   {:version 1
    :blocks
@@ -460,12 +469,14 @@
                              name
                              {:transform? false})
                      blocks' (map (fn [b]
-                                    (if (seq (:block/properties b))
-                                      (update b :block/content
-                                              (fn [content] (property/remove-properties (:block/format b) content)))
-                                      b)) blocks)
-                     children (outliner-tree/blocks->vec-tree blocks' name)]
-                 (assoc page :block/children children))))
+                                    (let [b' (if (seq (:block/properties b))
+                                               (update b :block/content
+                                                       (fn [content] (property/remove-properties (:block/format b) content)))
+                                               b)]
+                                      (safe-keywordize b'))) blocks)
+                     children (outliner-tree/blocks->vec-tree blocks' name)
+                     page' (safe-keywordize page)]
+                 (assoc page' :block/children children))))
         (nested-select-keys
          [:block/id
           :block/page-name

+ 54 - 27
src/main/frontend/handler/external.cljs

@@ -16,7 +16,9 @@
             [frontend.handler.page :as page]
             [frontend.handler.editor :as editor]
             [frontend.handler.notification :as notification]
-            [frontend.util :as util]))
+            [frontend.util :as util]
+            [clojure.core.async :as async]
+            [medley.core :as medley]))
 
 (defn index-files!
   "Create file structure, then parse into DB (client only)"
@@ -42,7 +44,7 @@
                                          ".md")]
                            {:file/path path
                             :file/content text}))))
-                   files)
+                files)
         files (remove nil? files)]
     (repo-handler/parse-files-and-load-to-db! repo files nil)
     (let [files (->> (map (fn [{:file/keys [path content]}] (when path [path content])) files)
@@ -53,13 +55,13 @@
                                             :finish-handler finish-handler}))
     (let [journal-pages-tx (let [titles (filter date/valid-journal-title? titles)]
                              (map
-                              (fn [title]
-                                (let [day (date/journal-title->int title)
-                                      page-name (util/page-name-sanity-lc (date-time-util/int->journal-title day (state/get-date-formatter)))]
-                                  {:block/name page-name
-                                   :block/journal? true
-                                   :block/journal-day day}))
-                              titles))]
+                               (fn [title]
+                                 (let [day (date/journal-title->int title)
+                                       page-name (util/page-name-sanity-lc (date-time-util/int->journal-title day (state/get-date-formatter)))]
+                                   {:block/name page-name
+                                    :block/journal? true
+                                    :block/journal-day day}))
+                               titles))]
       (when (seq journal-pages-tx)
         (db/transact! repo journal-pages-tx)))))
 
@@ -132,27 +134,43 @@
                                         "\nSkipped and continue the remaining import.") :error))))))
   title)
 
-(defn- pre-transact-uuids
+(defn- pre-transact-uuids!
   "Collect all uuids from page trees and write them to the db before hand."
   [pages]
-  (let [uuids (map (fn [block]
-                     {:block/uuid (:uuid block)})
-                   (mapcat #(tree-seq map? :children %)
-                           pages))]
-    (db/transact! uuids)
-    pages))
+  (let [uuids (mapv (fn [block]
+                      {:block/uuid (:uuid block)})
+                    (mapcat #(tree-seq map? :children %)
+                            pages))]
+    (db/transact! uuids)))
 
 (defn- import-from-tree!
   "Not rely on file system - backend compatible.
    tree-translator-fn: translate exported tree structure to the desired tree for import"
   [data tree-translator-fn]
-  (try (->> (:blocks data)
-            (map tree-translator-fn)
-            (pre-transact-uuids)
-            (mapv create-page-with-exported-tree!))
-       (editor/set-blocks-id! (db/get-all-referenced-blocks-uuid))
-       (catch js/Error e
-         (notification/show! (str "Error happens when importing:\n" e) :error))))
+  (let [imported-chan (async/promise-chan)]
+    (try
+      (let [blocks (->> (:blocks data)
+                        (mapv tree-translator-fn )
+                        (sort-by :title)
+                        (medley/indexed))
+            job-chan (async/to-chan! blocks)]
+        (state/set-state! [:graph/importing-state :total] (count blocks))
+        (pre-transact-uuids! blocks)
+        (async/go-loop []
+          (if-let [[i block] (async/<! job-chan)]
+            (do
+              (state/set-state! [:graph/importing-state :current-idx] (inc i))
+              (state/set-state! [:graph/importing-state :current-page] (:title block))
+              (async/<! (async/timeout 10))
+              (create-page-with-exported-tree! block)
+              (recur))
+            (do
+              (editor/set-blocks-id! (db/get-all-referenced-blocks-uuid))
+              (async/offer! imported-chan true)))))
+
+      (catch :default e
+        (notification/show! (str "Error happens when importing:\n" e) :error)
+        (async/offer! imported-chan true)))))
 
 (defn tree-vec-translate-edn
   "Actions to do for loading edn tree structure.
@@ -177,8 +195,16 @@
 
 (defn import-from-edn!
   [raw finished-ok-handler]
-  (import-from-tree! (edn/read-string raw) tree-vec-translate-edn)
-  (finished-ok-handler nil)) ;; it was designed to accept a list of imported page names but now deprecated
+  (try
+    (let [data (edn/read-string raw)]
+     (async/go
+       (async/<! (import-from-tree! data tree-vec-translate-edn))
+       (finished-ok-handler nil)))
+    (catch :default e
+      (js/console.error e)
+      (notification/show!
+       (str (.-message e))
+       :error)))) ;; it was designed to accept a list of imported page names but now deprecated
 
 (defn tree-vec-translate-json
   "Actions to do for loading json tree structure.
@@ -210,5 +236,6 @@
   [raw finished-ok-handler]
   (let [json     (js/JSON.parse raw)
         clj-data (js->clj json :keywordize-keys true)]
-    (import-from-tree! clj-data tree-vec-translate-json))
-  (finished-ok-handler nil)) ;; it was designed to accept a list of imported page names but now deprecated
+    (async/go
+      (async/<! (import-from-tree! clj-data tree-vec-translate-json))
+      (finished-ok-handler nil)))) ;; it was designed to accept a list of imported page names but now deprecated

+ 20 - 44
src/main/frontend/handler/file.cljs

@@ -2,8 +2,6 @@
   (:refer-clojure :exclude [load-file])
   (:require ["/frontend/utils" :as utils]
             [borkdude.rewrite-edn :as rewrite]
-            [cljs.core.async.interop :refer [<p!]]
-            [clojure.core.async :as async]
             [frontend.config :as config]
             [frontend.db :as db]
             [frontend.fs :as fs]
@@ -206,30 +204,8 @@
   (alter-file repo path new-content {:reset? false
                                      :re-render-root? false}))
 
-(defn alter-files
-  [repo files {:keys [reset? update-db?]
-               :or {reset? false
-                    update-db? true}
-               :as opts}]
-  ;; old file content
-  (let [file->content (let [paths (map first files)]
-                        (zipmap paths
-                                (map (fn [path] (db/get-file repo path)) paths)))]
-    ;; update db
-    (when update-db?
-      (doseq [[path content] files]
-        (if reset?
-          (reset-file! repo path content)
-          (db/set-file-content! repo path content))))
-
-    (when-let [chan (state/get-file-write-chan)]
-      (let [chan-callback (:chan-callback opts)]
-        (async/put! chan [repo files opts file->content])
-        (when chan-callback
-          (chan-callback))))))
-
 (defn alter-files-handler!
-  [repo files {:keys [finish-handler chan]} file->content]
+  [repo files {:keys [finish-handler]} file->content]
   (let [write-file-f (fn [[path content]]
                        (when path
                          (let [original-content (get file->content path)]
@@ -254,30 +230,30 @@
                                                                         :error error})))))))
         finish-handler (fn []
                          (when finish-handler
-                           (finish-handler))
-                         (ui-handler/re-render-file!))]
+                           (finish-handler)))]
     (-> (p/all (map write-file-f files))
         (p/then (fn []
-                  (finish-handler)
-                  (when chan
-                    (async/put! chan true))))
+                  (finish-handler)))
         (p/catch (fn [error]
                    (println "Alter files failed:")
-                   (js/console.error error)
-                   (async/put! chan false))))))
+                   (js/console.error error))))))
 
-(defn run-writes-chan!
-  []
-  (let [chan (state/get-file-write-chan)]
-    (async/go-loop []
-      (let [args (async/<! chan)]
-        ;; return a channel
-        (try
-          (<p! (apply alter-files-handler! args))
-          (catch js/Error e
-            (log/error :file/write-failed e))))
-      (recur))
-    chan))
+(defn alter-files
+  [repo files {:keys [reset? update-db?]
+               :or {reset? false
+                    update-db? true}
+               :as opts}]
+  ;; old file content
+  (let [file->content (let [paths (map first files)]
+                        (zipmap paths
+                                (map (fn [path] (db/get-file repo path)) paths)))]
+    ;; update db
+    (when update-db?
+      (doseq [[path content] files]
+        (if reset?
+          (reset-file! repo path content)
+          (db/set-file-content! repo path content))))
+    (alter-files-handler! repo files opts file->content)))
 
 (defn watch-for-current-graph-dir!
   []

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

@@ -245,9 +245,14 @@
         new-tag (if (re-find #"[\s\t]+" new-name)
                   (util/format "#[[%s]]" new-name)
                   (str "#" new-name))]
-    (-> (util/replace-ignore-case content (str "^" old-tag "\\b") new-tag)
-        (util/replace-ignore-case (str " " old-tag " ") (str " " new-tag " "))
-        (util/replace-ignore-case (str " " old-tag "$") (str " " new-tag)))))
+    ;; hash tag parsing rules https://github.com/logseq/mldoc/blob/701243eaf9b4157348f235670718f6ad19ebe7f8/test/test_markdown.ml#L631 
+    ;; Safari doesn't support look behind, don't use
+    ;; TODO: parse via mldoc
+    (string/replace content 
+                    (re-pattern (str "(?i)(^|\\s)(" (util/escape-regex-chars old-tag) ")(?=[,\\.]*($|\\s))"))
+                    ;;    case_insense^    ^lhs   ^_grp2                       look_ahead^         ^_grp3
+                    (fn [[_match lhs _grp2 _grp3]]
+                      (str lhs new-tag)))))
 
 (defn- replace-property-ref!
   [content old-name new-name]

+ 0 - 6
src/main/frontend/handler/ui.cljs

@@ -99,12 +99,6 @@
      (doseq [component (state/get-custom-query-components)]
        (rum/request-render component)))))
 
-(defn re-render-file!
-  []
-  (when-let [component (state/get-file-component)]
-    (when (= :file (state/get-current-route))
-      (rum/request-render component))))
-
 (defn highlight-element!
   [fragment]
   (let [id (and

+ 6 - 15
src/main/frontend/modules/file/core.cljs

@@ -1,13 +1,13 @@
 (ns frontend.modules.file.core
-  (:require [cljs.core.async :as async]
-            [clojure.string :as string]
+  (:require [clojure.string :as string]
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.db :as db]
             [frontend.db.utils :as db-utils]
             [frontend.state :as state]
             [frontend.util :as util]
-            [frontend.util.property :as property]))
+            [frontend.util.property :as property]
+            [frontend.handler.file :as file-handler]))
 
 (defn- indented-block-content
   [content spaces-tabs]
@@ -101,16 +101,6 @@
 
 (def init-level 1)
 
-(defn push-to-write-chan
-  [files & opts]
-  (let [repo (state/get-current-repo)
-        chan (state/get-file-write-chan)]
-    (assert (some? chan) "File write chan shouldn't be nil")
-    (let [chan-callback (:chan-callback opts)]
-      (async/put! chan [repo files opts])
-      (when chan-callback
-        (chan-callback)))))
-
 (defn- transact-file-tx-if-not-exists!
   [page ok-handler]
   (when-let [repo (state/get-current-repo)]
@@ -151,8 +141,9 @@
                       (tree->file-content tree {:init-level init-level}))
         _ (assert (string? file-path) "File path should satisfy string?")
         ;; FIXME: name conflicts between multiple graphs
-        files [[file-path new-content]]]
-    (push-to-write-chan files)))
+        files [[file-path new-content]]
+        repo (state/get-current-repo)]
+    (file-handler/alter-files-handler! repo files {} {})))
 
 (defn save-tree!
   [page-block tree]

+ 6 - 5
src/main/frontend/modules/outliner/file.cljs

@@ -12,7 +12,6 @@
             [lambdaisland.glogi :as log]
             [frontend.state :as state]))
 
-(defonce write-chan (async/chan 100))
 (defonce write-chan-batch-buf (atom []))
 
 (def batch-write-interval 1000)
@@ -48,8 +47,8 @@
         page-db-id (:db/id page-block)
         blocks-count (model/get-page-blocks-count repo page-db-id)]
     (if (and (> blocks-count 500)
-             (not (state/input-idle? repo :diff 3000)))           ; long page
-      (async/put! write-chan [repo page-db-id])
+             (not (state/input-idle? repo :diff 3000))) ; long page
+      (async/put! (state/get-file-write-chan) [repo page-db-id])
       (let [whiteboard? (:block/whiteboard? page-block)
             pull-keys (if whiteboard? whiteboard-blocks-pull-keys-with-persisted-ids '[*])
             blocks (model/get-page-blocks-no-cache repo (:block/name page-block) {:pull-keys pull-keys})
@@ -83,9 +82,11 @@
      "Write file failed, can't find the current page!"
      :error)
     (when-let [repo (state/get-current-repo)]
-      (async/put! write-chan [repo page-db-id]))))
+      (if (:graph/importing @state/state) ; write immediately
+        (write-files! [[repo page-db-id]])
+        (async/put! (state/get-file-write-chan) [repo page-db-id])))))
 
-(util/batch write-chan
+(util/batch (state/get-file-write-chan)
             batch-write-interval
             write-files!
             write-chan-batch-buf)

+ 8 - 4
src/main/frontend/modules/outliner/pipeline.cljs

@@ -70,15 +70,19 @@
             tx (util/concat-without-nil truncate-refs-tx refs-tx)
             tx-report' (if (seq tx)
                          (let [refs-tx-data' (:tx-data (db/transact! repo tx {:outliner/transact? true
-                                                                         :compute-new-refs? true}))]
+                                                                              :compute-new-refs? true}))]
                            ;; merge
                            (assoc tx-report :tx-data (concat (:tx-data tx-report) refs-tx-data')))
-                         tx-report)]
-        (react/refresh! repo tx-report')
+                         tx-report)
+            importing? (:graph/importing @state/state)]
+
+        (when-not importing?
+          (react/refresh! repo tx-report'))
 
         (doseq [p (seq pages)]
           (updated-page-hook tx-report p))
-        (when (and state/lsp-enabled? (seq blocks))
+
+        (when (and state/lsp-enabled? (seq blocks) (not importing?))
           (state/pub-event! [:plugin/hook-db-tx
                              {:blocks  blocks
                               :tx-data (:tx-data tx-report)

+ 7 - 14
src/main/frontend/state.cljs

@@ -24,11 +24,11 @@
    (atom
     {:route-match                           nil
      :today                                 nil
-     :system/events                         (async/chan 100)
-     :db/batch-txs                          (async/chan 100)
-     :file/writes                           (async/chan 100)
+     :system/events                         (async/chan 1000)
+     :db/batch-txs                          (async/chan 1000)
+     :file/writes                           (async/chan 10000)
      :file/unlinked-dirs                    #{}
-     :reactive/custom-queries               (async/chan 100)
+     :reactive/custom-queries               (async/chan 1000)
      :notification/show?                    false
      :notification/content                  nil
      :repo/loading-files?                   {}
@@ -232,7 +232,9 @@
 
      :encryption/graph-parsing?             false
 
-     :ui/find-in-page                     nil
+     :ui/find-in-page                       nil
+     :graph/importing                       nil
+     :graph/importing-state                 {}
      })))
 
 ;; block uuid -> {content(String) -> ast}
@@ -1021,10 +1023,6 @@
   []
   (set-state! :ui/file-component nil))
 
-(defn get-file-component
-  []
-  (get @state :ui/file-component))
-
 (defn set-journals-length!
   [value]
   (when value
@@ -1174,11 +1172,6 @@
   []
   (:reactive/custom-queries @state))
 
-(defn get-write-chan-length
-  []
-  (let [c (get-file-write-chan)]
-    (count (gobj/get c "buf"))))
-
 (defn get-left-sidebar-open?
   []
   (get-in @state [:ui/left-sidebar-open?]))

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

@@ -52,6 +52,8 @@
   < rum/reactive
   {:did-mount (fn [state]
                 (let [^js el (rum/dom-node state)]
+                  ;; Passing aria-label as a prop to TextareaAutosize removes the dash
+                  (.setAttribute el "aria-label" "editing block")
                   (. el addEventListener "mouseup"
                      #(let [start (util/get-selection-start el)
                             end (util/get-selection-end el)]

+ 10 - 9
src/main/frontend/util.cljc

@@ -532,17 +532,18 @@
          (str prefix new-value)))
      s)))
 
-(defonce default-escape-chars "[]{}().+*?|")
+(defonce escape-chars "[]{}().+*?|")
+
+(defn escape-regex-chars
+  "Escapes characters in string `old-value"
+  [old-value]
+  (reduce (fn [acc escape-char]
+            (string/replace acc escape-char (str "\\" escape-char)))
+          old-value escape-chars))
 
 (defn replace-ignore-case
-  [s old-value new-value & [escape-chars]]
-  (let [escape-chars (or escape-chars default-escape-chars)
-        old-value (if (string? escape-chars)
-                    (reduce (fn [acc escape-char]
-                              (string/replace acc escape-char (str "\\" escape-char)))
-                            old-value escape-chars)
-                    old-value)]
-    (string/replace s (re-pattern (str "(?i)" old-value)) new-value)))
+  [s old-value new-value]
+  (string/replace s (re-pattern (str "(?i)" (escape-regex-chars old-value))) new-value))
 
 ;; copy from https://stackoverflow.com/questions/18735665/how-can-i-get-the-positions-of-regex-matches-in-clojurescript
 #?(:cljs

+ 36 - 13
src/test/frontend/handler/page_test.cljs

@@ -68,23 +68,46 @@
   (are [x y] (= (let [[content old-name new-name] x]
                   (page-handler/replace-tag-ref! content old-name new-name))
                 y)
-       ["#foo" "foo" "bar"] "#bar"
-       ["#foo" "foo" "new bar"] "#[[new bar]]"
+    ["#foo" "foo" "bar"] "#bar"
+    ["#foo" "foo" "new bar"] "#[[new bar]]"
 
-       ["bla #foo bla" "foo" "bar"] "bla #bar bla"
-       ["bla #foo bla" "foo" "new bar"] "bla #[[new bar]] bla"
+    ["bla #foo bla" "foo" "bar"] "bla #bar bla"
+    ["bla #foo bla" "foo" "new bar"] "bla #[[new bar]] bla"
 
-       ["bla #foo" "foo" "bar"] "bla #bar"
-       ["bla #foo" "foo" "new bar"] "bla #[[new bar]]"
+    ["bla #foo" "foo" "bar"] "bla #bar"
+    ["bla #foo" "foo" "new bar"] "bla #[[new bar]]"
 
-       ["#foo #foobar bar#foo #foo" "foo" "bar"]
-       "#bar #foobar bar#foo #bar"
-       
-       ["#foo #foobar bar#foo #foo" "foo" "new bar"]
-       "#[[new bar]] #foobar bar#foo #[[new bar]]"
+    ["#foo #foobar" "foo" "bar"]
+    "#bar #foobar"
 
-       ["#logseq/foo #logseq/foobar bar#logseq/foo #logseq/foo" "logseq/foo" "logseq/bar"]
-       "#logseq/bar #logseq/foobar bar#logseq/foo #logseq/bar"))
+    ["#foo #foobar bar#foo #foo" "foo" "bar"]
+    "#bar #foobar bar#foo #bar"
+
+    ["#foo #foobar bar#foo #foo,," "foo" "bar"]
+    "#bar #foobar bar#foo #bar,,"
+
+    ["#foo #foobar bar#foo #foo #foo ball" "foo" "bar"]
+    "#bar #foobar bar#foo #bar #bar ball"
+
+    ["#foo #foobar bar#foo #foo\t#foo ball" "foo" "bar"]
+    "#bar #foobar bar#foo #bar\t#bar ball"
+
+    ["#foo #foobar bar#foo #foo" "foo" "new bar"]
+    "#[[new bar]] #foobar bar#foo #[[new bar]]"
+
+    ["#logseq/foo #logseq/foobar bar#logseq/foo #logseq/foo" "logseq/foo" "logseq/bar"]
+    "#logseq/bar #logseq/foobar bar#logseq/foo #logseq/bar"
+
+    ;; #6451
+    ["#中文" "中文" "中文2"] "#中文2"
+    ["#2中文" "2中文" "中文234"] "#中文234"
+    ["#2中文2" "2中文2" "中文1999"] "#中文1999"
+    ["#2中文,SLKDF" "2中文" "中文1999"] "#2中文,SLKDF"
+    ["#2中文, SLKDF" "2中文" "中文1999"] "#中文1999, SLKDF"
+    ["#2中文看来减肥了" "2中文" "中文1999"] "#2中文看来减肥了"
+    ["两份健康 #2中文 看来减肥了" "2中文" "中文1999"] "两份健康 #中文1999 看来减肥了"
+    ["sdaflk  #2中文   看asdf了" "2中文" "中文1999"] "sdaflk  #中文1999   看asdf了"
+    ["sdaflk  #2中文" "2中文" "中文1999"] "sdaflk  #中文1999"))
 
 (deftest test-replace-old-page!
   (are [x y] (= (let [[content old-name new-name] x]

+ 18 - 6
templates/config.edn

@@ -141,12 +141,12 @@
  :default-queries
  {:journals
   [{:title "🔨 NOW"
-    :query [:find (pull ?b [*])
+    :query [:find (pull ?h [*])
             :in $ ?start ?today
             :where
-            [?b :block/marker ?marker]
+            [?h :block/marker ?marker]
             [(contains? #{"NOW" "DOING"} ?marker)]
-            [?b :block/page ?p]
+            [?h :block/page ?p]
             [?p :block/journal? true]
             [?p :block/journal-day ?d]
             [(>= ?d ?start)]
@@ -157,12 +157,12 @@
                                    (get h :block/priority "Z")) result))
     :collapsed? false}
    {:title "📅 NEXT"
-    :query [:find (pull ?b [*])
+    :query [:find (pull ?h [*])
             :in $ ?start ?next
             :where
-            [?b :block/marker ?marker]
+            [?h :block/marker ?marker]
             [(contains? #{"NOW" "LATER" "TODO"} ?marker)]
-            [?b :block/page ?p]
+            [?h :block/page ?p]
             [?p :block/journal? true]
             [?p :block/journal-day ?d]
             [(> ?d ?start)]
@@ -265,4 +265,16 @@
  ;; :quick-capture-templates
  ;; {:text "[[quick capture]] **{time}**: {text} from {url}"
  ;;  :media "[[quick capture]] **{time}**: {url}"}
+
+ ;; dwim (do what I mean) for Enter key when editing.
+ ;; Context-awareness of Enter key makes editing more easily
+ ; :dwim/settings {
+ ;   :admonition&src?  true
+ ;   :markup?          false
+ ;   :block-ref?       true
+ ;   :page-ref?        true
+ ;   :properties?      true
+ ;   :list?            true
+ ; }
+
  }

+ 12 - 0
yarn.lock

@@ -1221,6 +1221,18 @@ autoprefixer@^9.8.6:
     postcss "^7.0.32"
     postcss-value-parser "^4.1.0"
 
+axe-core@^4.0.1:
+  version "4.4.3"
+  resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.3.tgz#11c74d23d5013c0fa5d183796729bc3482bd2f6f"
+  integrity sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w==
+
+axe-playwright@^1.1.11:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/axe-playwright/-/axe-playwright-1.1.11.tgz#e57638f08d29b58d157a2aeb34cf81730eab2cff"
+  integrity sha512-YHmUouvF/dFNxoFFwbCjPFmEPwoJSzPgZsD0KZs3xjsR03Rf2mAh771ugre950MaBYuiyxYDlurH5BOEJBK34Q==
+  dependencies:
+    axe-core "^4.0.1"
+
 bach@^1.0.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/bach/-/bach-1.2.0.tgz#4b3ce96bf27134f79a1b414a51c14e34c3bd9880"