Quellcode durchsuchen

Merge branch 'whiteboards' into enhance/whiteboards-ui

Konstantinos Kaloutas vor 3 Jahren
Ursprung
Commit
9dbbc95ae1
50 geänderte Dateien mit 712 neuen und 604 gelöschten Zeilen
  1. 4 1
      android/app/src/main/java/com/logseq/app/FolderPicker.java
  2. 6 7
      android/app/src/main/java/com/logseq/app/FsWatcher.java
  3. 4 0
      deps/db/src/logseq/db/schema.cljs
  4. 15 10
      deps/graph-parser/src/logseq/graph_parser/block.cljs
  5. 1 1
      deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs
  6. 14 3
      deps/graph-parser/src/logseq/graph_parser/util.cljs
  7. 29 0
      docs/accessibility.md
  8. 4 0
      docs/dev-practices.md
  9. 12 0
      e2e-tests/accessibility.spec.ts
  10. 4 4
      ios/App/App.xcodeproj/project.pbxproj
  11. 1 0
      package.json
  12. 1 1
      resources/css/common.css
  13. 243 215
      src/main/frontend/components/block.cljs
  14. 6 7
      src/main/frontend/components/export.cljs
  15. 0 80
      src/main/frontend/components/external.cljs
  16. 8 4
      src/main/frontend/components/header.cljs
  17. 1 1
      src/main/frontend/components/header.css
  18. 5 5
      src/main/frontend/components/onboarding.cljs
  19. 0 1
      src/main/frontend/components/onboarding/index.css
  20. 51 55
      src/main/frontend/components/onboarding/setups.cljs
  21. 4 3
      src/main/frontend/components/reference.cljs
  22. 2 1
      src/main/frontend/components/right_sidebar.cljs
  23. 5 4
      src/main/frontend/components/search.cljs
  24. 14 7
      src/main/frontend/components/sidebar.cljs
  25. 15 0
      src/main/frontend/components/sidebar.css
  26. 9 5
      src/main/frontend/components/theme.cljs
  27. 1 0
      src/main/frontend/dicts.cljc
  28. 7 11
      src/main/frontend/fs/capacitor_fs.cljs
  29. 2 4
      src/main/frontend/fs/nfs.cljs
  30. 3 5
      src/main/frontend/fs/node.cljs
  31. 8 7
      src/main/frontend/handler.cljs
  32. 20 4
      src/main/frontend/handler/block.cljs
  33. 6 1
      src/main/frontend/handler/editor.cljs
  34. 1 0
      src/main/frontend/handler/events.cljs
  35. 17 6
      src/main/frontend/handler/export.cljs
  36. 54 27
      src/main/frontend/handler/external.cljs
  37. 20 44
      src/main/frontend/handler/file.cljs
  38. 8 3
      src/main/frontend/handler/page.cljs
  39. 0 6
      src/main/frontend/handler/ui.cljs
  40. 6 15
      src/main/frontend/modules/file/core.cljs
  41. 0 1
      src/main/frontend/modules/outliner/core.cljs
  42. 6 5
      src/main/frontend/modules/outliner/file.cljs
  43. 8 4
      src/main/frontend/modules/outliner/pipeline.cljs
  44. 7 9
      src/main/frontend/modules/shortcut/config.cljs
  45. 7 14
      src/main/frontend/state.cljs
  46. 2 0
      src/main/frontend/ui.cljs
  47. 10 9
      src/main/frontend/util.cljc
  48. 36 13
      src/test/frontend/handler/page_test.cljs
  49. 13 1
      templates/config.edn
  50. 12 0
      yarn.lock

+ 4 - 1
android/app/src/main/java/com/logseq/app/FolderPicker.java

@@ -20,6 +20,8 @@ import com.getcapacitor.annotation.CapacitorPlugin;
 import com.getcapacitor.PluginCall;
 import com.getcapacitor.PluginMethod;
 
