浏览代码

Merge branch 'whiteboards' into enhance/whiteboards-ui

Konstantinos Kaloutas 3 年之前
父节点
当前提交
9dbbc95ae1
共有 50 个文件被更改,包括 712 次插入604 次删除
  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.PluginCall;
 import com.getcapacitor.PluginMethod;
 import com.getcapacitor.PluginMethod;
 
 
+import java.io.File;
+
 
 
 @CapacitorPlugin(name = "FolderPicker")
 @CapacitorPlugin(name = "FolderPicker")
 public class FolderPicker extends Plugin {
 public class FolderPicker extends Plugin {
@@ -63,7 +65,8 @@ public class FolderPicker extends Plugin {
         if (path == null || path.isEmpty()) {
         if (path == null || path.isEmpty()) {
             call.reject("Cannot support this directory type: " + docUri);
             call.reject("Cannot support this directory type: " + docUri);
         } else {
         } else {
-            ret.put("path", "file://" + path);
+            Uri folderUri = Uri.fromFile(new File(path));
+            ret.put("path", folderUri.toString());
             call.resolve(ret);
             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 |
             int mask = FileObserver.CLOSE_WRITE |
                     FileObserver.MOVE_SELF | FileObserver.MOVED_FROM | FileObserver.MOVED_TO |
                     FileObserver.MOVE_SELF | FileObserver.MOVED_FROM | FileObserver.MOVED_TO |
-                    FileObserver.DELETE | FileObserver.DELETE_SELF;
+                    FileObserver.DELETE | FileObserver.DELETE_SELF | FileObserver.CREATE;
 
 
             if (observers != null) {
             if (observers != null) {
                 call.reject("already watching");
                 call.reject("already watching");
@@ -72,7 +72,7 @@ public class FsWatcher extends Plugin {
             if (files != null) {
             if (files != null) {
                 for (File file : files) {
                 for (File file : files) {
                     String filename = file.getName();
                     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));
                         observers.add(new SingleFileObserver(file, mask));
                     }
                     }
                 }
                 }
@@ -90,7 +90,7 @@ public class FsWatcher extends Plugin {
 
 
     @PluginMethod()
     @PluginMethod()
     public void unwatch(PluginCall call) {
     public void unwatch(PluginCall call) {
-        Log.i("FsWatcher", "unwatching...");
+        Log.i("FsWatcher", "unwatch all...");
 
 
         if (observers != null) {
         if (observers != null) {
             for (int i = 0; i < observers.size(); ++i)
             for (int i = 0; i < observers.size(); ++i)
@@ -129,11 +129,9 @@ public class FsWatcher extends Plugin {
     public void onObserverEvent(int event, String path) {
     public void onObserverEvent(int event, String path) {
         JSObject obj = new JSObject();
         JSObject obj = new JSObject();
         String content = null;
         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);
         File f = new File(path);
         obj.put("path", Uri.fromFile(f));
         obj.put("path", Uri.fromFile(f));
-        obj.put("dir", "file://" + mPath);
+        obj.put("dir", Uri.fromFile(new File(mPath)));
 
 
         switch (event) {
         switch (event) {
             case FileObserver.CLOSE_WRITE:
             case FileObserver.CLOSE_WRITE:
@@ -223,7 +221,8 @@ public class FsWatcher extends Plugin {
         @Override
         @Override
         public void onEvent(int event, String path) {
         public void onEvent(int event, String path) {
             if (path != null && !path.equals("graphs-txid.edn") && !path.equals("broken-config.edn")) {
             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)) {
                 if (Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown)$", path)) {
                     String fullPath = mPath + "/" + path;
                     String fullPath = mPath + "/" + path;
                     if (event == FileObserver.MOVE_SELF || event == FileObserver.MOVED_FROM ||
                     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/heading-level
     :block/type
     :block/type
     :block/properties
     :block/properties
+    :block/properties-order
+    :block/invalid-properties
     :block/created-at
     :block/created-at
     :block/updated-at
     :block/updated-at
     :block/warning
     :block/warning
@@ -135,5 +137,7 @@
     :block/format
     :block/format
     :block/content
     :block/content
     :block/properties
     :block/properties
+    :block/properties-order
+    :block/invalid-properties
     :block/alias
     :block/alias
     :block/tags})
     :block/tags})

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

@@ -199,22 +199,19 @@
          (remove string/blank?)
          (remove string/blank?)
          distinct)))
          distinct)))
 
 
-(defn- invalid-property-key?
-  [s]
-  (string/includes? s "`"))
-
 (defn extract-properties
 (defn extract-properties
   [format properties user-config]
   [format properties user-config]
   (when (seq properties)
   (when (seq properties)
     (let [properties (seq properties)
     (let [properties (seq properties)
           page-refs (get-page-ref-names-from-properties format properties user-config)
           page-refs (get-page-ref-names-from-properties format properties user-config)
+          *invalid-properties (atom #{})
           properties (->> properties
           properties (->> properties
                           (map (fn [[k v]]
                           (map (fn [[k v]]
                                  (let [k (-> (string/lower-case (name k))
                                  (let [k (-> (string/lower-case (name k))
                                              (string/replace " " "-")
                                              (string/replace " " "-")
                                              (string/replace "_" "-")
                                              (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)
                                      (let [k (if (contains? #{"custom_id" "custom-id"} k)
                                                "id"
                                                "id"
                                                k)
                                                k)
@@ -235,10 +232,14 @@
                                                (set [v])
                                                (set [v])
                                                v)
                                                v)
                                            v (if (coll? v) (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)
        :properties-order (map first properties)
+       :invalid-properties @*invalid-properties
        :page-refs page-refs})))
        :page-refs page-refs})))
 
 
 (defn- paragraph-timestamp-block?
 (defn- paragraph-timestamp-block?
@@ -533,7 +534,7 @@
                  (cons
                  (cons
                   (merge
                   (merge
                    (let [content (utf8/substring encoded-content 0 first-block-start-pos)
                    (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})
                          id (get-custom-id-or-new-id {:properties properties})
                          property-refs (->> (get-page-refs-from-properties format properties db date-formatter user-config)
                          property-refs (->> (get-page-refs-from-properties format properties db date-formatter user-config)
                                             (map :block/original-name))
                                             (map :block/original-name))
@@ -542,6 +543,7 @@
                                 :level 1
                                 :level 1
                                 :properties properties
                                 :properties properties
                                 :properties-order (vec properties-order)
                                 :properties-order (vec properties-order)
+                                :invalid-properties invalid-properties
                                 :refs property-refs
                                 :refs property-refs
                                 :pre-block? true
                                 :pre-block? true
                                 :unordered true
                                 :unordered true
@@ -578,7 +580,10 @@
                 (assoc :properties (:properties properties))
                 (assoc :properties (:properties properties))
 
 
                 (seq (:properties-order 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])
         block (if (get-in block [:properties :collapsed])
                 (-> (assoc block :collapsed? true)
                 (-> (assoc block :collapsed? true)
                     (update :properties (fn [m] (dissoc m :collapsed)))
                     (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
   ;; only increase over time as the docs graph rarely has deletions
   (testing "Counts"
   (testing "Counts"
     (is (= 211 (count files)) "Correct file count")
     (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
     (is (= 3600
            (ffirst
            (ffirst

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

@@ -1,10 +1,11 @@
 (ns logseq.graph-parser.util
 (ns logseq.graph-parser.util
   "Util fns shared between graph-parser and rest of app. Util fns only rely on
   "Util fns shared between graph-parser and rest of app. Util fns only rely on
   clojure standard libraries."
   clojure standard libraries."
-  (:require [clojure.walk :as walk]
+  (:require [cljs.reader :as reader]
+            [clojure.edn :as edn]
             [clojure.string :as string]
             [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
 (defn path-normalize
   "Normalize file path (for reading paths from FS, not required by writting)"
   "Normalize file path (for reading paths from FS, not required by writting)"
@@ -150,6 +151,16 @@
   (when file
   (when file
     (normalize-format (keyword (string/lower-case (last (string/split 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
 (defn safe-read-string
   [content]
   [content]
   (try
   (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`.
 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
 ## Logging
 
 
 For logging, we use https://github.com/lambdaisland/glogi. When in development,
 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;
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				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\"";
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -568,7 +568,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				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_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -593,7 +593,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
 				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_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
@@ -620,7 +620,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
 				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;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 1 - 0
package.json

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

+ 1 - 1
resources/css/common.css

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

+ 243 - 215
src/main/frontend/components/block.cljs

@@ -71,6 +71,7 @@
             [promesa.core :as p]
             [promesa.core :as p]
             [reitit.frontend.easy :as rfe]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]
             [rum.core :as rum]
+            [clojure.set :as set]
             [shadow.loader :as loader]))
             [shadow.loader :as loader]))
 
 
 (defn safe-read-string
 (defn safe-read-string
@@ -237,7 +238,7 @@
                                                   (editor-handler/delete-asset-of-block!
                                                   (editor-handler/delete-asset-of-block!
                                                    {:block-id    block-id
                                                    {:block-id    block-id
                                                     :local?      local?
                                                     :local?      local?
-                                                    :delete-local? (first sub-selected)
+                                                    :delete-local? (and sub-selected (first sub-selected))
                                                     :repo        (state/get-current-repo)
                                                     :repo        (state/get-current-repo)
                                                     :href        src
                                                     :href        src
                                                     :title       title
                                                     :title       title
@@ -424,6 +425,27 @@
 
 
 (declare page-reference)
 (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
 (rum/defc page-inner
   "The inner div of page reference component
   "The inner div of page reference component
 
 
@@ -1861,41 +1883,53 @@
        :else
        :else
        (inline-text config (:block/format block) (str v)))]))
        (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
 (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
     (cond
       (seq properties)
       (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)
       (and pre-block? properties)
       [:span.opacity-50 "Properties"]
       [:span.opacity-50 "Properties"]
@@ -1903,6 +1937,16 @@
       :else
       :else
       nil)))
       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/defcs timestamp-cp < rum/reactive
   (rum/local false ::show?)
   (rum/local false ::show?)
   (rum/local {} ::pos)
   (rum/local {} ::pos)
@@ -2109,6 +2153,9 @@
         (when-let [scheduled-ast (block-handler/get-scheduled-ast block)]
         (when-let [scheduled-ast (block-handler/get-scheduled-ast block)]
           (timestamp-cp block "SCHEDULED" scheduled-ast)))
           (timestamp-cp block "SCHEDULED" scheduled-ast)))
 
 
+      (when-let [invalid-properties (:block/invalid-properties block)]
+        (invalid-properties-cp invalid-properties))
+
       (when (and (seq properties)
       (when (and (seq properties)
                  (let [hidden? (property/properties-hidden? properties)]
                  (let [hidden? (property/properties-hidden? properties)]
                    (not hidden?))
                    (not hidden?))
@@ -2477,7 +2524,8 @@
         *navigating-block (get state ::navigating-block)
         *navigating-block (get state ::navigating-block)
         navigating-block (rum/react *navigating-block)
         navigating-block (rum/react *navigating-block)
         navigated? (and (not= (:block/uuid block) navigating-block) 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])
                 (let [block (db/pull [:block/uuid navigating-block])
                       blocks (db/get-paginated-blocks repo (:db/id block)
                       blocks (db/get-paginated-blocks repo (:db/id block)
                                                       {:scoped-block-id (:db/id block)})
                                                       {:scoped-block-id (:db/id block)})
@@ -2606,7 +2654,7 @@
                     ::navigating-block (atom (:block/uuid block)))))
                     ::navigating-block (atom (:block/uuid block)))))
    :should-update (fn [old-state new-state]
    :should-update (fn [old-state new-state]
                     (let [compare-keys [:block/uuid :block/content :block/parent :block/collapsed?
                     (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?]
                           config-compare-keys [:show-cloze?]
                           b1 (second (:rum/args old-state))
                           b1 (second (:rum/args old-state))
                           b2 (second (:rum/args new-state))
                           b2 (second (:rum/args new-state))
@@ -3066,192 +3114,172 @@
 
 
 (defn ^:large-vars/cleanup-todo markup-element-cp
 (defn ^:large-vars/cleanup-todo markup-element-cp
   [{:keys [html-export?] :as config} item]
   [{: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?
         (if html-export?
           (latex/html-export content true true)
           (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
 (defn markup-elements-cp
   [config col]
   [config col]

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

@@ -12,8 +12,13 @@
   (when-let [current-repo (state/get-current-repo)]
   (when-let [current-repo (state/get-current-repo)]
     [:div.export
     [:div.export
      [:h1.title "Export"]
      [:h1.title "Export"]
-
      [:ul.mr-1
      [: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?)
       (when (util/electron?)
         [:li.mb-4
         [:li.mb-4
          [:a.font-medium {:on-click #(export/export-repo-as-html! current-repo)}
          [:a.font-medium {:on-click #(export/export-repo-as-html! current-repo)}
@@ -25,12 +30,6 @@
         [:li.mb-4
         [:li.mb-4
          [:a.font-medium {:on-click #(export/export-repo-as-opml! current-repo)}
          [:a.font-medium {:on-click #(export/export-repo-as-opml! current-repo)}
           (t :export-opml)]])
           (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?)
       (when-not (mobile-util/native-platform?)
        [:li.mb-4
        [:li.mb-4
         [:a.font-medium {:on-click #(export/export-repo-as-roam-json! current-repo)}
         [: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 []
 (rum/defc home-button []
   (ui/with-shortcut :go/home "left"
   (ui/with-shortcut :go/home "left"
     [:button.button.icon.inline
     [:button.button.icon.inline
-     {:on-click #(do
+     {:title "Home"
+      :on-click #(do
                    (when (mobile-util/native-iphone?)
                    (when (mobile-util/native-iphone?)
                      (state/set-left-sidebar-open! false))
                      (state/set-left-sidebar-open! false))
                    (route-handler/redirect-to-home!))}
                    (route-handler/redirect-to-home!))}
@@ -131,7 +132,8 @@
   [{:keys [on-click]}]
   [{:keys [on-click]}]
   (ui/with-shortcut :ui/toggle-left-sidebar "bottom"
   (ui/with-shortcut :ui/toggle-left-sidebar "bottom"
     [:button.#left-menu.cp__header-left-menu.button.icon
     [: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}})]))
       (ui/icon "menu-2" {:style {:fontSize ui/icon-size}})]))
 
 
 (rum/defc dropdown-menu < rum/reactive
 (rum/defc dropdown-menu < rum/reactive
@@ -142,7 +144,8 @@
     (ui/dropdown-with-links
     (ui/dropdown-with-links
      (fn [{:keys [toggle-fn]}]
      (fn [{:keys [toggle-fn]}]
        [:button.button.icon
        [:button.button.icon
-        {:on-click toggle-fn}
+        {:title "More"
+         :on-click toggle-fn}
         (ui/icon "dots" {:style {:fontSize ui/icon-size}})])
         (ui/icon "dots" {:style {:fontSize ui/icon-size}})])
      (->>
      (->>
       [(when (state/enable-editing?)
       [(when (state/enable-editing?)
@@ -249,7 +252,8 @@
          (when current-repo ;; this is for the Search button
          (when current-repo ;; this is for the Search button
            (ui/with-shortcut :go/search "right"
            (ui/with-shortcut :go/search "right"
              [:button.button.icon#search-button
              [: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?))
                                         (mobile-util/native-iphone?))
                                 (state/set-left-sidebar-open! false))
                                 (state/set-left-sidebar-open! false))
                               (state/pub-event! [:go/search]))}
                               (state/pub-event! [:go/search]))}

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

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

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

@@ -13,8 +13,8 @@
   []
   []
   [:div.help.cp__sidebar-help-docs
   [:div.help.cp__sidebar-help-docs
    (let [discourse-with-icon [:div.flex-row.inline-flex.items-center
    (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
          list
          [{:title "Usage"
          [{:title "Usage"
            :children [[[:a
            :children [[[:a
@@ -25,7 +25,7 @@
                       [(t :help/docs) "https://docs.logseq.com/"]
                       [(t :help/docs) "https://docs.logseq.com/"]
                       [(t :help/start) "https://docs.logseq.com/#/page/tutorial"]
                       [(t :help/start) "https://docs.logseq.com/#/page/tutorial"]
                       ["FAQ" "https://docs.logseq.com/#/page/faq"]]}
                       ["FAQ" "https://docs.logseq.com/#/page/faq"]]}
-          
+
           {:title "Community"
           {:title "Community"
            :children [[(t :help/awesome-logseq) "https://github.com/logseq/awesome-logseq"]
            :children [[(t :help/awesome-logseq) "https://github.com/logseq/awesome-logseq"]
                       [(t :help/blog) "https://blog.logseq.com"]
                       [(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/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/feature) "https://discuss.logseq.com/c/feature-requests/"]
                       [(t :help/changelog) "https://docs.logseq.com/#/page/changelog"]]}
                       [(t :help/changelog) "https://docs.logseq.com/#/page/changelog"]]}
-          
+
           {:title "About"
           {:title "About"
            :children [[(t :help/about) "https://logseq.com/blog/about"]]}
            :children [[(t :help/about) "https://logseq.com/blog/about"]]}
 
 
@@ -44,7 +44,7 @@
            :children [[(t :help/privacy) "https://logseq.com/blog/privacy-policy"]
            :children [[(t :help/privacy) "https://logseq.com/blog/privacy-policy"]
                       [(t :help/terms) "https://logseq.com/blog/terms"]]}]]
                       [(t :help/terms) "https://logseq.com/blog/terms"]]}]]
 
 
-          
+
 
 
      (map (fn [sublist]
      (map (fn [sublist]
             [[:p.mt-4.mb-1 [:b (:title 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 {
       small {
         font-size: 11px;
         font-size: 11px;
-        text-align: center;
       }
       }
 
 
       &:hover {
       &:hover {

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

@@ -2,9 +2,11 @@
   (:require [frontend.state :as state]
   (:require [frontend.state :as state]
             [rum.core :as rum]
             [rum.core :as rum]
             [frontend.ui :as ui]
             [frontend.ui :as ui]
+            [frontend.context.i18n :refer [t]]
             [frontend.components.svg :as svg]
             [frontend.components.svg :as svg]
             [frontend.handler.page :as page-handler]
             [frontend.handler.page :as page-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.route :as route-handler]
+            [frontend.handler.ui :as ui-handler]
             [frontend.util :as util]
             [frontend.util :as util]
             [frontend.handler.web.nfs :as nfs]
             [frontend.handler.web.nfs :as nfs]
             [frontend.mobile.util :as mobile-util]
             [frontend.mobile.util :as mobile-util]
@@ -126,15 +128,13 @@
               [:strong.uppercase title]
               [:strong.uppercase title]
               [:small.opacity-50 label]]]))]]])))
               [: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))
 (defonce *opml-imported-pages (atom nil))
 
 
 (defn- finished-cb
 (defn- finished-cb
   []
   []
+  (route-handler/redirect-to-home!)
   (notification/show! "Import finished!" :success)
   (notification/show! "Import finished!" :success)
-  (route-handler/redirect-to-home!))
+  (ui-handler/re-render-root!))
 
 
 (defn- roam-import-handler
 (defn- roam-import-handler
   [e]
   [e]
@@ -142,13 +142,16 @@
         file-name (gobj/get file "name")]
         file-name (gobj/get file "name")]
     (if (string/ends-with? file-name ".json")
     (if (string/ends-with? file-name ".json")
       (do
       (do
-        (reset! *roam-importing? true)
+        (state/set-state! :graph/importing :roam-json)
         (let [reader (js/FileReader.)]
         (let [reader (js/FileReader.)]
           (set! (.-onload reader)
           (set! (.-onload reader)
                 (fn [e]
                 (fn [e]
                   (let [text (.. e -target -result)]
                   (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)))
           (.readAsText reader file)))
       (notification/show! "Please choose a JSON file."
       (notification/show! "Please choose a JSON file."
                           :error))))
                           :error))))
@@ -157,32 +160,27 @@
   [e]
   [e]
   (let [file      (first (array-seq (.-files (.-target e))))
   (let [file      (first (array-seq (.-files (.-target e))))
         file-name (some-> (gobj/get file "name")
         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
 (defn- opml-import-handler
   [e]
   [e]
@@ -190,7 +188,7 @@
         file-name (gobj/get file "name")]
         file-name (gobj/get file "name")]
     (if (string/ends-with? file-name ".opml")
     (if (string/ends-with? file-name ".opml")
       (do
       (do
-        (reset! *opml-importing? true)
+        (state/set-state! :graph/importing :opml)
         (let [reader (js/FileReader.)]
         (let [reader (js/FileReader.)]
           (set! (.-onload reader)
           (set! (.-onload reader)
                 (fn [e]
                 (fn [e]
@@ -198,7 +196,7 @@
                     (external-handler/import-from-opml! text
                     (external-handler/import-from-opml! text
                                                         (fn [pages]
                                                         (fn [pages]
                                                           (reset! *opml-imported-pages pages)
                                                           (reset! *opml-imported-pages pages)
-                                                          (reset! *opml-importing? false)
+                                                          (state/set-state! :graph/importing nil)
                                                           (finished-cb))))))
                                                           (finished-cb))))))
           (.readAsText reader file)))
           (.readAsText reader file)))
       (notification/show! "Please choose a OPML file."
       (notification/show! "Please choose a OPML file."
@@ -206,11 +204,18 @@
 
 
 (rum/defc importer < rum/reactive
 (rum/defc importer < rum/reactive
   [{:keys [query-params]}]
   [{: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
     (setups-container
      :importer
      :importer
      [:article.flex.flex-col.items-center.importer.py-16.px-8
      [: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."]]
        [:h2 "If they are in a JSON, EDN or Markdown format Logseq can work with them."]]
       [:section.d.md:flex
       [:section.d.md:flex
        [:label.action-input.flex.items-center.mx-2.my-2
        [:label.action-input.flex.items-center.mx-2.my-2
-        {:disabled importing?}
         [:span.as-flex-center [:i (svg/roam-research 28)]]
         [:span.as-flex-center [:i (svg/roam-research 28)]]
         [:div.flex.flex-col
         [: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
         [:input.absolute.hidden
          {:id        "import-roam"
          {:id        "import-roam"
           :type      "file"
           :type      "file"
           :on-change roam-import-handler}]]
           :on-change roam-import-handler}]]
 
 
        [:label.action-input.flex.items-center.mx-2.my-2
        [:label.action-input.flex.items-center.mx-2.my-2
-        {:disabled importing?}
         [:span.as-flex-center [:i (svg/logo 28)]]
         [:span.as-flex-center [:i (svg/logo 28)]]
         [:span.flex.flex-col
         [: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
         [:input.absolute.hidden
          {:id        "import-lsq"
          {:id        "import-lsq"
           :type      "file"
           :type      "file"
           :on-change lsq-import-handler}]]
           :on-change lsq-import-handler}]]
 
 
        [:label.action-input.flex.items-center.mx-2.my-2
        [:label.action-input.flex.items-center.mx-2.my-2
-        {:disabled importing?}
         [:span.as-flex-center (ui/icon "sitemap" {:style {:fontSize "26px"}})]
         [:span.as-flex-center (ui/icon "sitemap" {:style {:fontSize "26px"}})]
         [:span.flex.flex-col
         [: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
         [:input.absolute.hidden
          {:id        "import-opml"
          {:id        "import-opml"

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

@@ -127,7 +127,7 @@
         *collapsed? (atom nil)]
         *collapsed? (atom nil)]
     (ui/foldable
     (ui/foldable
      [:div.flex.flex-row.flex-1.justify-between.items-center
      [: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)
                                  (when (seq filters)
                                    (str filter-n " of "))
                                    (str filter-n " of "))
                                  total
                                  total
@@ -200,7 +200,8 @@
           filters (when (seq filter-state)
           filters (when (seq filter-state)
                     (-> (group-by second filter-state)
                     (-> (group-by second filter-state)
                         (update-vals #(map first %))))
                         (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)
           total (count top-level-blocks)
           filtered-top-blocks (filter (fn [b] (top-level-blocks-ids (:db/id b))) filtered-ref-blocks)
           filtered-top-blocks (filter (fn [b] (top-level-blocks-ids (:db/id b))) filtered-ref-blocks)
           filter-n (count filtered-top-blocks)
           filter-n (count filtered-top-blocks)
@@ -274,7 +275,7 @@
         [:div.references.mt-6.flex-1.flex-row
         [:div.references.mt-6.flex-1.flex-row
          [:div.content.flex-1
          [:div.content.flex-1
           (ui/foldable
           (ui/foldable
-           [:h2.font-bold {:style {:opacity "0.3"}}
+           [:h2.font-bold.opacity-80
             (if @n-ref
             (if @n-ref
               (str @n-ref " Unlinked Reference" (when (> @n-ref 1)
               (str @n-ref " Unlinked Reference" (when (> @n-ref 1)
                                                   "s"))
                                                   "s"))

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

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

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

@@ -347,10 +347,11 @@
                             (let [page data]
                             (let [page data]
                               (when (string? page)
                               (when (string? page)
                                 (when-let [page (db/pull [:block/name (util/page-name-sanity-lc 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))
                             nil))
        :item-render (fn [{:keys [type data]}]
        :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"])
                                [".favorites .bd" ".recent .bd" ".dropdown-wrapper" ".nav-header"])
                      (close-modal-fn)))}
                      (close-modal-fn)))}
      [:div.flex.flex-col.pb-4.wrap.gap-4
      [: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)
        (repo/repos-dropdown)
 
 
        [:div.nav-header.flex.gap-1.flex-col
        [:div.nav-header.flex.gap-1.flex-col
@@ -283,8 +283,7 @@
       (when (and left-sidebar-open? (not config/publishing?)) (recent-pages t))
       (when (and left-sidebar-open? (not config/publishing?)) (recent-pages t))
 
 
       (when-not (mobile-util/native-platform?)
       (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?
          (when-not config/publishing?
            [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md.new-page-link
            [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md.new-page-link
             {:on-click (fn []
             {:on-click (fn []
@@ -346,7 +345,9 @@
                     :route-match route-match})
                     :route-match route-match})
 
 
      [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row
      [: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?)
       (when (util/electron?)
         (find-in-page/search))
         (find-in-page/search))
@@ -593,7 +594,8 @@
         default-home (get-default-home-if-valid)
         default-home (get-default-home-if-valid)
         logged? (user-handler/logged-in?)
         logged? (user-handler/logged-in?)
         show-action-bar? (state/sub :mobile/show-action-bar?)
         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
     (theme/container
      {:t             t
      {:t             t
       :theme         theme
       :theme         theme
@@ -606,15 +608,20 @@
       :settings-open? settings-open?
       :settings-open? settings-open?
       :sidebar-blocks-len (count right-sidebar-blocks)
       :sidebar-blocks-len (count right-sidebar-blocks)
       :system-theme? system-theme?
       :system-theme? system-theme?
+      :preferred-language preferred-language
       :on-click      (fn [e]
       :on-click      (fn [e]
                        (editor-handler/unhighlight-blocks!)
                        (editor-handler/unhighlight-blocks!)
                        (util/fix-open-external-with-shift! e))}
                        (util/fix-open-external-with-shift! e))}
 
 
-     [:div.theme-inner
+     [:main.theme-inner
       {:class (util/classnames [{:ls-left-sidebar-open left-sidebar-open?
       {:class (util/classnames [{:ls-left-sidebar-open left-sidebar-open?
                                  :ls-right-sidebar-open sidebar-open?
                                  :ls-right-sidebar-open sidebar-open?
                                  :ls-wide-mode wide-mode?}])}
                                  :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.#app-container
        [:div#left-container
        [:div#left-container
         {:class (if (state/sub :ui/sidebar-open?) "overflow-hidden" "w-full")}
         {: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%;
   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 {
 #left-container {
   @apply flex flex-1 flex-col relative h-screen;
   @apply flex flex-1 flex-col relative h-screen;
 }
 }

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

@@ -13,7 +13,7 @@
 
 
 (rum/defc container
 (rum/defc container
   [{:keys [route theme on-click current-repo nfs-granted? db-restoring?
   [{: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)
   (let [mounted-fn (use-mounted)
         [restored-sidebar? set-restored-sidebar?] (rum/use-state false)]
         [restored-sidebar? set-restored-sidebar?] (rum/use-state false)]
 
 
@@ -28,6 +28,10 @@
         (plugin-handler/hook-plugin-app :theme-mode-changed {:mode theme} nil))
         (plugin-handler/hook-plugin-app :theme-mode-changed {:mode theme} nil))
      [theme])
      [theme])
 
 
+    (rum/use-effect!
+     #(let [doc js/document.documentElement]
+        (.setAttribute doc "lang" preferred-language)))
+
     (rum/use-effect!
     (rum/use-effect!
      #(when (and restored-sidebar?
      #(when (and restored-sidebar?
                  (mounted-fn))
                  (mounted-fn))
@@ -68,10 +72,10 @@
                     config/publishing?
                     config/publishing?
                     ;; other graphs exists
                     ;; other graphs exists
                     (seq repos))
                     (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?])
      [db-restoring?])
 
 
     (rum/use-effect!
     (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?"
         :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?"
         :relaunch-confirm-to-work "Should relaunch app to make it work. Do you want to restart it now?"
         :import "Import"
         :import "Import"
+        :importing "Importing"
         :join-community "Join the community"
         :join-community "Join the community"
         :sponsor-us "Sponsor Us"
         :sponsor-us "Sponsor Us"
         :discourse-title "Our forum!"
         :discourse-title "Our forum!"

+ 7 - 11
src/main/frontend/fs/capacitor_fs.cljs

@@ -185,15 +185,13 @@
             repo-dir (config/get-local-dir repo)
             repo-dir (config/get-local-dir repo)
             ext (util/get-file-ext path)
             ext (util/get-file-ext path)
             db-content (or old-content (db/get-file repo 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
       (cond
         (and
         (and
          (not= stat :not-found)   ; file on the disk was deleted
          (not= stat :not-found)   ; file on the disk was deleted
          (not contents-matched?)
          (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)]
         (p/let [disk-content (encrypt/decrypt disk-content)]
           (state/pub-event! [:file/not-matched-from-disk path disk-content content]))
           (state/pub-event! [:file/not-matched-from-disk path disk-content content]))
 
 
@@ -275,12 +273,10 @@
     (readdir dir))
     (readdir dir))
   (unlink! [this repo path _opts]
   (unlink! [this repo path _opts]
     (p/let [path (get-file-path nil path)
     (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 "/" "_")
                           (string/replace "\\" "_"))
                           (string/replace "\\" "_"))
             new-path (str recycle-dir "/" file-name)]
             new-path (str recycle-dir "/" file-name)]

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

@@ -163,7 +163,6 @@
           (if file-handle
           (if file-handle
             (-> (p/let [local-file (.getFile file-handle)
             (-> (p/let [local-file (.getFile file-handle)
                         local-content (.text local-file)
                         local-content (.text local-file)
-                        pending-writes (state/get-write-chan-length)
                         ext (string/lower-case (util/get-file-ext path))
                         ext (string/lower-case (util/get-file-ext path))
                         db-content (db/get-file repo path)
                         db-content (db/get-file repo path)
                         contents-matched? (contents-matched? local-content (or db-content ""))]
                         contents-matched? (contents-matched? local-content (or db-content ""))]
@@ -172,9 +171,8 @@
                          (not (string/blank? db-content))
                          (not (string/blank? db-content))
                          (not (:skip-compare? opts))
                          (not (:skip-compare? opts))
                          (not contents-matched?)
                          (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)]
                       (p/let [local-content (encrypt/decrypt local-content)]
                         (state/pub-event! [:file/not-matched-from-disk path local-content content]))
                         (state/pub-event! [:file/not-matched-from-disk path local-content content]))
                       (p/let [_ (verify-permission repo file-handle true)
                       (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 "")
             disk-content (or disk-content "")
             ext (string/lower-case (util/get-file-ext path))
             ext (string/lower-case (util/get-file-ext path))
             db-content (or old-content (db/get-file repo 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
       (cond
         (and
         (and
          (not= stat :not-found)         ; file on the disk was deleted
          (not= stat :not-found)         ; file on the disk was deleted
          (not contents-matched?)
          (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)]
         (p/let [disk-content (encrypt/decrypt disk-content)]
           (state/pub-event! [:file/not-matched-from-disk path disk-content 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))
                       (string/includes? % "logseq_local_/")) nfs-dbs))
       (do (notification/show! ["DB version is not compatible, please clear cache then re-add your graph back."
       (do (notification/show! ["DB version is not compatible, please clear cache then re-add your graph back."
                                (ui/button
                                (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
           {:url config/local-repo
            :example? true})
            :example? true})
 
 
@@ -217,12 +218,12 @@
 
 
     (p/let [repos (get-repos)]
     (p/let [repos (get-repos)]
       (state/set-repos! 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!)
     (db/run-batch-txs!)
-    (file-handler/run-writes-chan!)
+
     (when config/dev?
     (when config/dev?
       (enable-datalog-console))
       (enable-datalog-console))
     (when (util/electron?)
     (when (util/electron?)

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

@@ -249,14 +249,17 @@
 
 
 (defn get-blocks-refed-pages
 (defn get-blocks-refed-pages
   [aliases ref-blocks]
   [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]
     (keep (fn [ref]
             (when (:block/name ref)
             (when (:block/name ref)
               {:db/id (:db/id ref)
               {:db/id (:db/id ref)
                :block/name (:block/name 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
 (defn filter-blocks
   [ref-blocks filters]
   [ref-blocks filters]
@@ -276,3 +279,16 @@
         (filter (fn [block]
         (filter (fn [block]
                   (let [ids (set (map :db/id (:block/path-refs block)))]
                   (let [ids (set (map :db/id (:block/path-refs block)))]
                     (set/subset? include-ids ids))))))))
                     (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}]
   [tree-vec format {:keys [target-block keep-uuid?] :as opts}]
   (let [blocks (block-tree->blocks tree-vec format keep-uuid?)
   (let [blocks (block-tree->blocks tree-vec format keep-uuid?)
         page-id (:db/id (:block/page target-block))
         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
     (paste-blocks
      blocks
      blocks
      opts)))
      opts)))

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

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

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

@@ -444,6 +444,15 @@
        x))
        x))
    vec-tree))
    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]
 (defn- blocks [db]
   {:version 1
   {:version 1
    :blocks
    :blocks
@@ -460,12 +469,14 @@
                              name
                              name
                              {:transform? false})
                              {:transform? false})
                      blocks' (map (fn [b]
                      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
         (nested-select-keys
          [:block/id
          [:block/id
           :block/page-name
           :block/page-name

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

@@ -16,7 +16,9 @@
             [frontend.handler.page :as page]
             [frontend.handler.page :as page]
             [frontend.handler.editor :as editor]
             [frontend.handler.editor :as editor]
             [frontend.handler.notification :as notification]
             [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!
 (defn index-files!
   "Create file structure, then parse into DB (client only)"
   "Create file structure, then parse into DB (client only)"
@@ -42,7 +44,7 @@
                                          ".md")]
                                          ".md")]
                            {:file/path path
                            {:file/path path
                             :file/content text}))))
                             :file/content text}))))
-                   files)
+                files)
         files (remove nil? files)]
         files (remove nil? files)]
     (repo-handler/parse-files-and-load-to-db! repo files nil)
     (repo-handler/parse-files-and-load-to-db! repo files nil)
     (let [files (->> (map (fn [{:file/keys [path content]}] (when path [path content])) files)
     (let [files (->> (map (fn [{:file/keys [path content]}] (when path [path content])) files)
@@ -53,13 +55,13 @@
                                             :finish-handler finish-handler}))
                                             :finish-handler finish-handler}))
     (let [journal-pages-tx (let [titles (filter date/valid-journal-title? titles)]
     (let [journal-pages-tx (let [titles (filter date/valid-journal-title? titles)]
                              (map
                              (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)
       (when (seq journal-pages-tx)
         (db/transact! repo journal-pages-tx)))))
         (db/transact! repo journal-pages-tx)))))
 
 
@@ -132,27 +134,43 @@
                                         "\nSkipped and continue the remaining import.") :error))))))
                                         "\nSkipped and continue the remaining import.") :error))))))
   title)
   title)
 
 
-(defn- pre-transact-uuids
+(defn- pre-transact-uuids!
   "Collect all uuids from page trees and write them to the db before hand."
   "Collect all uuids from page trees and write them to the db before hand."
   [pages]
   [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!
 (defn- import-from-tree!
   "Not rely on file system - backend compatible.
   "Not rely on file system - backend compatible.
    tree-translator-fn: translate exported tree structure to the desired tree for import"
    tree-translator-fn: translate exported tree structure to the desired tree for import"
   [data tree-translator-fn]
   [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
 (defn tree-vec-translate-edn
   "Actions to do for loading edn tree structure.
   "Actions to do for loading edn tree structure.
@@ -177,8 +195,16 @@
 
 
 (defn import-from-edn!
 (defn import-from-edn!
   [raw finished-ok-handler]
   [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
 (defn tree-vec-translate-json
   "Actions to do for loading json tree structure.
   "Actions to do for loading json tree structure.
@@ -210,5 +236,6 @@
   [raw finished-ok-handler]
   [raw finished-ok-handler]
   (let [json     (js/JSON.parse raw)
   (let [json     (js/JSON.parse raw)
         clj-data (js->clj json :keywordize-keys true)]
         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])
   (:refer-clojure :exclude [load-file])
   (:require ["/frontend/utils" :as utils]
   (:require ["/frontend/utils" :as utils]
             [borkdude.rewrite-edn :as rewrite]
             [borkdude.rewrite-edn :as rewrite]
-            [cljs.core.async.interop :refer [<p!]]
-            [clojure.core.async :as async]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.db :as db]
             [frontend.db :as db]
             [frontend.fs :as fs]
             [frontend.fs :as fs]
@@ -206,30 +204,8 @@
   (alter-file repo path new-content {:reset? false
   (alter-file repo path new-content {:reset? false
                                      :re-render-root? 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!
 (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]]
   (let [write-file-f (fn [[path content]]
                        (when path
                        (when path
                          (let [original-content (get file->content path)]
                          (let [original-content (get file->content path)]
@@ -254,30 +230,30 @@
                                                                         :error error})))))))
                                                                         :error error})))))))
         finish-handler (fn []
         finish-handler (fn []
                          (when finish-handler
                          (when finish-handler
-                           (finish-handler))
-                         (ui-handler/re-render-file!))]
+                           (finish-handler)))]
     (-> (p/all (map write-file-f files))
     (-> (p/all (map write-file-f files))
         (p/then (fn []
         (p/then (fn []
-                  (finish-handler)
-                  (when chan
-                    (async/put! chan true))))
+                  (finish-handler)))
         (p/catch (fn [error]
         (p/catch (fn [error]
                    (println "Alter files failed:")
                    (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!
 (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)
         new-tag (if (re-find #"[\s\t]+" new-name)
                   (util/format "#[[%s]]" new-name)
                   (util/format "#[[%s]]" new-name)
                   (str "#" 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!
 (defn- replace-property-ref!
   [content old-name new-name]
   [content old-name new-name]

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

@@ -99,12 +99,6 @@
      (doseq [component (state/get-custom-query-components)]
      (doseq [component (state/get-custom-query-components)]
        (rum/request-render component)))))
        (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!
 (defn highlight-element!
   [fragment]
   [fragment]
   (let [id (and
   (let [id (and

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

@@ -1,13 +1,13 @@
 (ns frontend.modules.file.core
 (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.config :as config]
             [frontend.date :as date]
             [frontend.date :as date]
             [frontend.db :as db]
             [frontend.db :as db]
             [frontend.db.utils :as db-utils]
             [frontend.db.utils :as db-utils]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.util :as util]
             [frontend.util :as util]
-            [frontend.util.property :as property]))
+            [frontend.util.property :as property]
+            [frontend.handler.file :as file-handler]))
 
 
 (defn- indented-block-content
 (defn- indented-block-content
   [content spaces-tabs]
   [content spaces-tabs]
@@ -101,16 +101,6 @@
 
 
 (def init-level 1)
 (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!
 (defn- transact-file-tx-if-not-exists!
   [page ok-handler]
   [page ok-handler]
   (when-let [repo (state/get-current-repo)]
   (when-let [repo (state/get-current-repo)]
@@ -151,8 +141,9 @@
                       (tree->file-content tree {:init-level init-level}))
                       (tree->file-content tree {:init-level init-level}))
         _ (assert (string? file-path) "File path should satisfy string?")
         _ (assert (string? file-path) "File path should satisfy string?")
         ;; FIXME: name conflicts between multiple graphs
         ;; 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!
 (defn save-tree!
   [page-block 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/alias]
                          [:db/retract id :block/tags]])))]
                          [:db/retract id :block/tags]])))]
       (swap! txs-state concat txs page-tx)
       (swap! txs-state concat txs page-tx)
-      (util/pprint @txs-state)
       block-id))
       block-id))
 
 
   (-get-children [this]
   (-get-children [this]

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

@@ -12,7 +12,6 @@
             [lambdaisland.glogi :as log]
             [lambdaisland.glogi :as log]
             [frontend.state :as state]))
             [frontend.state :as state]))
 
 
-(defonce write-chan (async/chan 100))
 (defonce write-chan-batch-buf (atom []))
 (defonce write-chan-batch-buf (atom []))
 
 
 (def batch-write-interval 1000)
 (def batch-write-interval 1000)
@@ -48,8 +47,8 @@
         page-db-id (:db/id page-block)
         page-db-id (:db/id page-block)
         blocks-count (model/get-page-blocks-count repo page-db-id)]
         blocks-count (model/get-page-blocks-count repo page-db-id)]
     (if (and (> blocks-count 500)
     (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)
       (let [whiteboard? (:block/whiteboard? page-block)
             pull-keys (if whiteboard? whiteboard-blocks-pull-keys-with-persisted-ids '[*])
             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})
             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!"
      "Write file failed, can't find the current page!"
      :error)
      :error)
     (when-let [repo (state/get-current-repo)]
     (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
             batch-write-interval
             write-files!
             write-files!
             write-chan-batch-buf)
             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 (util/concat-without-nil truncate-refs-tx refs-tx)
             tx-report' (if (seq tx)
             tx-report' (if (seq tx)
                          (let [refs-tx-data' (:tx-data (db/transact! repo tx {:outliner/transact? true
                          (let [refs-tx-data' (:tx-data (db/transact! repo tx {:outliner/transact? true
-                                                                         :compute-new-refs? true}))]
+                                                                              :compute-new-refs? true}))]
                            ;; merge
                            ;; merge
                            (assoc tx-report :tx-data (concat (:tx-data tx-report) refs-tx-data')))
                            (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)]
         (doseq [p (seq pages)]
           (updated-page-hook tx-report p))
           (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
           (state/pub-event! [:plugin/hook-db-tx
                              {:blocks  blocks
                              {:blocks  blocks
                               :tx-data (:tx-data tx-report)
                               :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))}
                                                 (route-handler/go-to-search! :global))}
 
 
    :go/electron-find-in-page       {:binding "mod+f"
    :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"]
    :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"]
    :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"
    :go/journals                    {:binding "g j"
                                     :fn      route-handler/go-to-journals!}
                                     :fn      route-handler/go-to-journals!}

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

@@ -24,11 +24,11 @@
    (atom
    (atom
     {:route-match                           nil
     {:route-match                           nil
      :today                                 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                    #{}
      :file/unlinked-dirs                    #{}
-     :reactive/custom-queries               (async/chan 100)
+     :reactive/custom-queries               (async/chan 1000)
      :notification/show?                    false
      :notification/show?                    false
      :notification/content                  nil
      :notification/content                  nil
      :repo/loading-files?                   {}
      :repo/loading-files?                   {}
@@ -232,7 +232,9 @@
 
 
      :encryption/graph-parsing?             false
      :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}
 ;; block uuid -> {content(String) -> ast}
@@ -1021,10 +1023,6 @@
   []
   []
   (set-state! :ui/file-component nil))
   (set-state! :ui/file-component nil))
 
 
-(defn get-file-component
-  []
-  (get @state :ui/file-component))
-
 (defn set-journals-length!
 (defn set-journals-length!
   [value]
   [value]
   (when value
   (when value
@@ -1174,11 +1172,6 @@
   []
   []
   (:reactive/custom-queries @state))
   (: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?
 (defn get-left-sidebar-open?
   []
   []
   (get-in @state [:ui/left-sidebar-open?]))
   (get-in @state [:ui/left-sidebar-open?]))

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

@@ -52,6 +52,8 @@
   < rum/reactive
   < rum/reactive
   {:did-mount (fn [state]
   {:did-mount (fn [state]
                 (let [^js el (rum/dom-node 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"
                   (. el addEventListener "mouseup"
                      #(let [start (util/get-selection-start el)
                      #(let [start (util/get-selection-start el)
                             end (util/get-selection-end el)]
                             end (util/get-selection-end el)]

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

@@ -532,17 +532,18 @@
          (str prefix new-value)))
          (str prefix new-value)))
      s)))
      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
 (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
 ;; copy from https://stackoverflow.com/questions/18735665/how-can-i-get-the-positions-of-regex-matches-in-clojurescript
 #?(:cljs
 #?(:cljs

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

@@ -68,23 +68,46 @@
   (are [x y] (= (let [[content old-name new-name] x]
   (are [x y] (= (let [[content old-name new-name] x]
                   (page-handler/replace-tag-ref! content old-name new-name))
                   (page-handler/replace-tag-ref! content old-name new-name))
                 y)
                 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!
 (deftest test-replace-old-page!
   (are [x y] (= (let [[content old-name new-name] x]
   (are [x y] (= (let [[content old-name new-name] x]

+ 13 - 1
templates/config.edn

@@ -162,7 +162,7 @@
             :where
             :where
             [?h :block/marker ?marker]
             [?h :block/marker ?marker]
             [(contains? #{"NOW" "LATER" "TODO"} ?marker)]
             [(contains? #{"NOW" "LATER" "TODO"} ?marker)]
-            [?h :block/ref-pages ?p]
+            [?h :block/page ?p]
             [?p :block/journal? true]
             [?p :block/journal? true]
             [?p :block/journal-day ?d]
             [?p :block/journal-day ?d]
             [(> ?d ?start)]
             [(> ?d ?start)]
@@ -265,4 +265,16 @@
  ;; :quick-capture-templates
  ;; :quick-capture-templates
  ;; {:text "[[quick capture]] **{time}**: {text} from {url}"
  ;; {:text "[[quick capture]] **{time}**: {text} from {url}"
  ;;  :media "[[quick capture]] **{time}**: {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 "^7.0.32"
     postcss-value-parser "^4.1.0"
     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:
 bach@^1.0.0:
   version "1.2.0"
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/bach/-/bach-1.2.0.tgz#4b3ce96bf27134f79a1b414a51c14e34c3bd9880"
   resolved "https://registry.yarnpkg.com/bach/-/bach-1.2.0.tgz#4b3ce96bf27134f79a1b414a51c14e34c3bd9880"