+import java.io.File;
+
 
 @CapacitorPlugin(name = "FolderPicker")
 public class FolderPicker extends Plugin {
@@ -63,7 +65,8 @@ public class FolderPicker extends Plugin {
         if (path == null || path.isEmpty()) {
             call.reject("Cannot support this directory type: " + docUri);
         } else {
-            ret.put("path", "file://" + path);
+            Uri folderUri = Uri.fromFile(new File(path));
+            ret.put("path", folderUri.toString());
             call.resolve(ret);
         }
     }

+ 6 - 7
android/app/src/main/java/com/logseq/app/FsWatcher.java

@@ -58,7 +58,7 @@ public class FsWatcher extends Plugin {
 
             int mask = FileObserver.CLOSE_WRITE |
                     FileObserver.MOVE_SELF | FileObserver.MOVED_FROM | FileObserver.MOVED_TO |
-                    FileObserver.DELETE | FileObserver.DELETE_SELF;
+                    FileObserver.DELETE | FileObserver.DELETE_SELF | FileObserver.CREATE;
 
             if (observers != null) {
                 call.reject("already watching");
@@ -72,7 +72,7 @@ public class FsWatcher extends Plugin {
             if (files != null) {
                 for (File file : files) {
                     String filename = file.getName();
-                    if (file.isDirectory() && !filename.startsWith(".") && !filename.equals("bak") && !filename.equals("node_modules")) {
+                    if (file.isDirectory() && !filename.startsWith(".") && !filename.equals("bak") && !filename.equals("version-files") && !filename.equals("node_modules")) {
                         observers.add(new SingleFileObserver(file, mask));
                     }
                 }
@@ -90,7 +90,7 @@ public class FsWatcher extends Plugin {
 
     @PluginMethod()
     public void unwatch(PluginCall call) {
-        Log.i("FsWatcher", "unwatching...");
+        Log.i("FsWatcher", "unwatch all...");
 
         if (observers != null) {
             for (int i = 0; i < observers.size(); ++i)
@@ -129,11 +129,9 @@ public class FsWatcher extends Plugin {
     public void onObserverEvent(int event, String path) {
         JSObject obj = new JSObject();
         String content = null;
-        // FIXME: Current repo/path impl requires path to be a URL, dir to be a bare
-        // path.
         File f = new File(path);
         obj.put("path", Uri.fromFile(f));
-        obj.put("dir", "file://" + mPath);
+        obj.put("dir", Uri.fromFile(new File(mPath)));
 
         switch (event) {
             case FileObserver.CLOSE_WRITE:
@@ -223,7 +221,8 @@ public class FsWatcher extends Plugin {
         @Override
         public void onEvent(int event, String path) {
             if (path != null && !path.equals("graphs-txid.edn") && !path.equals("broken-config.edn")) {
-                Log.d("FsWatcher", "got path=" + path + " event=" + event);
+                Log.d("FsWatcher", "got path=" + mPath + "/" + path + " event=" + event);
+                // TODO: handle newly created directory
                 if (Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown)$", path)) {
                     String fullPath = mPath + "/" + path;
                     if (event == FileObserver.MOVE_SELF || event == FileObserver.MOVED_FROM ||

+ 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);
 }

+ 243 - 215
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?))
@@ -2477,7 +2524,8 @@
         *navigating-block (get state ::navigating-block)
         navigating-block (rum/react *navigating-block)
         navigated? (and (not= (:block/uuid block) navigating-block) navigating-block)
-        block (if navigated?
+        block (if (or (and custom-query? (empty? (:block/children block)))
+                      navigated?)
                 (let [block (db/pull [:block/uuid navigating-block])
                       blocks (db/get-paginated-blocks repo (:db/id block)
                                                       {:scoped-block-id (:db/id block)})
@@ -2606,7 +2654,7 @@
                     ::navigating-block (atom (:block/uuid block)))))
    :should-update (fn [old-state new-state]
                     (let [compare-keys [:block/uuid :block/content :block/parent :block/collapsed?
-                                        :block/properties :block/left :block/children :block/_refs]
+                                        :block/properties :block/left :block/children :block/_refs :block/bottom? :block/top?]
                           config-compare-keys [:show-cloze?]
                           b1 (second (:rum/args old-state))
                           b2 (second (:rum/args new-state))
@@ -3066,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"

+ 4 - 3
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
@@ -200,7 +200,8 @@
           filters (when (seq filter-state)
                     (-> (group-by second filter-state)
                         (update-vals #(map first %))))
-          filtered-ref-blocks (block-handler/filter-blocks ref-blocks filters)
+          filtered-ref-blocks (->> (block-handler/filter-blocks ref-blocks filters)
+                                   (block-handler/get-filtered-ref-blocks-with-parents ref-blocks))
           total (count top-level-blocks)
           filtered-top-blocks (filter (fn [b] (top-level-blocks-ids (:db/id b))) filtered-ref-blocks)
           filter-n (count filtered-top-blocks)
@@ -274,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

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

@@ -347,10 +347,11 @@
                             (let [page data]
                               (when (string? page)
                                 (when-let [page (db/pull [:block/name (util/page-name-sanity-lc page)])]
-                                  (state/sidebar-add-block!
-                                   (state/get-current-repo)
-                                   (:db/id page)
-                                   :page))))
+                                 (state/sidebar-add-block!
+                                  (state/get-current-repo)
+                                  (:db/id page)
+                                  :page))
+                                (state/close-modal!)))
 
                             nil))
        :item-render (fn [{:keys [type data]}]

+ 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!"

+ 7 - 11
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]))
 
@@ -275,12 +273,10 @@
     (readdir dir))
   (unlink! [this repo path _opts]
     (p/let [path (get-file-path nil path)
-            path (if (string/starts-with? path "file://")
-                   (string/replace-first path "file://" "")
-                   path)
-            repo-dir (config/get-local-dir repo)
-            recycle-dir (str repo-dir config/app-name "/.recycle") ;; logseq/.recycle
-            file-name (-> (string/replace path repo-dir "")
+            repo-url (config/get-local-dir repo)
+            recycle-dir (str repo-url config/app-name "/.recycle") ;; logseq/.recycle
+            ;; convert url to pure path
+            file-name (-> (string/replace path repo-url "")
                           (string/replace "/" "_")
                           (string/replace "\\" "_"))
             new-path (str recycle-dir "/" file-name)]

+ 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?)

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

@@ -249,14 +249,17 @@
 
 (defn get-blocks-refed-pages
   [aliases ref-blocks]
-  (let [refs (->> (mapcat (fn [b] (conj (:block/path-refs b) (:block/page b))) ref-blocks)
-                  distinct
-                  (remove #(aliases (:db/id %))))]
+  (let [refs (->> (mapcat :block/refs ref-blocks)
+                  (remove #(aliases (:db/id %))))
+        pages (->> (map :block/page ref-blocks)
+                   (distinct)
+                   (remove #(aliases (:db/id %))))
+        all-refs (concat pages refs)]
     (keep (fn [ref]
             (when (:block/name ref)
               {:db/id (:db/id ref)
                :block/name (:block/name ref)
-               :block/original-name (:block/original-name ref)})) refs)))
+               :block/original-name (:block/original-name ref)})) all-refs)))
 
 (defn filter-blocks
   [ref-blocks filters]
@@ -276,3 +279,16 @@
         (filter (fn [block]
                   (let [ids (set (map :db/id (:block/path-refs block)))]
                     (set/subset? include-ids ids))))))))
+
+(defn get-filtered-ref-blocks-with-parents
+  [all-ref-blocks filtered-ref-blocks]
+  (when (seq filtered-ref-blocks)
+    (let [id->block (zipmap (map :db/id all-ref-blocks) all-ref-blocks)
+          get-parents (fn [block]
+                        (loop [block block
+                               result [block]]
+                          (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]

+ 0 - 1
src/main/frontend/modules/outliner/core.cljs

@@ -215,7 +215,6 @@
                          [:db/retract id :block/alias]
                          [:db/retract id :block/tags]])))]
       (swap! txs-state concat txs page-tx)
-      (util/pprint @txs-state)
       block-id))
 
   (-get-children [this]

+ 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 - 9
src/main/frontend/modules/shortcut/config.cljs

@@ -250,18 +250,16 @@
                                                 (route-handler/go-to-search! :global))}
 
    :go/electron-find-in-page       {:binding "mod+f"
-                                    :fn      #(when (util/electron?)
-                                                (search-handler/open-find-in-page!))}
-
+                                    :inactive (not (util/electron?))
+                                    :fn      #(search-handler/open-find-in-page!)}
+   
    :go/electron-jump-to-the-next {:binding ["enter" "mod+g"]
-                                    :fn      (fn [_state _e]
-                                               (when (util/electron?)
-                                                 (search-handler/loop-find-in-page! false)))}
+                                  :inactive (not (util/electron?))
+                                  :fn      #(search-handler/loop-find-in-page! false)}
 
    :go/electron-jump-to-the-previous {:binding ["shift+enter" "mod+shift+g"]
-                                             :fn      (fn [_state _e]
-                                                        (when (util/electron?)
-                                                          (search-handler/loop-find-in-page! true)))}
+                                      :inactive (not (util/electron?))
+                                      :fn      #(search-handler/loop-find-in-page! true)}
 
    :go/journals                    {:binding "g j"
                                     :fn      route-handler/go-to-journals!}

+ 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]

+ 13 - 1
templates/config.edn

@@ -162,7 +162,7 @@
             :where
             [?h :block/marker ?marker]
             [(contains? #{"NOW" "LATER" "TODO"} ?marker)]
-            [?h :block/ref-pages ?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"