Răsfoiți Sursa

Merge branch 'master' into feat/db

Tienson Qin 2 ani în urmă
părinte
comite
fd6b587235
100 a modificat fișierele cu 1976 adăugiri și 896 ștergeri
  1. 3 0
      .clj-kondo/config.edn
  2. 4 0
      .github/ISSUE_TEMPLATE/bug_report.yaml
  3. 13 2
      .github/workflows/build-desktop-release.yml
  4. 13 2
      .github/workflows/build.yml
  5. 12 1
      .github/workflows/e2e.yml
  6. 1 0
      CONTRIBUTING.md
  7. 2 2
      android/app/build.gradle
  8. 4 7
      bb.edn
  9. 2 2
      deps.edn
  10. 3 3
      deps/common/bb.edn
  11. 1 1
      deps/common/deps.edn
  12. 2 2
      deps/db/bb.edn
  13. 1 1
      deps/db/deps.edn
  14. 2 2
      deps/graph-parser/bb.edn
  15. 1 1
      deps/graph-parser/deps.edn
  16. 14 1
      deps/graph-parser/src/logseq/graph_parser/mldoc.cljc
  17. 18 0
      deps/graph-parser/test/logseq/graph_parser/mldoc_test.cljs
  18. 2 2
      deps/publishing/bb.edn
  19. 1 1
      deps/publishing/deps.edn
  20. 18 1
      deps/publishing/src/logseq/publishing/db.cljs
  21. 3 1
      deps/publishing/test/logseq/publishing/db_test.cljs
  22. 14 18
      docs/contributing-to-translations.md
  23. 5 2
      docs/dev-practices.md
  24. 3 1
      e2e-tests/code-editing.spec.ts
  25. 2 1
      e2e-tests/editor.spec.ts
  26. 2 1
      e2e-tests/fixtures.ts
  27. 1 0
      e2e-tests/history.spec.ts
  28. 2 1
      e2e-tests/hotkey.spec.ts
  29. 105 0
      e2e-tests/logseq-api.spec.ts
  30. 13 1
      e2e-tests/page-rename.spec.ts
  31. 1 11
      e2e-tests/plugins.spec.ts
  32. 0 1
      e2e-tests/util/page.ts
  33. 32 29
      e2e-tests/util/search-modal.ts
  34. 1 0
      e2e-tests/utils.ts
  35. 97 21
      e2e-tests/whiteboards.spec.ts
  36. 34 0
      e2e-tests/window.spec.ts
  37. 4 4
      ios/App/App.xcodeproj/project.pbxproj
  38. 13 5
      ios/App/App/FsWatcher.swift
  39. 1 1
      libs/src/LSPlugin.core.ts
  40. 24 20
      libs/src/LSPlugin.ts
  41. 2 2
      package.json
  42. 18 2
      playwright.config.ts
  43. 0 0
      resources/css/katex.min.css
  44. 0 0
      resources/js/katex.min.js
  45. 2 2
      resources/package.json
  46. 53 20
      scripts/src/logseq/tasks/lang.clj
  47. 16 1
      src/electron/electron/handler.cljs
  48. 37 14
      src/electron/electron/plugin.cljs
  49. 5 2
      src/electron/electron/window.cljs
  50. 3 10
      src/main/electron/listener.cljs
  51. 14 13
      src/main/frontend/commands.cljs
  52. 6 6
      src/main/frontend/components/assets.cljs
  53. 80 102
      src/main/frontend/components/block.cljs
  54. 1 1
      src/main/frontend/components/block.css
  55. 71 0
      src/main/frontend/components/block/macros.cljs
  56. 24 23
      src/main/frontend/components/bug_report.cljs
  57. 21 10
      src/main/frontend/components/container.cljs
  58. 29 3
      src/main/frontend/components/container.css
  59. 1 1
      src/main/frontend/components/conversion.cljs
  60. 6 4
      src/main/frontend/components/editor.cljs
  61. 1 1
      src/main/frontend/components/file.cljs
  62. 4 4
      src/main/frontend/components/file_sync.cljs
  63. 14 10
      src/main/frontend/components/header.cljs
  64. 5 3
      src/main/frontend/components/header.css
  65. 5 5
      src/main/frontend/components/onboarding.cljs
  66. 24 24
      src/main/frontend/components/onboarding/quick_tour.cljs
  67. 20 20
      src/main/frontend/components/onboarding/setups.cljs
  68. 18 20
      src/main/frontend/components/page.cljs
  69. 100 27
      src/main/frontend/components/plugins.cljs
  70. 65 16
      src/main/frontend/components/plugins.css
  71. 10 13
      src/main/frontend/components/query.cljs
  72. 29 29
      src/main/frontend/components/query/result.cljs
  73. 2 2
      src/main/frontend/components/repo.cljs
  74. 23 17
      src/main/frontend/components/right_sidebar.cljs
  75. 7 7
      src/main/frontend/components/search.cljs
  76. 269 50
      src/main/frontend/components/settings.cljs
  77. 57 40
      src/main/frontend/components/settings.css
  78. 90 44
      src/main/frontend/components/shortcut.cljs
  79. 28 0
      src/main/frontend/components/shortcut.css
  80. 26 0
      src/main/frontend/components/svg.cljs
  81. 0 2
      src/main/frontend/components/theme.css
  82. 2 2
      src/main/frontend/components/whiteboard.cljs
  83. 52 0
      src/main/frontend/components/window_controls.cljs
  84. 14 0
      src/main/frontend/components/window_controls.css
  85. 6 2
      src/main/frontend/config.cljs
  86. 17 5
      src/main/frontend/date.cljs
  87. 1 0
      src/main/frontend/db/model.cljs
  88. 2 2
      src/main/frontend/db/react.cljs
  89. 2 4
      src/main/frontend/extensions/excalidraw.cljs
  90. 2 3
      src/main/frontend/extensions/latex.cljs
  91. 107 95
      src/main/frontend/extensions/pdf/core.cljs
  92. 1 0
      src/main/frontend/extensions/pdf/toolbar.cljs
  93. 27 26
      src/main/frontend/extensions/srs.cljs
  94. 4 2
      src/main/frontend/extensions/tldraw.cljs
  95. 4 1
      src/main/frontend/handler/code.cljs
  96. 1 1
      src/main/frontend/handler/common/config_edn.cljs
  97. 49 36
      src/main/frontend/handler/editor.cljs
  98. 2 2
      src/main/frontend/handler/editor/lifecycle.cljs
  99. 22 15
      src/main/frontend/handler/events.cljs
  100. 1 1
      src/main/frontend/handler/global_config.cljs

+ 3 - 0
.clj-kondo/config.edn

@@ -15,6 +15,8 @@
   :aliased-namespace-symbol {:level :warning}
   :aliased-namespace-symbol {:level :warning}
   ;; Disable until it doesn't trigger false positives on rum/defcontext
   ;; Disable until it doesn't trigger false positives on rum/defcontext
   :earmuffed-var-not-dynamic {:level :off}
   :earmuffed-var-not-dynamic {:level :off}
+  ;; Disable until we decide to use conj! as recommended in docs
+  :unused-value {:level :off}
   :unresolved-symbol {:exclude [goog.DEBUG
   :unresolved-symbol {:exclude [goog.DEBUG
                                 goog.string.unescapeEntities
                                 goog.string.unescapeEntities
                                 ;; TODO:lint: Fix when fixing all type hints
                                 ;; TODO:lint: Fix when fixing all type hints
@@ -39,6 +41,7 @@
              electron.utils utils
              electron.utils utils
              "/electron/utils" js-utils
              "/electron/utils" js-utils
              frontend.commands commands
              frontend.commands commands
+             frontend.components.block.macros block-macros
              frontend.components.query query
              frontend.components.query query
              frontend.components.query.result query-result
              frontend.components.query.result query-result
              frontend.config config
              frontend.config config

+ 4 - 0
.github/ISSUE_TEMPLATE/bug_report.yaml

@@ -7,6 +7,10 @@ body:
         Thank you very much for opening a bug report with Logseq.
         Thank you very much for opening a bug report with Logseq.
 
 
         If you have a feature idea or need help, please go to [our Forum](https://discuss.logseq.com/) or [our Discord](https://discord.com/invite/KpN4eHY).
         If you have a feature idea or need help, please go to [our Forum](https://discuss.logseq.com/) or [our Discord](https://discord.com/invite/KpN4eHY).
+
+        Please make sure to provide a descriptive, deterministic, and reproducible report. It saves time for both the developers and users who are looking for solutions. Providing as much information as possible, including screenshots and logs, is highly appreciated. This will help us to better understand the issue and respond more effectively.
+
+        Please DO NOT use this template to ask questions. There are other appropriate channels to ask questions. This template is strictly for reporting bugs.
   - type: checkboxes
   - type: checkboxes
     id: confirm-search
     id: confirm-search
     attributes:
     attributes:

+ 13 - 2
.github/workflows/build-desktop-release.yml

@@ -122,7 +122,7 @@ jobs:
         run: |
         run: |
           sed -i 's/defonce version ".*"/defonce version "${{ steps.ref.outputs.version }}"/g' src/main/frontend/version.cljs
           sed -i 's/defonce version ".*"/defonce version "${{ steps.ref.outputs.version }}"/g' src/main/frontend/version.cljs
 
 
-      - name: Set Build Environment Variables (only when workflow_dispath)
+      - name: Set Build Environment Variables (only when workflow_dispatch)
         if: ${{ github.event_name == 'workflow_dispatch' }}
         if: ${{ github.event_name == 'workflow_dispatch' }}
         # if scheduled, use default settings
         # if scheduled, use default settings
         run: |
         run: |
@@ -204,8 +204,19 @@ jobs:
         env:
         env:
           PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
           PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
 
 
+      - name: Install Fluxbox
+        run: sudo apt-get update && sudo apt-get install -y fluxbox
+
+      # Emulate a virtual framebuffer on machines with no display hardware
+      - name: Run XVFB
+        run: Xvfb :1 -screen 0 1024x768x24 >/dev/null 2>&1 &
+
+      # Start a lightweight window manager to simulate window actions (maximize,restore etc)
+      - name: Start Fluxbox
+        run:  DISPLAY=:1.0 fluxbox >/dev/null 2>&1 &
+
       - name: Run Playwright test
       - name: Run Playwright test
-        run: xvfb-run -- npx playwright test --reporter github --shard=${{ matrix.shard }}/3
+        run: DISPLAY=:1.0 npx playwright test --reporter github --shard=${{ matrix.shard }}/3
         env:
         env:
           LOGSEQ_CI: true
           LOGSEQ_CI: true
           DEBUG: "pw:api"
           DEBUG: "pw:api"

+ 13 - 2
.github/workflows/build.yml

@@ -182,15 +182,26 @@ jobs:
       - name: Ensure static yarn.lock is up to date
       - name: Ensure static yarn.lock is up to date
         run: git diff --exit-code static/yarn.lock
         run: git diff --exit-code static/yarn.lock
 
 
+      - name: Install Fluxbox
+        run: sudo apt-get update && sudo apt-get install -y fluxbox
+
+      # Emulate a virtual framebuffer on machines with no display hardware
+      - name: Run XVFB
+        run: Xvfb :1 -screen 0 1024x768x24 >/dev/null 2>&1 &
+
+      # Start a lightweight window manager to simulate window actions (maximize,restore etc)
+      - name: Start Fluxbox
+        run:  DISPLAY=:1.0 fluxbox >/dev/null 2>&1 &
+
       - name: Run Playwright test - 1/2
       - name: Run Playwright test - 1/2
-        run: xvfb-run -- npx playwright test --reporter github --shard=1/2
+        run: DISPLAY=:1.0 npx playwright test --reporter github --shard=1/2
         env:
         env:
           LOGSEQ_CI: true
           LOGSEQ_CI: true
           DEBUG: "pw:api"
           DEBUG: "pw:api"
           RELEASE: true # skip dev only test
           RELEASE: true # skip dev only test
 
 
       - name: Run Playwright test - 2/2
       - name: Run Playwright test - 2/2
-        run: xvfb-run -- npx playwright test --reporter github --shard=2/2
+        run: DISPLAY=:1.0 npx playwright test --reporter github --shard=2/2
         env:
         env:
           LOGSEQ_CI: true
           LOGSEQ_CI: true
           DEBUG: "pw:api"
           DEBUG: "pw:api"

+ 12 - 1
.github/workflows/e2e.yml

@@ -137,8 +137,19 @@ jobs:
       - name: Ensure static yarn.lock is up to date
       - name: Ensure static yarn.lock is up to date
         run: git diff --exit-code static/yarn.lock
         run: git diff --exit-code static/yarn.lock
 
 
+      - name: Install Fluxbox
+        run: sudo apt-get update && sudo apt-get install -y fluxbox
+
+      # Emulate a virtual framebuffer on machines with no display hardware
+      - name: Run XVFB
+        run: Xvfb :1 -screen 0 1024x768x24 >/dev/null 2>&1 &
+
+      # Start a lightweight window manager to simulate window actions (maximize,restore etc)
+      - name: Start Fluxbox
+        run:  DISPLAY=:1.0 fluxbox >/dev/null 2>&1 &
+
       - name: Run Playwright test
       - name: Run Playwright test
-        run: xvfb-run -- npx playwright test --reporter github --shard=${{ matrix.shard }}/3
+        run: DISPLAY=:1.0 npx playwright test --reporter github --shard=${{ matrix.shard }}/3
         env:
         env:
           LOGSEQ_CI: true
           LOGSEQ_CI: true
           DEBUG: "pw:api"
           DEBUG: "pw:api"

+ 1 - 0
CONTRIBUTING.md

@@ -133,6 +133,7 @@ When submitting a Pull Request (PR) or expecting a subsequent review, please fol
    * Unrelated refactoring or heavy refactoring
    * Unrelated refactoring or heavy refactoring
    * Code or doc formatting changes including whitespace changes
    * Code or doc formatting changes including whitespace changes
    * Dependency updates e.g. in package.json
    * Dependency updates e.g. in package.json
+   * Changes that contain multiple unverified resources. This is risky for our users and is a lot of work to verify. A change with one resource that can be verified is acceptable.
 
 
 ### PR Additional Links
 ### PR Additional Links
 
 

+ 2 - 2
android/app/build.gradle

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

+ 4 - 7
bb.edn

@@ -5,13 +5,13 @@
   logseq/bb-tasks
   logseq/bb-tasks
   #_{:local/root "../bb-tasks"}
   #_{:local/root "../bb-tasks"}
   {:git/url "https://github.com/logseq/bb-tasks"
   {:git/url "https://github.com/logseq/bb-tasks"
-   :git/sha "4295d5df0458cc06a09c5d506510ee49b785407d"}
+   :git/sha "70d3edeb287f5cec7192e642549a401f7d6d4263"}
   logseq/graph-parser
   logseq/graph-parser
   {:local/root "deps/graph-parser"}
   {:local/root "deps/graph-parser"}
   org.clj-commons/digest
   org.clj-commons/digest
   {:mvn/version "1.4.100"}}
   {:mvn/version "1.4.100"}}
  :pods
  :pods
- {clj-kondo/clj-kondo {:version "2022.10.05"}
+ {clj-kondo/clj-kondo {:version "2023.05.26"}
   org.babashka/fswatcher {:version "0.0.3"}
   org.babashka/fswatcher {:version "0.0.3"}
   org.babashka/go-sqlite3 {:version "0.1.0"}}
   org.babashka/go-sqlite3 {:version "0.1.0"}}
  :tasks
  :tasks
@@ -39,8 +39,8 @@
   {:depends [dev:build-publishing]
   {:depends [dev:build-publishing]
    :doc "Build publishing spa app given graph and output dirs"
    :doc "Build publishing spa app given graph and output dirs"
    :task (apply shell {:dir "scripts"}
    :task (apply shell {:dir "scripts"}
-           "yarn -s nbb-logseq -cp src -m logseq.tasks.dev.publishing"
-           (into ["static"] *command-line-args*))}
+                "yarn -s nbb-logseq -cp src -m logseq.tasks.dev.publishing"
+                (into ["static"] *command-line-args*))}
 
 
   dev:npx-cap-run-ios
   dev:npx-cap-run-ios
   logseq.tasks.dev.mobile/npx-cap-run-ios
   logseq.tasks.dev.mobile/npx-cap-run-ios
@@ -113,9 +113,6 @@
   lang:missing
   lang:missing
   logseq.tasks.lang/list-missing
   logseq.tasks.lang/list-missing
 
 
-  lang:duplicates
-  logseq.tasks.lang/list-duplicates
-
   lang:validate-translations
   lang:validate-translations
   logseq.tasks.lang/validate-translations
   logseq.tasks.lang/validate-translations
 
 

+ 2 - 2
deps.edn

@@ -1,4 +1,4 @@
-{:paths ["src/main" "src/electron" "templates" "src/resources"]
+{:paths ["src/main" "src/electron" "src/resources"]
  :deps
  :deps
  {org.clojure/clojure                   {:mvn/version "1.11.1"}
  {org.clojure/clojure                   {:mvn/version "1.11.1"}
   rum/rum                               {:mvn/version "0.12.9"}
   rum/rum                               {:mvn/version "0.12.9"}
@@ -55,5 +55,5 @@
                    :main-opts ["-m" "cljs-test-runner.main" "-d" "src/bench" "-n" "frontend.benchmark-test-runner"]}
                    :main-opts ["-m" "cljs-test-runner.main" "-d" "src/bench" "-n" "frontend.benchmark-test-runner"]}
 
 
            ;; Use :replace-deps for tools. See https://github.com/clj-kondo/clj-kondo/issues/1536#issuecomment-1013006889
            ;; Use :replace-deps for tools. See https://github.com/clj-kondo/clj-kondo/issues/1536#issuecomment-1013006889
-           :clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2022.12.08"}}
+           :clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2023.05.26"}}
                        :main-opts  ["-m" "clj-kondo.main"]}}}
                        :main-opts  ["-m" "clj-kondo.main"]}}}

+ 3 - 3
deps/common/bb.edn

@@ -3,10 +3,10 @@
  {logseq/bb-tasks
  {logseq/bb-tasks
   #_{:local/root "../../../bb-tasks"}
   #_{:local/root "../../../bb-tasks"}
   {:git/url "https://github.com/logseq/bb-tasks"
   {:git/url "https://github.com/logseq/bb-tasks"
-   :git/sha "0d49051909bfa0c6b414e86606d82b4ea54f382c"}}
+   :git/sha "70d3edeb287f5cec7192e642549a401f7d6d4263"}}
 
 
  :pods
  :pods
- {clj-kondo/clj-kondo {:version "2023.03.17"}}
+ {clj-kondo/clj-kondo {:version "2023.05.26"}}
 
 
  :tasks
  :tasks
  {test:load-all-namespaces-with-nbb
  {test:load-all-namespaces-with-nbb
@@ -23,4 +23,4 @@
 
 
  :tasks/config
  :tasks/config
  {:large-vars
  {:large-vars
-  {:max-lines-count 45}}}
+  {:max-lines-count 45}}}

+ 1 - 1
deps/common/deps.edn

@@ -4,5 +4,5 @@
                        org.clojure/clojurescript {:mvn/version "1.11.54"}}
                        org.clojure/clojurescript {:mvn/version "1.11.54"}}
          :main-opts   ["-m" "cljs-test-runner.main"]}
          :main-opts   ["-m" "cljs-test-runner.main"]}
   :clj-kondo
   :clj-kondo
-  {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2022.12.08"}}
+  {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2023.05.26"}}
    :main-opts  ["-m" "clj-kondo.main"]}}}
    :main-opts  ["-m" "clj-kondo.main"]}}}

+ 2 - 2
deps/db/bb.edn

@@ -4,10 +4,10 @@
  {logseq/bb-tasks
  {logseq/bb-tasks
   #_{:local/root "../../../bb-tasks"}
   #_{:local/root "../../../bb-tasks"}
   {:git/url "https://github.com/logseq/bb-tasks"
   {:git/url "https://github.com/logseq/bb-tasks"
-   :git/sha "1815db538241082a01e95601e23e4290dd64d0c0"}}
+   :git/sha "70d3edeb287f5cec7192e642549a401f7d6d4263"}}
 
 
  :pods
  :pods
- {clj-kondo/clj-kondo {:version "2022.10.05"}}
+ {clj-kondo/clj-kondo {:version "2023.05.26"}}
 
 
  :tasks
  :tasks
  {test:load-all-namespaces-with-nbb
  {test:load-all-namespaces-with-nbb

+ 1 - 1
deps/db/deps.edn

@@ -3,5 +3,5 @@
  {datascript/datascript {:mvn/version "1.3.8"}}
  {datascript/datascript {:mvn/version "1.3.8"}}
  :aliases
  :aliases
  {:clj-kondo
  {:clj-kondo
-  {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2022.12.08"}}
+  {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2023.05.26"}}
    :main-opts  ["-m" "clj-kondo.main"]}}}
    :main-opts  ["-m" "clj-kondo.main"]}}}

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

@@ -3,10 +3,10 @@
  {logseq/bb-tasks
  {logseq/bb-tasks
   #_{:local/root "../../../bb-tasks"}
   #_{:local/root "../../../bb-tasks"}
   {:git/url "https://github.com/logseq/bb-tasks"
   {:git/url "https://github.com/logseq/bb-tasks"
-   :git/sha "1815db538241082a01e95601e23e4290dd64d0c0"}}
+   :git/sha "70d3edeb287f5cec7192e642549a401f7d6d4263"}}
  
  
  :pods
  :pods
- {clj-kondo/clj-kondo {:version "2022.10.05"}}
+ {clj-kondo/clj-kondo {:version "2023.05.26"}}
 
 
  :tasks
  :tasks
  {test:load-all-namespaces-with-nbb
  {test:load-all-namespaces-with-nbb

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

@@ -20,5 +20,5 @@
                        org.clojure/clojurescript {:mvn/version "1.11.54"}}
                        org.clojure/clojurescript {:mvn/version "1.11.54"}}
          :main-opts   ["-m" "cljs-test-runner.main"]}
          :main-opts   ["-m" "cljs-test-runner.main"]}
 
 
-  :clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2022.12.08"}}
+  :clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2023.05.26"}}
               :main-opts    ["-m" "clj-kondo.main"]}}}
               :main-opts    ["-m" "clj-kondo.main"]}}}

+ 14 - 1
deps/graph-parser/src/logseq/graph_parser/mldoc.cljc

@@ -75,13 +75,26 @@
           js/JSON.stringify))))
           js/JSON.stringify))))
 
 
 (defn remove-indentation-spaces
 (defn remove-indentation-spaces
+  "Remove the indentation spaces from the content. Only for markdown.
+   level - ast level + 1 (2 for the first level, 3 for the second level, etc., as the non-first line of multi-line block has 2 more space
+           Ex.
+              - level 1 multiline block first line
+                level 1 multiline block second line
+              \t- level 2 multiline block first line
+              \t  level 2 multiline block second line
+   remove-first-line? - apply the indentation removal to the first line or not"
   [s level remove-first-line?]
   [s level remove-first-line?]
   (let [lines (string/split-lines s)
   (let [lines (string/split-lines s)
         [f & r] lines
         [f & r] lines
         body (map (fn [line]
         body (map (fn [line]
+                    ;; Check if the indentation area only contains white spaces
+                    ;; Level = ast level + 1, 1-based indentation level
+                    ;; For markdown in Logseq, the indentation area for the non-first line of multi-line block is (ast level - 1) * "\t" + 2 * "(space)"
                     (if (string/blank? (gp-util/safe-subs line 0 level))
                     (if (string/blank? (gp-util/safe-subs line 0 level))
+                      ;; If valid, then remove the indentation area spaces. Keep the rest of the line (might contain leading spaces)
                       (gp-util/safe-subs line level)
                       (gp-util/safe-subs line level)
-                      line))
+                      ;; Otherwise, trim these invalid spaces
+                      (string/triml line)))
                (if remove-first-line? lines r))
                (if remove-first-line? lines r))
         content (if remove-first-line? body (cons f body))]
         content (if remove-first-line? body (cons f body))]
     (string/join "\n" content)))
     (string/join "\n" content)))

+ 18 - 0
deps/graph-parser/test/logseq/graph_parser/mldoc_test.cljs

@@ -119,6 +119,24 @@ body"
       (is ["@tag" "tag1" "tag2"] (sort (:filetags props)))
       (is ["@tag" "tag1" "tag2"] (sort (:filetags props)))
       (is ["@tag" "tag1" "tag2" "tag3"] (sort (:tags props))))))
       (is ["@tag" "tag1" "tag2" "tag3"] (sort (:tags props))))))
 
 
+(deftest remove-indentation-spaces
+  (testing "Remove indentations for every line"
+    (is (=  "block 1.1\n  line 1\n    line 2\nline 3\nline 4"
+            (let [s "block 1.1
+    line 1
+      line 2
+ line 3
+line 4"]
+              (gp-mldoc/remove-indentation-spaces s 2 false)))) 
+    (is (=  "\t- block 1.1\n  line 1\n    line 2\nline 3\nline 4"
+            (let [s "\t- block 1.1
+\t    line 1
+\t      line 2
+\t line 3
+\tline 4"]
+              (gp-mldoc/remove-indentation-spaces s 3 false))))))
+    
+
 (deftest ^:integration test->edn
 (deftest ^:integration test->edn
   (let [graph-dir "test/docs-0.9.2"
   (let [graph-dir "test/docs-0.9.2"
         _ (docs-graph-helper/clone-docs-repo-if-not-exists graph-dir "v0.9.2")
         _ (docs-graph-helper/clone-docs-repo-if-not-exists graph-dir "v0.9.2")

+ 2 - 2
deps/publishing/bb.edn

@@ -3,10 +3,10 @@
  {logseq/bb-tasks
  {logseq/bb-tasks
   #_{:local/root "../../../bb-tasks"}
   #_{:local/root "../../../bb-tasks"}
   {:git/url "https://github.com/logseq/bb-tasks"
   {:git/url "https://github.com/logseq/bb-tasks"
-   :git/sha "0d49051909bfa0c6b414e86606d82b4ea54f382c"}}
+   :git/sha "70d3edeb287f5cec7192e642549a401f7d6d4263"}}
 
 
  :pods
  :pods
- {clj-kondo/clj-kondo {:version "2023.03.17"}}
+ {clj-kondo/clj-kondo {:version "2023.05.26"}}
 
 
  :tasks
  :tasks
  {test:load-all-namespaces-with-nbb
  {test:load-all-namespaces-with-nbb

+ 1 - 1
deps/publishing/deps.edn

@@ -3,5 +3,5 @@
  {logseq/db {:local/root "../db"}}
  {logseq/db {:local/root "../db"}}
 
 
  :aliases
  :aliases
- {:clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2023.03.17"}}
+ {:clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2023.05.26"}}
               :main-opts    ["-m" "clj-kondo.main"]}}}
               :main-opts    ["-m" "clj-kondo.main"]}}}

+ 18 - 1
deps/publishing/src/logseq/publishing/db.cljs

@@ -2,6 +2,8 @@
   "Provides db fns and associated util fns for publishing"
   "Provides db fns and associated util fns for publishing"
   (:require [datascript.core :as d]
   (:require [datascript.core :as d]
             [logseq.db.schema :as db-schema]
             [logseq.db.schema :as db-schema]
+            [logseq.db.rules :as rules]
+            [clojure.set :as set]
             [clojure.string :as string]))
             [clojure.string :as string]))
 
 
 (defn ^:api get-area-block-asset-url
 (defn ^:api get-area-block-asset-url
@@ -92,6 +94,20 @@
      flatten
      flatten
      distinct)))
      distinct)))
 
 
+(defn- get-aliases-for-page-ids
+  [db page-ids]
+  (->> (d/q '[:find ?e
+              :in $ ?pages %
+              :where
+              [?page :block/name]
+              [(contains? ?pages ?page)]
+              (alias ?page ?e)]
+            db
+            (set page-ids)
+            (:alias rules/rules))
+       (map first)
+       set))
+
 (defn clean-export!
 (defn clean-export!
   "Prepares a database assuming all pages are public unless a page has a 'public:: false'"
   "Prepares a database assuming all pages are public unless a page has a 'public:: false'"
   [db]
   [db]
@@ -113,7 +129,8 @@
   "Prepares a database assuming all pages are private unless a page has a 'public:: true'"
   "Prepares a database assuming all pages are private unless a page has a 'public:: true'"
   [db]
   [db]
   (when-let [public-pages* (seq (get-public-pages db))]
   (when-let [public-pages* (seq (get-public-pages db))]
-    (let [public-pages (set public-pages*)
+    (let [public-pages (set/union (set public-pages*)
+                                  (get-aliases-for-page-ids db public-pages*))
           exported-namespace? #(contains? #{"block" "recent"} %)
           exported-namespace? #(contains? #{"block" "recent"} %)
           filtered-db (d/filter db
           filtered-db (d/filter db
                                 (fn [db datom]
                                 (fn [db datom]

+ 3 - 1
deps/publishing/test/logseq/publishing/db_test.cljs

@@ -36,7 +36,7 @@
 (deftest filter-only-public-pages-and-blocks
 (deftest filter-only-public-pages-and-blocks
   (let [conn (ldb/start-conn)
   (let [conn (ldb/start-conn)
         _ (graph-parser/parse-file conn "page1.md" "- b11\n- b12\n- ![awesome.png](../assets/awesome_1648822509908_0.png)")
         _ (graph-parser/parse-file conn "page1.md" "- b11\n- b12\n- ![awesome.png](../assets/awesome_1648822509908_0.png)")
-        _ (graph-parser/parse-file conn "page2.md" "public:: true\n- b21\n- ![thumb-on-fire.PNG](../assets/thumb-on-fire_1648822523866_0.PNG)")
+        _ (graph-parser/parse-file conn "page2.md" "alias:: page2-alias\npublic:: true\n- b21\n- ![thumb-on-fire.PNG](../assets/thumb-on-fire_1648822523866_0.PNG)")
         _ (graph-parser/parse-file conn "page3.md" "public:: true\n- b31")
         _ (graph-parser/parse-file conn "page3.md" "public:: true\n- b31")
         [filtered-db assets] (publish-db/filter-only-public-pages-and-blocks @conn)
         [filtered-db assets] (publish-db/filter-only-public-pages-and-blocks @conn)
         exported-pages (->> (d/q '[:find (pull ?b [*])
         exported-pages (->> (d/q '[:find (pull ?b [*])
@@ -56,6 +56,8 @@
         "Contains all pages that have been marked public")
         "Contains all pages that have been marked public")
     (is (not (contains? exported-pages "page1"))
     (is (not (contains? exported-pages "page1"))
         "Doesn't contain private page")
         "Doesn't contain private page")
+    (is (seq (d/entity filtered-db [:block/name "page2-alias"]))
+          "Alias of public page is exported")
     (is (= #{"page2" "page3"} exported-block-pages)
     (is (= #{"page2" "page3"} exported-block-pages)
         "Only exports blocks from public pages")
         "Only exports blocks from public pages")
     (is (= ["thumb-on-fire_1648822523866_0.PNG"] assets)
     (is (= ["thumb-on-fire_1648822523866_0.PNG"] assets)

+ 14 - 18
docs/contributing-to-translations.md

@@ -80,27 +80,23 @@ $ bb lang:missing es --copy
 
 
 Almost all translations are small. The only exceptions to this are the keys `:tutorial/text` and `:tutorial/dummy-notes`. These translations are files that are part of the onboarding tutorial and can be found under [src/resources/tutorials/](https://github.com/logseq/logseq/blob/master/src/resources/tutorials/).
 Almost all translations are small. The only exceptions to this are the keys `:tutorial/text` and `:tutorial/dummy-notes`. These translations are files that are part of the onboarding tutorial and can be found under [src/resources/tutorials/](https://github.com/logseq/logseq/blob/master/src/resources/tutorials/).
 
 
-## Fix Untranslated
-
-There is a lot to translate and sometimes we forget to translate a string. To see what translation keys are still left for your language use :
-
-```shell
-$ bb lang:duplicates LOCALE
-
-Keys with duplicate values found:
-
-|                  :translation-key | :duplicate-value |
-|-----------------------------------+------------------|
-|                          :general |          General |
-|                           :logseq |           Logseq |
-|                               :no |               No |
-```
+### Editing Tips
 
 
+* Some translations may include punctuation like `:` or `!`. When translating them, please use the punctuation that makes the most sense for your language as you don't have to follow the English ones.
+* Some translations may include arguments/interpolations e.g. `{1}`. If you see them in a translation, be sure to include them. These arguments are substituted in the string and are usually used something the app needs to calculate e.g. a number. See [these docs](https://github.com/tonsky/tongue#interpolation) for more examples.
 ## Fix Mistakes
 ## Fix Mistakes
 
 
-Sometimes, we typo a translation key or forget to use it. If this happens,
-the github CI step of `bb lang:validate-translations` will detect these errors
-and tell you what's wrong.
+There is a lint command to catch common translation mistakes - `bb
+lang:validate-translations`. This runs for all contribution pull requests so
+you'll need to ensure it doesn't fail. Mistakes that it catches:
+
+* Adding translation entries for nonexistent entries in English.
+    * Most common mistake is mistyping an entry name
+* Adding English entries for translations that don't exist in the UI.
+* Adding translation entries that are just duplicates of the English entry.
+    * This catches contributors copying entries from English and then forgetting to translate. Sometimes you do want to have the translation be the same. For this case, add an entry to `allowed-duplicates` in
+[lang.clj](https://github.com/logseq/logseq/blob/master/scripts/src/logseq/tasks/lang.clj) for your language
+with a list of duplicated entries e.g. `:nb-NO #{:port ...}`.
 
 
 ## Add a Language
 ## Add a Language
 
 

+ 5 - 2
docs/dev-practices.md

@@ -72,11 +72,14 @@ queries and rules. Our queries are linted through clj-kondo and
 [datalog-parser](https://github.com/lambdaforge/datalog-parser). clj-kondo will
 [datalog-parser](https://github.com/lambdaforge/datalog-parser). clj-kondo will
 error if it detects an invalid query.
 error if it detects an invalid query.
 
 
-### Invalid translations
+### Translations
 
 
 Our translations can be configured incorrectly. We can catch some of these
 Our translations can be configured incorrectly. We can catch some of these
 mistakes [as noted here](./contributing-to-translations.md#fix-mistakes).
 mistakes [as noted here](./contributing-to-translations.md#fix-mistakes).
 
 
+Punctuation and delimiting characters (e.g. `:`, `:`, `?`) should be part of the translatable string.
+Those characters and their position may vary depending on the language.
+
 ### Spell Checker
 ### Spell Checker
 
 
 We use [typos](https://github.com/crate-ci/typos) to spell check our source code.
 We use [typos](https://github.com/crate-ci/typos) to spell check our source code.
@@ -282,7 +285,7 @@ point out:
 * `dev:validate-repo-config-edn` - Validate a repo config.edn
 * `dev:validate-repo-config-edn` - Validate a repo config.edn
 
 
   ```sh
   ```sh
-  bb dev:validate-repo-config-edn templates/config.edn
+  bb dev:validate-repo-config-edn src/resources/templates/config.edn
   ```
   ```
 
 
 * `dev:publishing` - Build a publishing app for a given graph dir. If the
 * `dev:publishing` - Build a publishing app for a given graph dir. If the

+ 3 - 1
e2e-tests/code-editing.spec.ts

@@ -9,7 +9,9 @@ import { createRandomPage, escapeToCodeEditor, escapeToBlockEditor } from './uti
  * For more information, see: https://codemirror.net/doc/manual.html
  * For more information, see: https://codemirror.net/doc/manual.html
  */
  */
 
 
-test('switch code editing mode', async ({ page }) => {
+// TODO: Fix test that started intermittently failing some time around
+// https://github.com/logseq/logseq/pull/9540
+test.skip('switch code editing mode', async ({ page }) => {
   await createRandomPage(page)
   await createRandomPage(page)
 
 
   // NOTE: ` will trigger auto-pairing in Logseq
   // NOTE: ` will trigger auto-pairing in Logseq

+ 2 - 1
e2e-tests/editor.spec.ts

@@ -152,6 +152,7 @@ test(
     // This test requires dev mode
     // This test requires dev mode
     test.skip(process.env.RELEASE === 'true', 'not available for release version')
     test.skip(process.env.RELEASE === 'true', 'not available for release version')
 
 
+    // @ts-ignore
     for (let [idx, events] of [
     for (let [idx, events] of [
       kb_events.win10_pinyin_left_full_square_bracket,
       kb_events.win10_pinyin_left_full_square_bracket,
       kb_events.macos_pinyin_left_full_square_bracket
       kb_events.macos_pinyin_left_full_square_bracket
@@ -168,7 +169,7 @@ test(
       expect(await page.inputValue(':nth-match(textarea, 1)')).toBe(check_text + '[[]]')
       expect(await page.inputValue(':nth-match(textarea, 1)')).toBe(check_text + '[[]]')
     };
     };
 
 
-    // dont trigger RIME #3440
+    // @ts-ignore dont trigger RIME #3440
     for (let [idx, events] of [
     for (let [idx, events] of [
       kb_events.macos_pinyin_selecting_candidate_double_left_square_bracket,
       kb_events.macos_pinyin_selecting_candidate_double_left_square_bracket,
       kb_events.win10_RIME_selecting_candidate_double_left_square_bracket
       kb_events.win10_RIME_selecting_candidate_double_left_square_bracket

+ 2 - 1
e2e-tests/fixtures.ts

@@ -27,12 +27,13 @@ const consoleLogWatcher = (msg: ConsoleMessage) => {
 
 
   // List of error messages to ignore
   // List of error messages to ignore
   const ignoreErrors = [
   const ignoreErrors = [
-    /net::ERR_CONNECTION_REFUSED/,
+    /net/,
     /^Error with Permissions-Policy header:/
     /^Error with Permissions-Policy header:/
   ];
   ];
 
 
   // If the text matches any of the ignoreErrors, return early
   // If the text matches any of the ignoreErrors, return early
   if (ignoreErrors.some(error => text.match(error))) {
   if (ignoreErrors.some(error => text.match(error))) {
+    console.log(`WARN:: ${text}\n`)
     return;
     return;
   }
   }
 
 

+ 1 - 0
e2e-tests/history.spec.ts

@@ -43,6 +43,7 @@ test('undo/redo of a renamed page should be preserved', async ({ page, block })
   await page.waitForTimeout(500) // Wait for 500ms autosave period to expire
   await page.waitForTimeout(500) // Wait for 500ms autosave period to expire
 
 
   await renamePage(page, randomString(10))
   await renamePage(page, randomString(10))
+  await page.click('.ui__confirm-modal button')
 
 
   await page.keyboard.press(modKey + '+z')
   await page.keyboard.press(modKey + '+z')
   await page.waitForTimeout(100)
   await page.waitForTimeout(100)

+ 2 - 1
e2e-tests/hotkey.spec.ts

@@ -1,9 +1,10 @@
 import { expect } from '@playwright/test'
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
 import { test } from './fixtures'
-import { createRandomPage, enterNextBlock, lastBlock, modKey, IsLinux } from './utils'
+import { createRandomPage, enterNextBlock, lastBlock, modKey, IsLinux, closeSearchBox } from './utils'
 
 
 test('open search dialog', async ({ page }) => {
 test('open search dialog', async ({ page }) => {
   await page.waitForTimeout(200)
   await page.waitForTimeout(200)
+  await closeSearchBox(page)
   await page.keyboard.press(modKey + '+k')
   await page.keyboard.press(modKey + '+k')
 
 
   await page.waitForSelector('[placeholder="Search or create page"]')
   await page.waitForSelector('[placeholder="Search or create page"]')

+ 105 - 0
e2e-tests/logseq-api.spec.ts

@@ -0,0 +1,105 @@
+import { test } from './fixtures'
+import { expect } from '@playwright/test'
+
+test('block related apis',
+  async ({ page }) => {
+    const callAPI = callPageAPI.bind(null, page)
+
+    const bPageName = 'block-test-page'
+    await callAPI('create_page', bPageName, null, { createFirstBlock: false })
+
+    await page.waitForSelector(`span[data-ref="${bPageName}"]`)
+
+    let p = await callAPI('get_current_page')
+    const bp = await callAPI('append_block_in_page', bPageName, 'tests')
+
+    expect(p.name).toBe(bPageName)
+
+    p = await callAPI('get_page', bPageName)
+
+    expect(p.name).toBe(bPageName)
+
+    await callAPI('edit_block', bp.uuid)
+
+    const b = (await callAPI('get_current_block'))
+    expect(Object.keys(b)).toContain('uuid')
+
+    await page.waitForSelector('.block-editor > textarea')
+    await page.locator('.block-editor > textarea').fill('')
+    const content = 'test api'
+    await page.type('.block-editor > textarea', content)
+
+    const editingContent = await callAPI('get_editing_block_content')
+    expect(editingContent).toBe(content)
+
+    // create
+    let b1 = await callAPI('insert_block', b.uuid, content)
+    b1 = await callAPI('get_block', b1.uuid)
+
+    expect(b1.parent.id).toBe(b.id)
+
+    // update
+    const content1 = content + '+ update!'
+    await callAPI('update_block', b1.uuid, content1)
+    await page.waitForTimeout(1000)
+    b1 = await callAPI('get_block', b1.uuid)
+
+    expect(b1.content).toBe(content1)
+
+    // remove
+    await callAPI('remove_block', b1.uuid)
+    b1 = await callAPI('get_block', b1.uuid)
+
+    expect(b1).toBeNull()
+
+    // traverse
+    b1 = await callAPI('insert_block', b.uuid, content1, { sibling: true })
+    const nb = await callAPI('get_next_sibling_block', b.uuid)
+    const pb = await callAPI('get_previous_sibling_block', b1.uuid)
+
+    expect(nb.uuid).toBe(b1.uuid)
+    expect(pb.uuid).toBe(b.uuid)
+
+    // move
+    await callAPI('move_block', b.uuid, b1.uuid)
+    const mb = await callAPI('get_next_sibling_block', b1.uuid)
+
+    expect(mb.uuid).toBe(b.uuid)
+
+    // properties
+    await callAPI('upsert_block_property', b1.uuid, 'a', 1)
+    let prop1 = await callAPI('get_block_property', b1.uuid, 'a')
+
+    expect(prop1).toBe(1)
+
+    await callAPI('upsert_block_property', b1.uuid, 'a', 2)
+    prop1 = await callAPI('get_block_property', b1.uuid, 'a')
+
+    expect(prop1).toBe(2)
+
+    await callAPI('remove_block_property', b1.uuid, 'a')
+    prop1 = await callAPI('get_block_property', b1.uuid, 'a')
+
+    expect(prop1).toBeNull()
+
+    await callAPI('upsert_block_property', b1.uuid, 'a', 1)
+    await callAPI('upsert_block_property', b1.uuid, 'b', 1)
+
+    prop1 = await callAPI('get_block_properties', b1.uuid)
+
+    expect(prop1).toEqual({ a: 1, b: 1 })
+
+    // await page.pause()
+  })
+
+/**
+ * @param page
+ * @param method
+ * @param args
+ */
+export async function callPageAPI(page, method, ...args) {
+  return await page.evaluate(([method, args]) => {
+    // @ts-ignore
+    return window.logseq.api[method]?.(...args)
+  }, [method, args])
+}

+ 13 - 1
e2e-tests/page-rename.spec.ts

@@ -1,6 +1,6 @@
 import { expect, Page } from '@playwright/test'
 import { expect, Page } from '@playwright/test'
 import { test } from './fixtures'
 import { test } from './fixtures'
-import { createPage, randomLowerString, randomString, renamePage } from './utils'
+import { closeSearchBox, createPage, randomLowerString, randomString, renamePage, searchPage } from './utils'
 
 
 /***
 /***
  * Test rename feature
  * Test rename feature
@@ -15,6 +15,7 @@ async function page_rename_test(page: Page, original_page_name: string, new_page
 
 
   // Rename page in UI
   // Rename page in UI
   await renamePage(page, new_name)
   await renamePage(page, new_name)
+  await page.click('.ui__confirm-modal button')
 
 
   expect(await page.innerText('.page-title .title')).toBe(new_name)
   expect(await page.innerText('.page-title .title')).toBe(new_name)
 
 
@@ -45,6 +46,7 @@ async function homepage_rename_test(page: Page, original_page_name: string, new_
   expect(await page.locator('.home-nav span.flex-1').innerText()).toBe(original_name);
   expect(await page.locator('.home-nav span.flex-1').innerText()).toBe(original_name);
 
 
   await renamePage(page, new_name)
   await renamePage(page, new_name)
+  await page.click('.ui__confirm-modal button')
 
 
   expect(await page.locator('.home-nav span.flex-1').innerText()).toBe(new_name);
   expect(await page.locator('.home-nav span.flex-1').innerText()).toBe(new_name);
 
 
@@ -64,6 +66,16 @@ test('page rename test', async ({ page }) => {
   await page_rename_test(page, "abcd", "a.b.c.d")
   await page_rename_test(page, "abcd", "a.b.c.d")
   await page_rename_test(page, "abcd", "a/b/c/d")
   await page_rename_test(page, "abcd", "a/b/c/d")
 
 
+  // The page name in page search are not updated after changing the capitalization of the page name #9577
+  // https://github.com/logseq/logseq/issues/9577
+  // Expect the page name to be updated in the search results
+  await page_rename_test(page, "DcBA_", "dCBA_")
+  const results = await searchPage(page, "DcBA_")
+  // search result 0 is the new page & 1 is the new whiteboard
+  const thirdResultRow = await results[2].innerText()
+  expect(thirdResultRow).toContain("dCBA_");
+  expect(thirdResultRow).not.toContain("DcBA_");
+  await closeSearchBox(page)
 })
 })
 
 
 // TODO introduce more samples when #4722 is fixed
 // TODO introduce more samples when #4722 is fixed

+ 1 - 11
e2e-tests/plugins.spec.ts

@@ -1,5 +1,6 @@
 import { expect } from '@playwright/test'
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
 import { test } from './fixtures'
+import { callPageAPI } from './logseq-api.spec'
 
 
 test.skip('enabled plugin system default', async ({ page }) => {
 test.skip('enabled plugin system default', async ({ page }) => {
   const callAPI = callPageAPI.bind(null, page)
   const callAPI = callPageAPI.bind(null, page)
@@ -59,14 +60,3 @@ test.skip('play a plugin<logseq-journals-calendar> from the Marketplace', async
   await expect(page.locator('body[data-page="page"]')).toBeVisible()
   await expect(page.locator('body[data-page="page"]')).toBeVisible()
 })
 })
 
 
-/**
- * @param page
- * @param method
- * @param args
- */
-async function callPageAPI(page, method, ...args) {
-  return await page.evaluate(([method, args]) => {
-    // @ts-ignore
-    return window.logseq.api[method]?.(...args)
-  }, [method, args])
-}

+ 0 - 1
e2e-tests/util/page.ts

@@ -11,5 +11,4 @@ export async function renamePage(page: Page, new_name: string) {
   await page.fill('input[type="text"]', '')
   await page.fill('input[type="text"]', '')
   await page.type('.title input', new_name)
   await page.type('.title input', new_name)
   await page.keyboard.press('Enter')
   await page.keyboard.press('Enter')
-  await page.click('.ui__confirm-modal button')
 }
 }

+ 32 - 29
e2e-tests/util/search-modal.ts

@@ -1,63 +1,66 @@
 import { Page, Locator, ElementHandle } from '@playwright/test'
 import { Page, Locator, ElementHandle } from '@playwright/test'
 import { randomString } from './basic'
 import { randomString } from './basic'
 
 
+export async function closeSearchBox(page: Page): Promise<void> {
+    await page.keyboard.press("Escape") // escape (potential) search box typing
+    await page.waitForTimeout(500)
+    await page.keyboard.press("Escape") // escape modal
+}
+
 export async function createRandomPage(page: Page) {
 export async function createRandomPage(page: Page) {
     const randomTitle = randomString(20)
     const randomTitle = randomString(20)
-  
+    await closeSearchBox(page)
     // Click #search-button
     // Click #search-button
     await page.click('#search-button')
     await page.click('#search-button')
     // Fill [placeholder="Search or create page"]
     // Fill [placeholder="Search or create page"]
     await page.fill('[placeholder="Search or create page"]', randomTitle)
     await page.fill('[placeholder="Search or create page"]', randomTitle)
     // Click text=/.*New page: "new page".*/
     // Click text=/.*New page: "new page".*/
-    await page.click('text=/.*New page: ".*/')
+    await page.click('text=/.*New page:".*/')
     // Wait for h1 to be from our new page
     // Wait for h1 to be from our new page
     await page.waitForSelector(`h1 >> text="${randomTitle}"`, { state: 'visible' })
     await page.waitForSelector(`h1 >> text="${randomTitle}"`, { state: 'visible' })
     // wait for textarea of first block
     // wait for textarea of first block
     await page.waitForSelector('textarea >> nth=0', { state: 'visible' })
     await page.waitForSelector('textarea >> nth=0', { state: 'visible' })
-  
+
     return randomTitle;
     return randomTitle;
-  }
-  
-  export async function createPage(page: Page, page_name: string) {// Click #search-button
+}
+
+export async function createPage(page: Page, page_name: string) {// Click #search-button
+    await closeSearchBox(page)
     await page.click('#search-button')
     await page.click('#search-button')
     // Fill [placeholder="Search or create page"]
     // Fill [placeholder="Search or create page"]
     await page.fill('[placeholder="Search or create page"]', page_name)
     await page.fill('[placeholder="Search or create page"]', page_name)
     // Click text=/.*New page: "new page".*/
     // Click text=/.*New page: "new page".*/
-    await page.click('text=/.*New page: ".*/')
+    await page.click('text=/.*New page:".*/')
     // wait for textarea of first block
     // wait for textarea of first block
     await page.waitForSelector('textarea >> nth=0', { state: 'visible' })
     await page.waitForSelector('textarea >> nth=0', { state: 'visible' })
-  
+
     return page_name;
     return page_name;
   }
   }
-  
-  export async function searchAndJumpToPage(page: Page, pageTitle: string) {
+
+export async function searchAndJumpToPage(page: Page, pageTitle: string) {
+    await closeSearchBox(page)
     await page.click('#search-button')
     await page.click('#search-button')
     await page.type('[placeholder="Search or create page"]', pageTitle)
     await page.type('[placeholder="Search or create page"]', pageTitle)
     await page.waitForSelector(`[data-page-ref="${pageTitle}"]`, { state: 'visible' })
     await page.waitForSelector(`[data-page-ref="${pageTitle}"]`, { state: 'visible' })
     page.click(`[data-page-ref="${pageTitle}"]`)
     page.click(`[data-page-ref="${pageTitle}"]`)
     await page.waitForNavigation()
     await page.waitForNavigation()
     return pageTitle;
     return pageTitle;
-  }
-  
-  /**
-   * type a search query into the search box
-   * stop at the point where search box shows up
-   * 
-   * @param page the pw page object
-   * @param query the search query to type into the search box
-   * @returns the HTML element for the search results ui
-   */
-  export async function searchPage(page: Page, query: string): Promise<ElementHandle<SVGElement | HTMLElement>[]>{
+}
+
+/**
+ * type a search query into the search box
+ * stop at the point where search box shows up
+ *
+ * @param page the pw page object
+ * @param query the search query to type into the search box
+ * @returns the HTML element for the search results ui
+ */
+export async function searchPage(page: Page, query: string): Promise<ElementHandle<SVGElement | HTMLElement>[]>{
+    await closeSearchBox(page)
     await page.click('#search-button')
     await page.click('#search-button')
     await page.waitForSelector('[placeholder="Search or create page"]')
     await page.waitForSelector('[placeholder="Search or create page"]')
     await page.type('[placeholder="Search or create page"]', query, { delay: 10 })
     await page.type('[placeholder="Search or create page"]', query, { delay: 10 })
     await page.waitForTimeout(2000) // wait longer for search contents to render
     await page.waitForTimeout(2000) // wait longer for search contents to render
-  
+
     return page.$$('#ui__ac-inner>div');
     return page.$$('#ui__ac-inner>div');
-  }
-  
-  export async function closeSearchBox(page: Page): Promise<void> {
-    await page.keyboard.press("Escape") // escape (potential) search box typing
-    await page.waitForTimeout(500)
-    await page.keyboard.press("Escape") // escape modal
-  }
+}

+ 1 - 0
e2e-tests/utils.ts

@@ -2,6 +2,7 @@ import { Page, Locator } from 'playwright'
 import { expect, ConsoleMessage } from '@playwright/test'
 import { expect, ConsoleMessage } from '@playwright/test'
 import * as pathlib from 'path'
 import * as pathlib from 'path'
 import { modKey } from './util/basic'
 import { modKey } from './util/basic'
+import { Block } from './types'
 
 
 // TODO: The file should be a facade of utils in the /util folder
 // TODO: The file should be a facade of utils in the /util folder
 // No more additional functions should be added to this file
 // No more additional functions should be added to this file

+ 97 - 21
e2e-tests/whiteboards.spec.ts

@@ -1,6 +1,6 @@
 import { expect } from '@playwright/test'
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
 import { test } from './fixtures'
-import { modKey } from './utils'
+import { modKey, renamePage } from './utils'
 
 
 test('enable whiteboards', async ({ page }) => {
 test('enable whiteboards', async ({ page }) => {
   if (await page.$('.nav-header .whiteboard') === null) {
   if (await page.$('.nav-header .whiteboard') === null) {
@@ -85,10 +85,10 @@ test('draw a rectangle', async ({ page }) => {
 
 
   await page.keyboard.type('wr')
   await page.keyboard.type('wr')
 
 
-  await page.mouse.move(bounds.x + 5, bounds.y + 5)
+  await page.mouse.move(bounds.x + 105, bounds.y + 105)
   await page.mouse.down()
   await page.mouse.down()
 
 
-  await page.mouse.move(bounds.x + 50, bounds.y + 50 )
+  await page.mouse.move(bounds.x + 150, bounds.y + 150 )
   await page.mouse.up()
   await page.mouse.up()
   await page.keyboard.press('Escape')
   await page.keyboard.press('Escape')
 
 
@@ -114,12 +114,14 @@ test('clone the rectangle', async ({ page }) => {
   const canvas = await page.waitForSelector('.logseq-tldraw')
   const canvas = await page.waitForSelector('.logseq-tldraw')
   const bounds = (await canvas.boundingBox())!
   const bounds = (await canvas.boundingBox())!
 
 
-  await page.mouse.move(bounds.x + 20, bounds.y + 20, {steps: 5})
+  await page.mouse.move(bounds.x + 400, bounds.y + 400)
+
+  await page.mouse.move(bounds.x + 120, bounds.y + 120, {steps: 5})
 
 
   await page.keyboard.down('Alt')
   await page.keyboard.down('Alt')
   await page.mouse.down()
   await page.mouse.down()
 
 
-  await page.mouse.move(bounds.x + 100, bounds.y + 100, {steps: 5})
+  await page.mouse.move(bounds.x + 200, bounds.y + 200, {steps: 5})
   await page.mouse.up()
   await page.mouse.up()
   await page.keyboard.up('Alt')
   await page.keyboard.up('Alt')
 
 
@@ -163,10 +165,10 @@ test('connect rectangles with an arrow', async ({ page }) => {
 
 
   await page.keyboard.type('wc')
   await page.keyboard.type('wc')
 
 
-  await page.mouse.move(bounds.x + 20, bounds.y + 20)
+  await page.mouse.move(bounds.x + 120, bounds.y + 120)
   await page.mouse.down()
   await page.mouse.down()
 
 
-  await page.mouse.move(bounds.x + 100, bounds.y + 100, {steps: 5}) // will fail without steps
+  await page.mouse.move(bounds.x + 200, bounds.y + 200, {steps: 5}) // will fail without steps
   await page.mouse.up()
   await page.mouse.up()
   await page.keyboard.press('Escape')
   await page.keyboard.press('Escape')
 
 
@@ -191,10 +193,15 @@ test('undo the delete action', async ({ page }) => {
 })
 })
 
 
 test('convert the first rectangle to ellipse', async ({ page }) => {
 test('convert the first rectangle to ellipse', async ({ page }) => {
+  const canvas = await page.waitForSelector('.logseq-tldraw')
+  const bounds = (await canvas.boundingBox())!
+
   await page.keyboard.press('Escape')
   await page.keyboard.press('Escape')
-  await page.waitForTimeout(1000)
-  await page.click('.logseq-tldraw .tl-box-container:first-of-type')
-  await page.mouse.move(0, 0)  // move mouse to trigger a rerender of the context bar
+  await page.mouse.move(bounds.x + 220, bounds.y + 220)
+  await page.mouse.down()
+  await page.mouse.up()
+  await page.mouse.move(bounds.x + 520, bounds.y + 520)
+
   await page.click('.tl-context-bar .tl-geometry-tools-pane-anchor')
   await page.click('.tl-context-bar .tl-geometry-tools-pane-anchor')
   await page.click('.tl-context-bar .tl-geometry-toolbar [data-tool=ellipse]')
   await page.click('.tl-context-bar .tl-geometry-toolbar [data-tool=ellipse]')
 
 
@@ -223,9 +230,14 @@ test('undo the shape conversion', async ({ page }) => {
 })
 })
 
 
 test('locked elements should not be removed', async ({ page }) => {
 test('locked elements should not be removed', async ({ page }) => {
+  const canvas = await page.waitForSelector('.logseq-tldraw')
+  const bounds = (await canvas.boundingBox())!
+
   await page.keyboard.press('Escape')
   await page.keyboard.press('Escape')
-  await page.waitForTimeout(1000)
-  await page.click('.logseq-tldraw .tl-box-container:first-of-type')
+  await page.mouse.move(bounds.x + 220, bounds.y + 220)
+  await page.mouse.down()
+  await page.mouse.up()
+  await page.mouse.move(bounds.x + 520, bounds.y + 520)
   await page.keyboard.press(`${modKey}+l`)
   await page.keyboard.press(`${modKey}+l`)
   await page.keyboard.press('Delete')
   await page.keyboard.press('Delete')
   await page.keyboard.press(`${modKey}+Shift+l`)
   await page.keyboard.press(`${modKey}+Shift+l`)
@@ -269,7 +281,7 @@ test('create a block', async ({ page }) => {
   const bounds = (await canvas.boundingBox())!
   const bounds = (await canvas.boundingBox())!
 
 
   await page.keyboard.type('ws')
   await page.keyboard.type('ws')
-  await page.mouse.dblclick(bounds.x + 5, bounds.y + 5)
+  await page.mouse.dblclick(bounds.x + 105, bounds.y + 105)
   await page.waitForTimeout(100)
   await page.waitForTimeout(100)
 
 
   await page.keyboard.type('a')
   await page.keyboard.type('a')
@@ -279,15 +291,17 @@ test('create a block', async ({ page }) => {
   await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container')).toHaveCount(1)
   await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container')).toHaveCount(1)
 })
 })
 
 
-test('expand the block', async ({ page }) => {
+// TODO: Fix the failing test
+test.skip('expand the block', async ({ page }) => {
   await page.keyboard.press('Escape')
   await page.keyboard.press('Escape')
-  await page.click('.logseq-tldraw .tl-context-bar .tie-object-expanded ')
+  await page.keyboard.press(modKey + '+ArrowDown')
   await page.waitForTimeout(100)
   await page.waitForTimeout(100)
 
 
   await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container .tl-logseq-portal-header')).toHaveCount(1)
   await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container .tl-logseq-portal-header')).toHaveCount(1)
 })
 })
 
 
-test('undo the expand action', async ({ page }) => {
+// TODO: Depends on the previous test
+test.skip('undo the expand action', async ({ page }) => {
   await page.keyboard.press(modKey + '+z')
   await page.keyboard.press(modKey + '+z')
 
 
   await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container .tl-logseq-portal-header')).toHaveCount(0)
   await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container .tl-logseq-portal-header')).toHaveCount(0)
@@ -304,7 +318,7 @@ test('copy/paste url to create an iFrame shape', async ({ page }) => {
   const bounds = (await canvas.boundingBox())!
   const bounds = (await canvas.boundingBox())!
 
 
   await page.keyboard.type('wt')
   await page.keyboard.type('wt')
-  await page.mouse.move(bounds.x + 5, bounds.y + 5)
+  await page.mouse.move(bounds.x + 105, bounds.y + 105)
   await page.mouse.down()
   await page.mouse.down()
   await page.waitForTimeout(100)
   await page.waitForTimeout(100)
 
 
@@ -323,7 +337,7 @@ test('copy/paste twitter status url to create a Tweet shape', async ({ page }) =
   const bounds = (await canvas.boundingBox())!
   const bounds = (await canvas.boundingBox())!
 
 
   await page.keyboard.type('wt')
   await page.keyboard.type('wt')
-  await page.mouse.move(bounds.x + 5, bounds.y + 5)
+  await page.mouse.move(bounds.x + 105, bounds.y + 105)
   await page.mouse.down()
   await page.mouse.down()
   await page.waitForTimeout(100)
   await page.waitForTimeout(100)
 
 
@@ -342,7 +356,7 @@ test('copy/paste youtube video url to create a Youtube shape', async ({ page })
   const bounds = (await canvas.boundingBox())!
   const bounds = (await canvas.boundingBox())!
 
 
   await page.keyboard.type('wt')
   await page.keyboard.type('wt')
-  await page.mouse.move(bounds.x + 5, bounds.y + 5)
+  await page.mouse.move(bounds.x + 105, bounds.y + 105)
   await page.mouse.down()
   await page.mouse.down()
   await page.waitForTimeout(100)
   await page.waitForTimeout(100)
 
 
@@ -394,8 +408,8 @@ test('quick add another whiteboard', async ({ page }) => {
   const canvas = await page.waitForSelector('.logseq-tldraw')
   const canvas = await page.waitForSelector('.logseq-tldraw')
   await canvas.dblclick({
   await canvas.dblclick({
     position: {
     position: {
-      x: 100,
-      y: 100,
+      x: 200,
+      y: 200,
     },
     },
   })
   })
 
 
@@ -425,3 +439,65 @@ test('go to another board and check reference', async ({ page }) => {
   const pageRefCount$ = page.locator('.whiteboard-page-refs-count')
   const pageRefCount$ = page.locator('.whiteboard-page-refs-count')
   await expect(pageRefCount$.locator('.open-page-ref-link')).toContainText('1')
   await expect(pageRefCount$.locator('.open-page-ref-link')).toContainText('1')
 })
 })
+
+test('Create an embedded whiteboard', async ({ page }) => {
+  const canvas = await page.waitForSelector('.logseq-tldraw')
+  await canvas.dblclick({
+    position: {
+      x: 110,
+      y: 110,
+    },
+  })
+
+  const quickAdd$ = page.locator('.tl-quick-search')
+  await expect(quickAdd$).toBeVisible()
+
+  await page.fill('.tl-quick-search input', 'My embedded whiteboard')
+  await quickAdd$
+    .locator('div[data-index="2"] .tl-quick-search-option')
+    .first()
+    .click()
+
+  await expect(quickAdd$).toBeHidden()
+  await expect(page.locator('.tl-logseq-portal-header a')).toContainText('My embedded whiteboard')
+})
+
+test('New whiteboard should have the correct name', async ({ page }) => {
+  page.locator('.tl-logseq-portal-header a').click()
+
+  await expect(page.locator('.whiteboard-page-title')).toContainText('My embedded whiteboard')
+})
+
+test('Create an embedded page', async ({ page }) => {
+  const canvas = await page.waitForSelector('.logseq-tldraw')
+  await canvas.dblclick({
+    position: {
+      x: 150,
+      y: 150,
+    },
+  })
+
+  const quickAdd$ = page.locator('.tl-quick-search')
+  await expect(quickAdd$).toBeVisible()
+
+  await page.fill('.tl-quick-search input', 'My page')
+  await quickAdd$
+    .locator('div[data-index="1"] .tl-quick-search-option')
+    .first()
+    .click()
+
+  await expect(quickAdd$).toBeHidden()
+  await expect(page.locator('.tl-logseq-portal-header a')).toContainText('My page')
+})
+
+test('New page should have the correct name', async ({ page }) => {
+  page.locator('.tl-logseq-portal-header a').click()
+
+  await expect(page.locator('.ls-page-title')).toContainText('My page')
+})
+
+test('Renaming a page to an existing whiteboard name should be prohibited', async ({ page }) => {
+  await renamePage(page, "My embedded whiteboard")
+
+  await expect(page.locator('.page-title input')).toHaveValue('My page')
+})

+ 34 - 0
e2e-tests/window.spec.ts

@@ -0,0 +1,34 @@
+import { expect } from '@playwright/test'
+import { test } from './fixtures'
+import { IsMac } from './utils';
+
+if (!IsMac) {
+    test('window should not be maximized on first launch', async ({ page, app }) => {
+        await expect(page.locator('.window-controls .maximize-toggle.maximize')).toHaveCount(1)
+    })
+
+    test('window should be maximized and icon should change on maximize-toggle click', async ({ page }) => {
+        await page.click('.window-controls .maximize-toggle.maximize')
+
+        await expect(page.locator('.window-controls .maximize-toggle.restore')).toHaveCount(1)
+    })
+
+    test('window should be restored and icon should change on maximize-toggle click', async ({ page }) => {
+        await page.click('.window-controls .maximize-toggle.restore')
+
+        await expect(page.locator('.window-controls .maximize-toggle.maximize')).toHaveCount(1)
+    })
+
+    test('window controls should be hidden on fullscreen mode', async ({ page }) => {
+        // Keyboard press F11 won't work, probably because it's a chromium shortcut (not a document event)
+        await page.evaluate(`window.document.body.requestFullscreen()`)
+
+        await expect(page.locator('.window-controls .maximize-toggle')).toHaveCount(0)
+    })
+
+    test('window controls should be visible when we exit fullscreen mode', async ({ page }) => {
+        await page.click('.window-controls .fullscreen-toggle')
+
+        await expect(page.locator('.window-controls')).toHaveCount(1)
+    })
+}

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

@@ -519,7 +519,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.9.8;
+				MARKETING_VERSION = 0.9.9;
 				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)";
@@ -546,7 +546,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.9.8;
+				MARKETING_VERSION = 0.9.9;
 				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 = "";
@@ -571,7 +571,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.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.9.8;
+				MARKETING_VERSION = 0.9.9;
 				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;
@@ -598,7 +598,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.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.9.8;
+				MARKETING_VERSION = 0.9.9;
 				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)";

+ 13 - 5
ios/App/App/FsWatcher.swift

@@ -56,7 +56,7 @@ public class FsWatcher: CAPPlugin, PollingWatcherDelegate {
             DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
             DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                 self.notifyListeners("watcher", data: ["event": "unlink",
                 self.notifyListeners("watcher", data: ["event": "unlink",
                                                        "dir": baseUrl.description as Any,
                                                        "dir": baseUrl.description as Any,
-                                                       "path": url.description
+                                                       "path": url.relativePath(from: baseUrl)?.precomposedStringWithCanonicalMapping as Any
                 ])
                 ])
             }
             }
         case .Add, .Change:
         case .Add, .Change:
@@ -173,11 +173,11 @@ public class PollingWatcher {
     public init?(at: URL) {
     public init?(at: URL) {
         url = at
         url = at
     }
     }
-    
+
     public func start() {
     public func start() {
-        
+
         self.tick(notify: false)
         self.tick(notify: false)
-        
+
         let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".timer")
         let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".timer")
         timer = DispatchSource.makeTimerSource(queue: queue)
         timer = DispatchSource.makeTimerSource(queue: queue)
         timer!.setEventHandler(qos: .background, flags: []) { [weak self] in
         timer!.setEventHandler(qos: .background, flags: []) { [weak self] in
@@ -278,10 +278,18 @@ extension URL {
             return nil
             return nil
         }
         }
 
 
+        // NOTE: standardizedFileURL will remove `/private` prefix
+        // If the file is not exist, it won't remove the prefix.
+
         // Remove/replace "." and "..", make paths absolute:
         // Remove/replace "." and "..", make paths absolute:
-        let destComponents = self.standardizedFileURL.pathComponents
+        var destComponents = self.standardizedFileURL.pathComponents
         let baseComponents = base.standardizedFileURL.pathComponents
         let baseComponents = base.standardizedFileURL.pathComponents
 
 
+        // replace "private" when needed
+        if destComponents.count > 1 && destComponents[1] == "private" && baseComponents.count > 1 && baseComponents[1] != "private" {
+            destComponents.remove(at: 1)
+        }
+
         // Find number of common path components:
         // Find number of common path components:
         var i = 0
         var i = 0
         while i < destComponents.count && i < baseComponents.count
         while i < destComponents.count && i < baseComponents.count

+ 1 - 1
libs/src/LSPlugin.core.ts

@@ -233,7 +233,7 @@ function initMainUIHandlers(pluginLocal: PluginLocal) {
   pluginLocal.on(_('attrs'), (attrs: Partial<UIContainerAttrs>) => {
   pluginLocal.on(_('attrs'), (attrs: Partial<UIContainerAttrs>) => {
     const el = pluginLocal.getMainUIContainer()
     const el = pluginLocal.getMainUIContainer()
     Object.entries(attrs).forEach(([k, v]) => {
     Object.entries(attrs).forEach(([k, v]) => {
-      el?.setAttribute(k, v)
+      el?.setAttribute(k, String(v))
       if (k === 'draggable' && v) {
       if (k === 'draggable' && v) {
         pluginLocal._dispose(
         pluginLocal._dispose(
           pluginLocal._setupDraggableContainer(el, {
           pluginLocal._setupDraggableContainer(el, {

+ 24 - 20
libs/src/LSPlugin.ts

@@ -33,8 +33,6 @@ export type StyleOptions = {
 export type UIContainerAttrs = {
 export type UIContainerAttrs = {
   draggable: boolean
   draggable: boolean
   resizable: boolean
   resizable: boolean
-
-  [key: string]: any
 }
 }
 
 
 export type UIBaseOptions = {
 export type UIBaseOptions = {
@@ -75,20 +73,34 @@ export interface LSPluginPkgConfig {
   mode: 'shadow' | 'iframe'
   mode: 'shadow' | 'iframe'
   themes: Theme[]
   themes: Theme[]
   icon: string
   icon: string
-
-  [key: string]: any
+  /**
+   * Alternative entrypoint for development.
+   */
+  devEntry: unknown
+  /**
+   * For legacy themes, do not use.
+   */
+  theme: unknown
 }
 }
 
 
 export interface LSPluginBaseInfo {
 export interface LSPluginBaseInfo {
-  id: string // should be unique
+  /**
+   * Must be unique.
+   */
+  id: string
   mode: 'shadow' | 'iframe'
   mode: 'shadow' | 'iframe'
-
   settings: {
   settings: {
     disabled: boolean
     disabled: boolean
-    [key: string]: any
-  }
-
-  [key: string]: any
+  } & Record<string, unknown>
+  effect: boolean
+  /**
+   * For internal use only. Indicates if plugin is installed in dot root.
+   */
+  iir: boolean
+  /**
+   * For internal use only.
+   */
+  lsr: string
 }
 }
 
 
 export type IHookEvent = {
 export type IHookEvent = {
@@ -146,8 +158,6 @@ export interface AppUserConfigs {
   showBracket: boolean
   showBracket: boolean
   enabledFlashcards: boolean
   enabledFlashcards: boolean
   enabledJournals: boolean
   enabledJournals: boolean
-
-  [key: string]: any
 }
 }
 
 
 /**
 /**
@@ -157,8 +167,6 @@ export interface AppGraphInfo {
   name: string
   name: string
   url: string
   url: string
   path: string
   path: string
-
-  [key: string]: any
 }
 }
 
 
 /**
 /**
@@ -184,8 +192,6 @@ export interface BlockEntity {
   level?: number
   level?: number
   meta?: { timestamps: any; properties: any; startPos: number; endPos: number }
   meta?: { timestamps: any; properties: any; startPos: number; endPos: number }
   title?: Array<any>
   title?: Array<any>
-
-  [key: string]: any
 }
 }
 
 
 /**
 /**
@@ -205,8 +211,6 @@ export interface PageEntity {
   format?: 'markdown' | 'org'
   format?: 'markdown' | 'org'
   journalDay?: number
   journalDay?: number
   updatedAt?: number
   updatedAt?: number
-
-  [key: string]: any
 }
 }
 
 
 export type BlockIdentity = BlockUUID | Pick<BlockEntity, 'uuid'>
 export type BlockIdentity = BlockUUID | Pick<BlockEntity, 'uuid'>
@@ -1078,8 +1082,8 @@ export interface ILSPluginUser extends EventEmitter<LSPluginUserEvents> {
 
 
   resolveResourceFullUrl(filePath: string): string
   resolveResourceFullUrl(filePath: string): string
 
 
-  App: IAppProxy & Record<string, any>
-  Editor: IEditorProxy & Record<string, any>
+  App: IAppProxy
+  Editor: IEditorProxy
   DB: IDBProxy
   DB: IDBProxy
   Git: IGitProxy
   Git: IGitProxy
   UI: IUIProxy
   UI: IUIProxy

+ 2 - 2
package.json

@@ -117,7 +117,7 @@
         "highlight.js": "10.4.1",
         "highlight.js": "10.4.1",
         "ignore": "5.1.8",
         "ignore": "5.1.8",
         "jszip": "3.8.0",
         "jszip": "3.8.0",
-        "mldoc": "^1.5.1",
+        "mldoc": "^1.5.5",
         "path": "0.12.7",
         "path": "0.12.7",
         "path-complete-extname": "1.0.0",
         "path-complete-extname": "1.0.0",
         "pixi-graph-fork": "0.2.0",
         "pixi-graph-fork": "0.2.0",
@@ -158,4 +158,4 @@
         "pixi-graph-fork/@pixi/mixin-get-child-by-name": "6.2.0",
         "pixi-graph-fork/@pixi/mixin-get-child-by-name": "6.2.0",
         "pixi-graph-fork/@pixi/math": "6.2.0"
         "pixi-graph-fork/@pixi/math": "6.2.0"
     }
     }
-}
+}

+ 18 - 2
playwright.config.ts

@@ -1,12 +1,28 @@
 import { PlaywrightTestConfig } from '@playwright/test'
 import { PlaywrightTestConfig } from '@playwright/test'
 
 
 const config: PlaywrightTestConfig = {
 const config: PlaywrightTestConfig = {
+  // The directory where the tests are located
+  // The order of the tests is determined by the file names alphabetically.
   testDir: './e2e-tests',
   testDir: './e2e-tests',
+
+  // The number of retries before marking a test as failed.
   maxFailures: 1,
   maxFailures: 1,
-  workers: 1, // NOTE: must be 1 for now, otherwise tests will fail.
+
+  // The number of Logseq instances to run in parallel.
+  // NOTE: must be 1 for now, otherwise tests will fail.
+  workers: 1,
+
+  // 'github' for GitHub Actions CI to generate annotations, plus a concise 'dot'.
+  // default 'list' when running locally.
+  reporter: process.env.CI ? 'github' : 'list',
+
+  // Fail the build on CI if test.only is present.
+  forbidOnly: !!process.env.CI,
+
   use: {
   use: {
+    // SCapture screenshot after each test failure.
     screenshot: 'only-on-failure',
     screenshot: 'only-on-failure',
-  }
+  },
 }
 }
 
 
 export default config
 export default config

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
resources/css/katex.min.css


Fișier diff suprimat deoarece este prea mare
+ 0 - 0
resources/js/katex.min.js


+ 2 - 2
resources/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "Logseq",
   "name": "Logseq",
   "productName": "Logseq",
   "productName": "Logseq",
-  "version": "0.9.8",
+  "version": "0.9.9",
   "main": "electron.js",
   "main": "electron.js",
   "author": "Logseq",
   "author": "Logseq",
   "license": "AGPL-3.0",
   "license": "AGPL-3.0",
@@ -22,7 +22,7 @@
   "dependencies": {
   "dependencies": {
     "better-sqlite3": "8.0.1",
     "better-sqlite3": "8.0.1",
     "chokidar": "^3.5.1",
     "chokidar": "^3.5.1",
-    "dugite": "1.108.0",
+    "dugite": "2.5.0",
     "electron-dl": "3.3.0",
     "electron-dl": "3.3.0",
     "electron-log": "4.3.1",
     "electron-log": "4.3.1",
     "electron-squirrel-startup": "1.0.0",
     "electron-squirrel-startup": "1.0.0",

+ 53 - 20
scripts/src/logseq/tasks/lang.clj

@@ -115,6 +115,14 @@
    "(t title" []
    "(t title" []
    "(t subtitle" [:asset/physical-delete]})
    "(t subtitle" [:asset/physical-delete]})
 
 
+(defn- whiteboard-dicts
+  []
+  (->> (shell {:out :string}
+              "grep -E -oh" "\\bt\\('[^ ']+" "-r" "tldraw/apps/tldraw-logseq/src/components")
+       :out
+       string/split-lines
+       (map #(keyword (subs % 3)))))
+
 (defn- validate-ui-translations-are-used
 (defn- validate-ui-translations-are-used
   "This validation checks to see that translations done by (t ...) are equal to
   "This validation checks to see that translations done by (t ...) are equal to
   the ones defined for the default :en lang. This catches translations that have
   the ones defined for the default :en lang. This catches translations that have
@@ -129,6 +137,7 @@
                           string/split-lines
                           string/split-lines
                           (map #(keyword (subs % 4)))
                           (map #(keyword (subs % 4)))
                           (concat (mapcat val manual-ui-dicts))
                           (concat (mapcat val manual-ui-dicts))
+                          (concat (whiteboard-dicts))
                           set)
                           set)
         expected-dicts (set (remove #(re-find #"^(command|shortcut)\." (str (namespace %)))
         expected-dicts (set (remove #(re-find #"^(command|shortcut)\." (str (namespace %)))
                                     (keys (:en (get-dicts)))))
                                     (keys (:en (get-dicts)))))
@@ -145,31 +154,55 @@
           (task-util/print-table (map #(hash-map :invalid-key %) expected-only)))
           (task-util/print-table (map #(hash-map :invalid-key %) expected-only)))
         (System/exit 1)))))
         (System/exit 1)))))
 
 
-(defn validate-translations
-  "Runs multiple translation validations that fail fast if one of them is invalid"
-  []
-  (validate-non-default-languages)
-  (validate-ui-translations-are-used))
+(def allowed-duplicates
+  "Allows certain keys in a language to have the same translation
+   as English. Happens more in romance languages but pretty rare otherwise"
+  {:fr #{:port :type :help/docs :search-item/page :shortcut.category/navigating :text/image
+         :settings-of-plugins}
+   :de #{:graph :host :plugins :port :right-side-bar/whiteboards :search-item/block
+         :settings-of-plugins :search-item/whiteboard :shortcut.category/navigating
+         :settings-page/enable-tooltip :settings-page/enable-whiteboards :settings-page/plugin-system}
+   :es #{:settings-page/tab-general :settings-page/tab-editor :whiteboard/color}
+   :it #{:plugins}
+   :nl #{:plugins :type :left-side-bar/nav-recent-pages :plugin/update}
+   :pl #{:port}
+   :pt-BR #{:plugins :right-side-bar/flashcards :settings-page/enable-flashcards :page/backlinks
+            :host :settings-page/tab-editor :shortcut.category/plugins :whiteboard/link}
+   :pt-PT #{:plugins :settings-of-plugins :plugin/downloads :right-side-bar/flashcards
+            :settings-page/enable-flashcards :settings-page/plugin-system}
+   :nb-NO #{:port :type :whiteboard :right-side-bar/flashcards :right-side-bar/whiteboards 
+            :search-item/whiteboard :settings-page/enable-flashcards :settings-page/enable-whiteboards 
+            :settings-page/tab-editor :shortcut.category/whiteboard :whiteboard/medium 
+            :whiteboard/twitter-url :whiteboard/youtube-url}
+   :tr #{:help/awesome-logseq}
+   })
 
 
-(defn list-duplicates
-  "Lists translations that are the same as the one in English"
-  [& args]
+(defn- validate-languages-dont-have-duplicates
+  "Looks up duplicates for all languages"
+  []
   (let [dicts (get-dicts)
   (let [dicts (get-dicts)
         en-dicts (dicts :en)
         en-dicts (dicts :en)
-        lang (or (keyword (first args))
-                 (task-util/print-usage "LOCALE"))
-        lang-dicts (dicts lang)
         invalid-dicts
         invalid-dicts
-        (sort-by
-         :translation-key
-         (keep
-          #(when (= (en-dicts %) (lang-dicts %))
-             {:translation-key %
-              :duplicate-value (shorten (lang-dicts %) 70)})
-          (keys lang-dicts)))]
+        (->> (dissoc dicts :en)
+             (mapcat
+              (fn [[lang lang-dicts]]
+                (keep
+                 #(when (= (en-dicts %) (lang-dicts %))
+                    {:translation-key %
+                     :lang lang
+                     :duplicate-value (shorten (lang-dicts %) 70)})
+                 (keys (apply dissoc lang-dicts (allowed-duplicates lang))))))
+             (sort-by (juxt :lang :translation-key)))]
     (if (empty? invalid-dicts)
     (if (empty? invalid-dicts)
-      (println "No duplicated keys found!")
+      (println "All languages have no duplicate English values!")
       (do
       (do
-        (println "Keys with duplicate values found:")
+        (println "These translations keys are invalid because they are just copying the English value:")
         (task-util/print-table invalid-dicts)
         (task-util/print-table invalid-dicts)
         (System/exit 1)))))
         (System/exit 1)))))
+
+(defn validate-translations
+  "Runs multiple translation validations that fail fast if one of them is invalid"
+  []
+  (validate-non-default-languages)
+  (validate-ui-translations-are-used)
+  (validate-languages-dont-have-duplicates))

+ 16 - 1
src/electron/electron/handler.cljs

@@ -502,7 +502,8 @@
   js/__dirname)
   js/__dirname)
 
 
 (defmethod handle :getAppBaseInfo [^js win [_ _opts]]
 (defmethod handle :getAppBaseInfo [^js win [_ _opts]]
-  {:isFullScreen (.isFullScreen win)})
+  {:isFullScreen (.isFullScreen win)
+   :isMaximized (.isMaximized win)})
 
 
 (defmethod handle :getAssetsFiles [^js win [_ {:keys [exts]}]]
 (defmethod handle :getAssetsFiles [^js win [_ {:keys [exts]}]]
   (when-let [graph-path (state/get-window-graph-path win)]
   (when-let [graph-path (state/get-window-graph-path win)]
@@ -679,6 +680,20 @@
   (when-let [web-content (.-webContents win)]
   (when-let [web-content (.-webContents win)]
     (.reload web-content)))
     (.reload web-content)))
 
 
+(defmethod handle :window-minimize [^js win]
+  (.minimize win))
+
+(defmethod handle :window-toggle-maximized [^js win]
+  (if (.isMaximized win)
+    (.unmaximize win)
+    (.maximize win)))
+
+(defmethod handle :window-toggle-fullscreen [^js win]
+  (.setFullScreen win (not (.isFullScreen win))))
+
+(defmethod handle :window-close [^js win]
+  (.close win))
+
 ;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;
 ;; file-sync-rs-apis ;;
 ;; file-sync-rs-apis ;;
 ;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;

+ 37 - 14
src/electron/electron/plugin.cljs

@@ -19,6 +19,23 @@
               (.. win -webContents
               (.. win -webContents
                   (send (name type) (bean/->js payload))))))
                   (send (name type) (bean/->js payload))))))
 
 
+(defonce github-api-0 "https://api.github.com")
+(defonce github-api-1 "https://plugins.logseq.io/github/api")
+(defonce *github-api (atom github-api-0))
+(defonce *last-valid-github-api (atom nil))
+
+(defn valid-github-api!
+  []
+  (when (or (nil? @*last-valid-github-api)
+            (> (- (js/Date.now) @*last-valid-github-api) (* 1000 60)))
+    (let [target github-api-1]
+      (-> (fetch (str target "/rate_limit") {:timeout 2000})
+          (p/then #(when (not= (.-status %) 200) (throw (js/Error. (.-statusText %)))))
+          (p/then #(do (reset! *github-api target) (debug "INFO: use github api - " target)))
+          (p/catch #(do (reset! *github-api github-api-0) (debug "ERR: valid github api - " %)))
+          (p/finally #(reset! *last-valid-github-api (js/Date.now)))))))
+
+
 (defn dotdir-file?
 (defn dotdir-file?
   [file]
   [file]
   (and file (string/starts-with? (node-path/normalize file) cfgs/dot-root)))
   (and file (string/starts-with? (node-path/normalize file) cfgs/dot-root)))
@@ -34,13 +51,14 @@
 (defn- fetch-release-asset
 (defn- fetch-release-asset
   [{:keys [repo theme]} url-suffix {:keys [response-transform]
   [{:keys [repo theme]} url-suffix {:keys [response-transform]
                                     :or   {response-transform identity}}]
                                     :or   {response-transform identity}}]
-  (-> (p/let [repo         (some-> repo (string/trim) (string/replace #"^/+(.+?)/+$" "$1"))
-              api          #(str "https://api.github.com/repos/" repo "/" %)
+  (-> (p/let [_            (valid-github-api!)
+              repo         (some-> repo (string/trim) (string/replace #"^/+(.+?)/+$" "$1"))
+              api          #(str @*github-api "/repos/" repo "/" %)
               endpoint     (api url-suffix)
               endpoint     (api url-suffix)
-              ^js res      (fetch endpoint)
+              ^js res      (fetch endpoint {:timeout (* 1000 5)})
               illegal-text (when-not (= 200 (.-status res)) (.text res))
               illegal-text (when-not (= 200 (.-status res)) (.text res))
               _            (when-not (string/blank? illegal-text) (throw (js/Error. (str "Github API Failed(" (.-status res) ") " illegal-text))))
               _            (when-not (string/blank? illegal-text) (throw (js/Error. (str "Github API Failed(" (.-status res) ") " illegal-text))))
-              _            (debug "[Release URL] " endpoint "[Status/Text]" (.-status res))
+              _            (debug "Release latest:" endpoint ":status" (.-status res))
               res          (response-transform res)
               res          (response-transform res)
               res          (.json res)
               res          (.json res)
               res          (bean/->clj res)
               res          (bean/->clj res)
@@ -83,7 +101,7 @@
                             ;; cases. Previous logseq versions did not store the
                             ;; cases. Previous logseq versions did not store the
                             ;; plugin's git tag required to correctly install it
                             ;; plugin's git tag required to correctly install it
                             (let [repo' (some-> repo (string/trim) (string/replace #"^/+(.+?)/+$" "$1"))
                             (let [repo' (some-> repo (string/trim) (string/replace #"^/+(.+?)/+$" "$1"))
-                                  api   #(str "https://api.github.com/repos/" repo' "/" %)]
+                                  api   #(str @*github-api "/repos/" repo' "/" %)]
                               (fetch (api "releases/latest")))
                               (fetch (api "releases/latest")))
                             res))}))
                             res))}))
 
 
@@ -174,10 +192,10 @@
           updating?       (and version (. semver valid coerced-version)
           updating?       (and version (. semver valid coerced-version)
                                (not= action :install))]
                                (not= action :install))]
 
 
-      (debug (if updating? "Updating:" "Installing:") repo)
+      (debug "===" (if updating? "Updating:" "Installing:") repo "===")
 
 
       (-> (p/create
       (-> (p/create
-            (fn [resolve _reject]
+            (fn [resolve reject]
               ;;(reset! *installing-or-updating item)
               ;;(reset! *installing-or-updating item)
               ;; get releases
               ;; get releases
               (-> (p/let [[asset latest-version notes]
               (-> (p/let [[asset latest-version notes]
@@ -185,18 +203,19 @@
                             (fetch-specific-release-asset item)
                             (fetch-specific-release-asset item)
                             (fetch-latest-release-asset item))
                             (fetch-latest-release-asset item))
 
 
-                          _      (debug "[Release Asset] #" latest-version " =>" (:url asset))
+                          _      (debug "Release latest:" latest-version " from" (:url asset))
 
 
                           ;; compare latest version
                           ;; compare latest version
                           _      (when-let [coerced-latest-version
                           _      (when-let [coerced-latest-version
                                             (and updating? latest-version
                                             (and updating? latest-version
                                                  (. semver coerce latest-version))]
                                                  (. semver coerce latest-version))]
 
 
-                                   (debug "[Updating Latest?] " version " > " latest-version)
+                                   (debug "Release compare:" version "(current) > " latest-version "(latest)")
 
 
                                    (if (. semver lt coerced-version coerced-latest-version)
                                    (if (. semver lt coerced-version coerced-latest-version)
-                                     (debug "[Updating Latest] " latest-version)
-                                     (throw (js/Error. :no-new-version))))
+                                     (debug "Updating latest:" latest-version)
+                                     (do (debug "Update skip: no new version")
+                                         (throw (js/Error. :no-new-version)))))
 
 
                           dl-url (if-not (string? asset)
                           dl-url (if-not (string? asset)
                                    (:browser_download_url asset) asset)
                                    (:browser_download_url asset) asset)
@@ -207,7 +226,7 @@
 
 
                           dest   (.join node-path cfgs/dot-root "plugins" (:id item))
                           dest   (.join node-path cfgs/dot-root "plugins" (:id item))
                           _      (when-not only-check (download-asset-zip item dl-url latest-version dest))
                           _      (when-not only-check (download-asset-zip item dl-url latest-version dest))
-                          _      (debug (str "[" (if only-check "Checked" "Updated") "DONE]") latest-version)]
+                          _      (debug (str "[" (if only-check "Checked" "Updated") " DONE]") latest-version)]
 
 
                     (emit :lsp-updates
                     (emit :lsp-updates
                           {:status     :completed
                           {:status     :completed
@@ -224,8 +243,12 @@
                             {:status     :error
                             {:status     :error
                              :only-check only-check
                              :only-check only-check
                              :payload    (assoc item :error-code (.-message e))})
                              :payload    (assoc item :error-code (.-message e))})
-                      (debug e))
-                    (resolve nil)))))
+                      (reject e))))))
+
+          (p/catch
+            (fn [^js e]
+              (when-not (contains? #{":no-new-version"} (.-message e))
+                (debug e))))
 
 
           (p/finally
           (p/finally
             (fn []))))
             (fn []))))

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

@@ -26,10 +26,11 @@
    (create-main-window! url nil))
    (create-main-window! url nil))
   ([url opts]
   ([url opts]
    (let [win-state (windowStateKeeper (clj->js {:defaultWidth 980 :defaultHeight 700}))
    (let [win-state (windowStateKeeper (clj->js {:defaultWidth 980 :defaultHeight 700}))
+         native-titlebar? (cfgs/get-item :window/native-titlebar?)
          win-opts  (cond->
          win-opts  (cond->
                      {:width                (.-width win-state)
                      {:width                (.-width win-state)
                       :height               (.-height win-state)
                       :height               (.-height win-state)
-                      :frame                true
+                      :frame                (or mac? native-titlebar?)
                       :titleBarStyle        "hiddenInset"
                       :titleBarStyle        "hiddenInset"
                       :trafficLightPosition {:x 16 :y 16}
                       :trafficLightPosition {:x 16 :y 16}
                       :autoHideMenuBar      (not mac?)
                       :autoHideMenuBar      (not mac?)
@@ -187,7 +188,9 @@
 
 
       (doto win
       (doto win
         (.on "enter-full-screen" #(.send web-contents "full-screen" "enter"))
         (.on "enter-full-screen" #(.send web-contents "full-screen" "enter"))
-        (.on "leave-full-screen" #(.send web-contents "full-screen" "leave")))
+        (.on "leave-full-screen" #(.send web-contents "full-screen" "leave"))
+        (.on "maximize" #(.send web-contents "maximize" true))
+        (.on "unmaximize" #(.send web-contents "maximize" false)))
 
 
       ;; clear
       ;; clear
       (fn []
       (fn []

+ 3 - 10
src/main/electron/listener.cljs

@@ -5,7 +5,6 @@
             [datascript.core :as d]
             [datascript.core :as d]
             [dommy.core :as dom]
             [dommy.core :as dom]
             [electron.ipc :as ipc]
             [electron.ipc :as ipc]
-            [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
             [frontend.db :as db]
             [frontend.db.model :as db-model]
             [frontend.db.model :as db-model]
             [frontend.fs.sync :as sync]
             [frontend.fs.sync :as sync]
@@ -35,9 +34,7 @@
   []
   []
   ;; only persist current db!
   ;; only persist current db!
   ;; TODO rename the function and event to persist-db
   ;; TODO rename the function and event to persist-db
-  (repo-handler/persist-db! {:before     #(notification/show!
-                                           (ui/loading (t :graph/persist))
-                                           :warning)
+  (repo-handler/persist-db! {:before     #(ui/notify-graph-persist!)
                              :on-success #(ipc/ipc "persistent-dbs-saved")
                              :on-success #(ipc/ipc "persistent-dbs-saved")
                              :on-error   #(ipc/ipc "persistent-dbs-error")}))
                              :on-error   #(ipc/ipc "persistent-dbs-error")}))
 
 
@@ -139,15 +136,11 @@
                  ;; fire back "broadcastPersistGraphDone" on done
                  ;; fire back "broadcastPersistGraphDone" on done
                  (fn [data]
                  (fn [data]
                    (let [repo (bean/->clj data)
                    (let [repo (bean/->clj data)
-                         before-f #(notification/show!
-                                    (ui/loading (t :graph/persist))
-                                    :warning)
+                         before-f #(ui/notify-graph-persist!)
                          after-f #(ipc/ipc "broadcastPersistGraphDone")
                          after-f #(ipc/ipc "broadcastPersistGraphDone")
                          error-f (fn []
                          error-f (fn []
                                    (after-f)
                                    (after-f)
-                                   (notification/show!
-                                    (t :graph/persist-error)
-                                    :error))
+                                   (ui/notify-graph-persist-error!))
                          handlers {:before     before-f
                          handlers {:before     before-f
                                    :on-success after-f
                                    :on-success after-f
                                    :on-error   error-f}]
                                    :on-error   error-f}]

+ 14 - 13
src/main/frontend/commands.cljs

@@ -30,6 +30,7 @@
 (defonce angle-bracket "<")
 (defonce angle-bracket "<")
 (defonce hashtag "#")
 (defonce hashtag "#")
 (defonce colon ":")
 (defonce colon ":")
+(defonce command-trigger "/")
 (defonce *current-command (atom nil))
 (defonce *current-command (atom nil))
 
 
 (def query-doc
 (def query-doc
@@ -52,7 +53,7 @@
     "."]])
     "."]])
 
 
 (defn link-steps []
 (defn link-steps []
-  [[:editor/input (str (state/get-editor-command-trigger) "link")]
+  [[:editor/input (str command-trigger "link")]
    [:editor/show-input [{:command :link
    [:editor/show-input [{:command :link
                          :id :link
                          :id :link
                          :placeholder "Link"
                          :placeholder "Link"
@@ -62,7 +63,7 @@
                          :placeholder "Label"}]]])
                          :placeholder "Label"}]]])
 
 
 (defn image-link-steps []
 (defn image-link-steps []
-  [[:editor/input (str (state/get-editor-command-trigger) "link")]
+  [[:editor/input (str command-trigger "link")]
    [:editor/show-input [{:command :image-link
    [:editor/show-input [{:command :image-link
                          :id :link
                          :id :link
                          :placeholder "Link"
                          :placeholder "Link"
@@ -72,7 +73,7 @@
                          :placeholder "Label"}]]])
                          :placeholder "Label"}]]])
 
 
 (defn zotero-steps []
 (defn zotero-steps []
-  [[:editor/input (str (state/get-editor-command-trigger) "zotero")]
+  [[:editor/input (str command-trigger "zotero")]
    [:editor/show-zotero]])
    [:editor/show-zotero]])
 
 
 (def *extend-slash-commands (atom []))
 (def *extend-slash-commands (atom []))
@@ -96,19 +97,19 @@
   [type]
   [type]
   (let [template (util/format "@@%s: @@"
   (let [template (util/format "@@%s: @@"
                               type)]
                               type)]
-    [[:editor/input template {:last-pattern (state/get-editor-command-trigger)
+    [[:editor/input template {:last-pattern command-trigger
                               :backward-pos 2}]]))
                               :backward-pos 2}]]))
 
 
 (defn embed-page
 (defn embed-page
   []
   []
   (conj
   (conj
-   [[:editor/input "{{embed [[]]}}" {:last-pattern (state/get-editor-command-trigger)
+   [[:editor/input "{{embed [[]]}}" {:last-pattern command-trigger
                                      :backward-pos 4}]]
                                      :backward-pos 4}]]
    [:editor/search-page :embed]))
    [:editor/search-page :embed]))
 
 
 (defn embed-block
 (defn embed-block
   []
   []
-  [[:editor/input "{{embed (())}}" {:last-pattern (state/get-editor-command-trigger)
+  [[:editor/input "{{embed (())}}" {:last-pattern command-trigger
                                     :backward-pos 4}]
                                     :backward-pos 4}]
    [:editor/search-block :embed]])
    [:editor/search-block :embed]])
 
 
@@ -229,9 +230,9 @@
      ["Image link" (image-link-steps) "Create a HTTP link to a image"]
      ["Image link" (image-link-steps) "Create a HTTP link to a image"]
      (when (state/markdown?)
      (when (state/markdown?)
        ["Underline" [[:editor/input "<ins></ins>"
        ["Underline" [[:editor/input "<ins></ins>"
-                      {:last-pattern (state/get-editor-command-trigger)
+                      {:last-pattern command-trigger
                        :backward-pos 6}]] "Create a underline text decoration"])
                        :backward-pos 6}]] "Create a underline text decoration"])
-     ["Template" [[:editor/input (state/get-editor-command-trigger) nil]
+     ["Template" [[:editor/input command-trigger nil]
                   [:editor/search-template]] "Insert a created template here"]
                   [:editor/search-template]] "Insert a created template here"]
      (cond
      (cond
        (and (util/electron?) (config/local-file-based-graph? (state/get-current-repo)))
        (and (util/electron?) (config/local-file-based-graph? (state/get-current-repo)))
@@ -277,7 +278,7 @@
     [["Query" [[:editor/input "{{query }}" {:backward-pos 2}]
     [["Query" [[:editor/input "{{query }}" {:backward-pos 2}]
                [:editor/exit]] query-doc]
                [:editor/exit]] query-doc]
      ["Zotero" (zotero-steps) "Import Zotero journal article"]
      ["Zotero" (zotero-steps) "Import Zotero journal article"]
-     ["Query table function" [[:editor/input "{{function }}" {:backward-pos 2}]] "Create a query table function"]
+     ["Query function" [[:editor/input "{{function }}" {:backward-pos 2}]] "Create a query function"]
      ["Calculator" [[:editor/input "```calc\n\n```" {:type "block"
      ["Calculator" [[:editor/input "```calc\n\n```" {:type "block"
                                                      :backward-pos 4}]
                                                      :backward-pos 4}]
                     [:codemirror/focus]] "Insert a calculator"]
                     [:codemirror/focus]] "Insert a calculator"]
@@ -290,12 +291,12 @@
                  text)) "Draw a graph with Excalidraw"]
                  text)) "Draw a graph with Excalidraw"]
      ["Embed HTML " (->inline "html")]
      ["Embed HTML " (->inline "html")]
 
 
-     ["Embed Video URL" [[:editor/input "{{video }}" {:last-pattern (state/get-editor-command-trigger)
+     ["Embed Video URL" [[:editor/input "{{video }}" {:last-pattern command-trigger
                                                       :backward-pos 2}]]]
                                                       :backward-pos 2}]]]
 
 
      ["Embed Youtube timestamp" [[:youtube/insert-timestamp]]]
      ["Embed Youtube timestamp" [[:youtube/insert-timestamp]]]
 
 
-     ["Embed Twitter tweet" [[:editor/input "{{tweet }}" {:last-pattern (state/get-editor-command-trigger)
+     ["Embed Twitter tweet" [[:editor/input "{{tweet }}" {:last-pattern command-trigger
                                                           :backward-pos 2}]]]]
                                                           :backward-pos 2}]]]]
 
 
     @*extend-slash-commands
     @*extend-slash-commands
@@ -335,7 +336,7 @@
   (when-let [input (gdom/getElement id)]
   (when-let [input (gdom/getElement id)]
     (let [last-pattern (when-not (= last-pattern :skip-check)
     (let [last-pattern (when-not (= last-pattern :skip-check)
                          (when-not backward-truncate-number
                          (when-not backward-truncate-number
-                          (or last-pattern (state/get-editor-command-trigger))))
+                           (or last-pattern command-trigger)))
           edit-content (gobj/get input "value")
           edit-content (gobj/get input "value")
           current-pos (cursor/pos input)
           current-pos (cursor/pos input)
           current-pos (or
           current-pos (or
@@ -527,7 +528,7 @@
       (let [edit-content (gobj/get current-input "value")
       (let [edit-content (gobj/get current-input "value")
             current-pos (cursor/pos current-input)
             current-pos (cursor/pos current-input)
             prefix (subs edit-content 0 current-pos)
             prefix (subs edit-content 0 current-pos)
-            prefix (util/replace-last (state/get-editor-command-trigger) prefix "" (boolean space?))
+            prefix (util/replace-last command-trigger prefix "" (boolean space?))
             new-value (str prefix
             new-value (str prefix
                            (subs edit-content current-pos))]
                            (subs edit-content current-pos))]
         (state/set-block-content-and-last-pos! input-id
         (state/set-block-content-and-last-pos! input-id

+ 6 - 6
src/main/frontend/components/assets.cljs

@@ -88,11 +88,11 @@
        :disabled (string/blank? val)
        :disabled (string/blank? val)
        :on-click on-submit)]]))
        :on-click on-submit)]]))
 
 
-(rum/defc restart-button [active?]
-  (when active?
-    (ui/button (t :plugin/restart)
-               :on-click #(js/logseq.api.relaunch)
-               :small? true :intent "logseq")))
+(rum/defc restart-button
+  []
+  (ui/button (t :plugin/restart)
+             :on-click #(js/logseq.api.relaunch)
+             :small? true :intent "logseq"))
 
 
 (rum/defcs ^:large-vars/data-var alias-directories
 (rum/defcs ^:large-vars/data-var alias-directories
   < rum/reactive
   < rum/reactive
@@ -215,7 +215,7 @@
              #(state/set-assets-alias-enabled! (not alias-enabled?))
              #(state/set-assets-alias-enabled! (not alias-enabled?))
              true)]
              true)]
       [:span
       [:span
-       (restart-button alias-enabled-changed?)]]
+       (when alias-enabled-changed? (restart-button))]]
 
 
      (when alias-enabled?
      (when alias-enabled?
        [:div.pt-4
        [:div.pt-4

+ 80 - 102
src/main/frontend/components/block.cljs

@@ -6,13 +6,13 @@
             [cljs-bean.core :as bean]
             [cljs-bean.core :as bean]
             [cljs.core.match :refer [match]]
             [cljs.core.match :refer [match]]
             [cljs.reader :as reader]
             [cljs.reader :as reader]
-            [clojure.set :as set]
             [clojure.string :as string]
             [clojure.string :as string]
             [clojure.walk :as walk]
             [clojure.walk :as walk]
             [datascript.core :as d]
             [datascript.core :as d]
             [datascript.impl.entity :as de]
             [datascript.impl.entity :as de]
             [dommy.core :as dom]
             [dommy.core :as dom]
             [frontend.commands :as commands]
             [frontend.commands :as commands]
+            [frontend.components.block.macros :as block-macros]
             [frontend.components.datetime :as datetime-comp]
             [frontend.components.datetime :as datetime-comp]
             [frontend.components.lazy-editor :as lazy-editor]
             [frontend.components.lazy-editor :as lazy-editor]
             [frontend.components.macro :as macro]
             [frontend.components.macro :as macro]
@@ -39,13 +39,11 @@
             [frontend.fs :as fs]
             [frontend.fs :as fs]
             [frontend.handler.assets :as assets-handler]
             [frontend.handler.assets :as assets-handler]
             [frontend.handler.block :as block-handler]
             [frontend.handler.block :as block-handler]
-            [frontend.handler.common :as common-handler]
             [frontend.handler.dnd :as dnd]
             [frontend.handler.dnd :as dnd]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.file-sync :as file-sync]
             [frontend.handler.file-sync :as file-sync]
             [frontend.handler.notification :as notification]
             [frontend.handler.notification :as notification]
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.plugin :as plugin-handler]
-            [frontend.handler.query :as query-handler]
             [frontend.handler.repeated :as repeated]
             [frontend.handler.repeated :as repeated]
             [frontend.handler.route :as route-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.ui :as ui-handler]
@@ -63,6 +61,7 @@
             [frontend.util.clock :as clock]
             [frontend.util.clock :as clock]
             [frontend.util.drawer :as drawer]
             [frontend.util.drawer :as drawer]
             [frontend.util.property-edit :as property-edit]
             [frontend.util.property-edit :as property-edit]
+            [frontend.util.property :as property]
             [frontend.util.text :as text-util]
             [frontend.util.text :as text-util]
             [goog.dom :as gdom]
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [goog.object :as gobj]
@@ -70,7 +69,6 @@
             [logseq.graph-parser.block :as gp-block]
             [logseq.graph-parser.block :as gp-block]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.mldoc :as gp-mldoc]
-            [logseq.graph-parser.property :as gp-property]
             [logseq.graph-parser.text :as text]
             [logseq.graph-parser.text :as text]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util.block-ref :as block-ref]
             [logseq.graph-parser.util.block-ref :as block-ref]
@@ -554,7 +552,7 @@
                untitled? (str " opacity-50"))
                untitled? (str " opacity-50"))
       :data-ref page-name
       :data-ref page-name
       :draggable true
       :draggable true
-      :on-drag-start (fn [e] (editor-handler/block->data-transfer! page-name e))
+      :on-drag-start (fn [e] (editor-handler/block->data-transfer! page-name-in-block e))
       :on-mouse-down (fn [_e] (reset! *mouse-down? true))
       :on-mouse-down (fn [_e] (reset! *mouse-down? true))
       :on-mouse-up (fn [e]
       :on-mouse-up (fn [e]
                      (when @*mouse-down?
                      (when @*mouse-down?
@@ -1253,17 +1251,7 @@
 (defn- macro-function-cp
 (defn- macro-function-cp
   [config arguments]
   [config arguments]
   (or
   (or
-   (when (:query-result config)
-     (when-let [query-result (rum/react (:query-result config))]
-       (let [fn-string (-> (util/format "(fn [result] %s)" (first arguments))
-                           (common-handler/safe-read-string "failed to parse function")
-                           (query-handler/normalize-query-function query-result)
-                           (str))
-             f (sci/eval-string fn-string)]
-         (when (fn? f)
-           (try (f query-result)
-                (catch :default e
-                  (js/console.error e)))))))
+   (some-> (:query-result config) rum/react (block-macros/function-macro arguments))
    [:span.warning
    [:span.warning
     (util/format "{{function %s}}" (first arguments))]))
     (util/format "{{function %s}}" (first arguments))]))
 
 
@@ -1696,7 +1684,7 @@
        :block)
        :block)
       (util/stop e))
       (util/stop e))
 
 
-    (whiteboard-handler/inside-portal? (.-target e))
+    (and (util/meta-key? e) (whiteboard-handler/inside-portal? (.-target e)))
     (do (whiteboard-handler/add-new-block-portal-shape!
     (do (whiteboard-handler/add-new-block-portal-shape!
          uuid
          uuid
          (whiteboard-handler/closest-shape (.-target e)))
          (whiteboard-handler/closest-shape (.-target e)))
@@ -1967,11 +1955,12 @@
                  (not= "nil" marker))
                  (not= "nil" marker))
         {:class (str (string/lower-case marker))})
         {:class (str (string/lower-case marker))})
       (when bg-color
       (when bg-color
-        {:style {:background-color (if (some #{bg-color} ui/block-background-colors)
-                                     (str "var(--ls-highlight-color-" bg-color ")")
-                                     bg-color)
-                 :color (when-not (some #{bg-color} ui/block-background-colors) "white")}
-         :class "px-1 with-bg-color"}))
+        (let [built-in-color? (ui/built-in-color? bg-color)]
+          {:style {:background-color (if built-in-color?
+                                       (str "var(--ls-highlight-color-" bg-color ")")
+                                       bg-color)
+                   :color (when-not built-in-color? "white")}
+           :class "px-1 with-bg-color"})))
 
 
      ;; children
      ;; children
      (let [area?  (= :area (keyword (:hl-type properties)))
      (let [area?  (= :area (keyword (:hl-type properties)))
@@ -2067,66 +2056,25 @@
         :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")
-
-(def hidden-editable-block-properties
-  "Properties that are hidden in a block (block property)"
-  (into #{:logseq.query/nlp-date}
-        gp-property/editable-view-and-table-properties))
-
-(assert (set/subset? hidden-editable-block-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/keys [pre-block?] :as block}]
   [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-edit/hidden-properties))
-                           pre-block?
-                           (dissoc-keys hidden-editable-page-properties)
-                           (not pre-block?)
-                           (dissoc-keys hidden-editable-block-properties)
-                           pre-block?
-                           (add-aliases-to-properties block))]
+  (let [ordered-properties
+        (property/get-visible-ordered-properties (:block/properties block)
+                                                 (:block/properties-order block)
+                                                 {:pre-block? pre-block?
+                                                  :page-id (:db/id (:block/page block))})]
     (cond
     (cond
-      (seq properties)
-      (let [properties-order (cond->> (:block/properties-order block)
-                                      true
-                                      (remove (property-edit/hidden-properties))
-                                      pre-block?
-                                      (remove hidden-editable-page-properties))
-            properties-order (distinct properties-order)
-            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)
+      (seq ordered-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? ordered-properties)
       [:span.opacity-50 "Properties"]
       [:span.opacity-50 "Properties"]
 
 
       :else
       :else
@@ -2764,27 +2712,49 @@
        (= (:id config)
        (= (:id config)
           (str (:block/uuid block)))))
           (str (:block/uuid block)))))
 
 
+(defn- build-config [config block {:keys [navigating-block navigated?]}]
+  (cond-> config
+    navigated?
+    (assoc :id (str navigating-block))
+
+    true
+    (assoc :block block)
+
+    ;; Each block might have multiple queries, but we store only the first query's result.
+    ;; This :query-result atom is used by the query function feature to share results between
+    ;; the parent's query block and the children blocks. This works because config is shared
+    ;; between parent and children blocks
+    (nil? (:query-result config))
+    (assoc :query-result (atom nil))
+
+    (:ref? config)
+    (block-handler/attach-order-list-state block)))
+
+(defn- build-block [repo config block* {:keys [navigating-block navigated?]}]
+  (let [block (if (or (and (:custom-query? config)
+                           (empty? (:block/_parent block*))
+                           (not (and (:dsl-query? config)
+                                     (string/includes? (:query config) "not"))))
+                      navigated?)
+                (db/entity [:block/uuid navigating-block])
+                block*)]
+    (merge (db/sub-block (:db/id block))
+           (select-keys block [:block/level]))))
+
 (rum/defc ^:large-vars/cleanup-todo block-container-inner < rum/reactive db-mixins/query
 (rum/defc ^:large-vars/cleanup-todo block-container-inner < rum/reactive db-mixins/query
-  [state repo config block {:keys [edit? edit-input-id]}]
-  (let [ref? (:ref? config)
-        custom-query? (boolean (:custom-query? config))
+  [state repo config* block* {:keys [edit? edit-input-id]}]
+  (let [ref? (:ref? config*)
+        custom-query? (boolean (:custom-query? config*))
         ref-or-custom-query? (or ref? custom-query?)
         ref-or-custom-query? (or ref? custom-query?)
         *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)
-        block (merge (db/sub-block (:db/id block))
-                     (select-keys block [:block/level]))
-        {:block/keys [uuid pre-block? refs level format content properties]} block
+        navigated? (and (not= (:block/uuid block*) navigating-block) navigating-block)
+        block (build-block repo config* block* {:navigating-block navigating-block :navigated? navigated?})
+        {:block/keys [uuid pre-block? refs level content properties]} block
         children (db/sort-by-left (:block/_parent block) block)
         children (db/sort-by-left (:block/_parent block) block)
         {:block.temp/keys [top?]} block
         {:block.temp/keys [top?]} block
-        config (if navigated? (assoc config :id (str navigating-block)) config)
+        config (build-config config* block {:navigated? navigated? :navigating-block navigating-block})
         blocks-container-id (:blocks-container-id config)
         blocks-container-id (:blocks-container-id config)
-        config (assoc config :block block)
-        ;; Each block might have multiple queries, but we store only the first query's result
-        config (if (nil? (:query-result config))
-                 (assoc config :query-result (atom nil))
-                 config)
-        config (if ref? (block-handler/attach-order-list-state config block) config)
         heading? (:heading properties)
         heading? (:heading properties)
         *control-show? (get state ::control-show?)
         *control-show? (get state ::control-show?)
         db-collapsed? (util/collapsed? block)
         db-collapsed? (util/collapsed? block)
@@ -2810,6 +2780,8 @@
         data-refs-self (build-refs-data-value refs)
         data-refs-self (build-refs-data-value refs)
         card? (string/includes? data-refs-self "\"card\"")
         card? (string/includes? data-refs-self "\"card\"")
         review-cards? (:review-cards? config)
         review-cards? (:review-cards? config)
+        own-number-list? (:own-order-number-list? config)
+        order-list? (boolean own-number-list?)
         selected? (when-not slide?
         selected? (when-not slide?
                     (state/sub-block-selected? blocks-container-id uuid))]
                     (state/sub-block-selected? blocks-container-id uuid))]
     [:div.ls-block
     [:div.ls-block
@@ -2822,6 +2794,7 @@
                    (when pre-block? " pre-block")
                    (when pre-block? " pre-block")
                    (when (and card? (not review-cards?)) " shadow-md")
                    (when (and card? (not review-cards?)) " shadow-md")
                    (when selected? " selected noselect")
                    (when selected? " selected noselect")
+                   (when order-list? " is-order-list")
                    (when (string/blank? content) " is-blank"))
                    (when (string/blank? content) " is-blank"))
        :blockid (str uuid)
        :blockid (str uuid)
        :haschild (str (boolean has-child?))}
        :haschild (str (boolean has-child?))}
@@ -2850,7 +2823,7 @@
      (when top?
      (when top?
        (dnd-separator-wrapper block block-id slide? true false))
        (dnd-separator-wrapper block block-id slide? true false))
 
 
-     [:div.flex.flex-row.pr-2
+     [:div.block-main-container.flex.flex-row.pr-2
       {:class (if (and heading? (seq (:block/title block))) "items-baseline" "")
       {:class (if (and heading? (seq (:block/title block))) "items-baseline" "")
        :on-touch-start (fn [event uuid] (block-handler/on-touch-start event uuid))
        :on-touch-start (fn [event uuid] (block-handler/on-touch-start event uuid))
        :on-touch-move (fn [event]
        :on-touch-move (fn [event]
@@ -2872,7 +2845,7 @@
       (if whiteboard-block?
       (if whiteboard-block?
         (block-reference {} (str uuid) nil)
         (block-reference {} (str uuid) nil)
         ;; Not embed self
         ;; Not embed self
-        (let [block (merge block (block/parse-title-and-body uuid format pre-block? content))
+        (let [block (merge block (block/parse-title-and-body uuid (:block/format block) pre-block? content))
               hide-block-refs-count? (and (:embed? config)
               hide-block-refs-count? (and (:embed? config)
                                           (= (:block/uuid block) (:embed-id config)))]
                                           (= (:block/uuid block) (:embed-id config)))]
           (block-content-or-editor config block edit-input-id block-id edit? hide-block-refs-count?)))
           (block-content-or-editor config block edit-input-id block-id edit? hide-block-refs-count?)))
@@ -3140,14 +3113,15 @@
 
 
         :else
         :else
         (let [language (if (contains? #{"edn" "clj" "cljc" "cljs"} language) "clojure" language)]
         (let [language (if (contains? #{"edn" "clj" "cljc" "cljs"} language) "clojure" language)]
-          [:div {:ref (fn [el]
-                        (set-inside-portal? (and el (whiteboard-handler/inside-portal? el))))}
+          [:div.ui-fenced-code-editor
+           {:ref (fn [el]
+                   (set-inside-portal? (and el (whiteboard-handler/inside-portal? el))))}
            (cond
            (cond
              (nil? inside-portal?) nil
              (nil? inside-portal?) nil
 
 
              (or (:slide? config) inside-portal?)
              (or (:slide? config) inside-portal?)
              (highlight/highlight (str (random-uuid))
              (highlight/highlight (str (random-uuid))
-                                  {:class (str "language-" language)
+                                  {:class     (str "language-" language)
                                    :data-lang language}
                                    :data-lang language}
                                   code)
                                   code)
 
 
@@ -3316,10 +3290,14 @@
             [:sup.fn (str name "↩︎")]])]])
             [:sup.fn (str name "↩︎")]])]])
 
 
       ["Src" options]
       ["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?))]
+      (let [lang (util/safe-lower-case (:language options))]
+        [:div.cp__fenced-code-block
+         {:data-lang lang}
+         (if-let [opts (plugin-handler/hook-fenced-code-by-type lang)]
+           [:div.ui-fenced-code-wrap
+            (src-cp config options html-export?)
+            (plugins/hook-ui-fenced-code (:block config) (string/join "" (:lines options)) opts)]
+           (src-cp config options html-export?))])
 
 
       :else
       :else
       "")
       "")

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

@@ -1,7 +1,7 @@
 .block-content-wrapper {
 .block-content-wrapper {
   /* 38px is the width of block-control */
   /* 38px is the width of block-control */
   width: calc(100% - 22px);
   width: calc(100% - 22px);
-
+  user-select: text;
   @screen sm {
   @screen sm {
     width: calc(100% - 33px);
     width: calc(100% - 33px);
     overflow-x: visible;
     overflow-x: visible;

+ 71 - 0
src/main/frontend/components/block/macros.cljs

@@ -0,0 +1,71 @@
+(ns frontend.components.block.macros
+  "Logseq macros that render and evaluate in blocks"
+  (:require [clojure.walk :as walk]
+            [frontend.extensions.sci :as sci]
+            [frontend.handler.common :as common-handler]
+            [goog.string :as gstring]
+            [goog.string.format]))
+
+(defn- normalize-query-function
+  [ast result]
+  (let [ast (walk/prewalk
+             (fn [f]
+               (if (and (list? f)
+                        (keyword? (second f))
+                        (contains? #{'sum 'average 'count 'min 'max} (first f)))
+                 (if (contains? #{'min 'max} (first f))
+                   (list
+                    'apply
+                    (first f)
+                    (list 'map (second f) 'result))
+                   (list
+                    (first f)
+                    (list 'map (second f) 'result)))
+                 f))
+             ast)]
+    (walk/postwalk
+     (fn [f]
+       (cond
+         (keyword? f)
+         ;; These keyword aliases should be the same as those used in the query-table for sorting
+         (case f
+           :block
+           :block/content
+
+           :page
+           :block/name
+
+           :created-at
+           :block/created-at
+
+           :updated-at
+           :block/updated-at
+
+           (let [vals (map #(get-in % [:block/properties f]) result)
+                 int? (some integer? vals)]
+             `(~'fn [~'b]
+                    (~'let [~'result-str (~'get-in ~'b [:block/properties ~f])
+                            ~'result-num (~'parseFloat ~'result-str)
+                            ~'result (if (~'isNaN ~'result-num) ~'result-str ~'result-num)]
+                           (~'or ~'result (~'when ~int? 0))))))
+
+         :else
+         f))
+     ast)))
+
+(defn function-macro
+  "Provides functionality for {{function}}"
+  [query-result* arguments]
+  (let [query-result (if (map? query-result*)
+                       ;; Ungroup results grouped by page in page view
+                       (mapcat val query-result*)
+                       query-result*)
+        fn-string (-> (gstring/format "(fn [result] %s)" (first arguments))
+                      (common-handler/safe-read-string "failed to parse function")
+                      (normalize-query-function query-result)
+                      (str))
+        f (sci/eval-string fn-string)]
+    (when (fn? f)
+      (try (f query-result)
+           (catch :default e
+             (js/console.error e))))))

+ 24 - 23
src/main/frontend/components/bug_report.cljs

@@ -5,7 +5,8 @@
             [frontend.util :as util]
             [frontend.util :as util]
             [reitit.frontend.easy :as rfe]
             [reitit.frontend.easy :as rfe]
             [clojure.string :as string]
             [clojure.string :as string]
-            [frontend.handler.notification :as notification]))
+            [frontend.handler.notification :as notification]
+            [frontend.context.i18n :refer [t]]))
 
 
 (defn parse-clipboard-data-transfer
 (defn parse-clipboard-data-transfer
   "parse dataTransfer
   "parse dataTransfer
@@ -42,7 +43,7 @@
 
 
         copy-result-to-clipboard! (fn [result]
         copy-result-to-clipboard! (fn [result]
                                     (util/copy-to-clipboard! result)
                                     (util/copy-to-clipboard! result)
-                                    (notification/show! "Copied to clipboard!"))
+                                    (notification/show! (t :bug-report/inspector-page-copy-notif)))
 
 
         reset-step! (fn []
         reset-step! (fn []
                       (set-step! 0)
                       (set-step! 0)
@@ -56,26 +57,26 @@
 
 
     [:div.flex.flex-col
     [:div.flex.flex-col
      (when (= step 0)
      (when (= step 0)
-       (list [:div.mx-auto "Press Ctrl+V / ⌘+V to inspect your clipboard data"]
-             [:div.mx-auto "or click here to paste if you are using the mobile version"]
+       (list [:div.mx-auto (t :bug-report/inspector-page-desc-1)]
+             [:div.mx-auto (t :bug-report/inspector-page-desc-2)]
              ;; for mobile
              ;; for mobile
-             [:input.form-input.is-large.transition.duration-150.ease-in-out {:type "text" :placeholder "Long press here to paste if you are on mobile"}]
+             [:input.form-input.is-large.transition.duration-150.ease-in-out {:type "text" :placeholder (t :bug-report/inspector-page-placeholder)}]
              [:div.flex.justify-between.items-center.mt-2
              [:div.flex.justify-between.items-center.mt-2
-              [:div "Something wrong? No problem, click to go back to the previous step."]
-              (ui/button "Go back" :on-click #(util/open-url (rfe/href :bug-report)))]))
+              [:div (t :bug-report/inspector-page-tip)]
+              (ui/button (t :bug-report/inspector-page-btn-back) :on-click #(util/open-url (rfe/href :bug-report)))]))
 
 
      (when (= step 1)
      (when (= step 1)
        (list
        (list
-        [:div "Here is the data read from clipboard."]
+        [:div (t :bug-report/inspector-page-desc-clipboard)]
         [:div.flex.justify-between.items-center.mt-2
         [:div.flex.justify-between.items-center.mt-2
-         [:div "If this is okay to share, click the copy button."]
-         (ui/button "Copy the result" :on-click #(copy-result-to-clipboard! (js/JSON.stringify (clj->js result) nil 2)))]
+         [:div (t :bug-report/inspector-page-desc-copy)]
+         (ui/button (t :bug-report/inspector-page-btn-copy) :on-click #(copy-result-to-clipboard! (js/JSON.stringify (clj->js result) nil 2)))]
         [:div.flex.justify-between.items-center.mt-2
         [:div.flex.justify-between.items-center.mt-2
-         [:div "Now you can report the result pasted to your clipboard. Please paste the result in the 'Additional Context' section and state where you copied the original content from. Thanks!"]
-         (ui/button "Create an issue" :href header/bug-report-url)]
+         [:div (t :bug-report/inspector-page-desc-create-issue)]
+         (ui/button (t :bug-report/inspector-page-btn-create-issue) :href header/bug-report-url)]
         [:div.flex.justify-between.items-center.mt-2
         [:div.flex.justify-between.items-center.mt-2
-         [:div "Something wrong? No problem, click to go back to the previous step."]
-         (ui/button "Go back" :on-click reset-step!)]
+         [:div (t :bug-report/inspector-page-tip)]
+         (ui/button (t :bug-report/inspector-page-btn-back) :on-click reset-step!)]
 
 
         [:pre.whitespace-pre-wrap [:code (js/JSON.stringify (clj->js result) nil 2)]]))]))
         [:pre.whitespace-pre-wrap [:code (js/JSON.stringify (clj->js result) nil 2)]]))]))
 
 
@@ -102,17 +103,17 @@
    [:div.flex.flex-col.items-center
    [:div.flex.flex-col.items-center
     [:div.flex.items-center.mb-2
     [:div.flex.items-center.mb-2
      (ui/icon "bug")
      (ui/icon "bug")
-     [:h1.text-3xl.ml-2 "Bug report"]]
-    [:div.opacity-60 "Can you help us out by submitting a bug report? We'll get it sorted out as soon as we can."]]
+     [:h1.text-3xl.ml-2 (t :bug-report/main-title)]]
+    [:div.opacity-60 (t :bug-report/main-desc)]]
    [:div.cp__bug-report-reporter.rounded-lg.p-8.mt-8
    [:div.cp__bug-report-reporter.rounded-lg.p-8.mt-8
-    [:h1.text-2xl "Is the bug you encountered related to these features?"]
-    [:div.opacity-60 "You can use these handy tools to give us additional information."]
-    (report-item-button "Clipboard helper"
-                 "Inspect and collect clipboard data"
+    [:h1.text-2xl (t :bug-report/section-clipboard-title)]
+    [:div.opacity-60 (t :bug-report/section-clipboard-desc)]
+    (report-item-button (t :bug-report/section-clipboard-btn-title)
+                 (t :bug-report/section-clipboard-btn-desc)
                  "clipboard"
                  "clipboard"
                  {:on-click #(util/open-url (rfe/href :bug-report-tools {:tool "clipboard-data-inspector"}))})
                  {:on-click #(util/open-url (rfe/href :bug-report-tools {:tool "clipboard-data-inspector"}))})
     [:div.py-2] ;; divider
     [:div.py-2] ;; divider
     [:div.flex.flex-col
     [:div.flex.flex-col
-     [:h1.text-2xl "Or..."]
-     [:div.opacity-60 "If there are no tools available for you to gather additional information, please report the bug directly."]
-     (report-item-button "Submit a bug report" "Help Make Logseq Better!" "message-report" {:on-click #(util/open-url header/bug-report-url)})]]])
+     [:h1.text-2xl (t :bug-report/section-issues-title)]
+     [:div.opacity-60 (t :bug-report/section-issues-desc)]
+     (report-item-button (t :bug-report/section-issues-btn-title) (t :bug-report/section-issues-btn-desc) "message-report" {:on-click #(util/open-url header/bug-report-url)})]]])

+ 21 - 10
src/main/frontend/components/container.cljs

@@ -35,12 +35,13 @@
             [frontend.ui :as ui]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util :as util]
             [frontend.util.cursor :as cursor]
             [frontend.util.cursor :as cursor]
+            [frontend.components.window-controls :as window-controls]
             [goog.dom :as gdom]
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [goog.object :as gobj]
+            [logseq.common.path :as path]
             [react-draggable]
             [react-draggable]
             [reitit.frontend.easy :as rfe]
             [reitit.frontend.easy :as rfe]
-            [rum.core :as rum]
-            [logseq.common.path :as path]))
+            [rum.core :as rum]))
 
 
 (rum/defc nav-content-item < rum/reactive
 (rum/defc nav-content-item < rum/reactive
   [name {:keys [class]} child]
   [name {:keys [class]} child]
@@ -284,8 +285,8 @@
                               (when (< touching-x-offset 0)
                               (when (< touching-x-offset 0)
                                 (max touching-x-offset (- 0 (:width el-rect))))))
                                 (max touching-x-offset (- 0 (:width el-rect))))))
         offset-ratio (and (number? touching-x-offset)
         offset-ratio (and (number? touching-x-offset)
-                            (some->> (:width el-rect)
-                                     (/ touching-x-offset)))]
+                          (some->> (:width el-rect)
+                                   (/ touching-x-offset)))]
 
 
     (rum/use-effect!
     (rum/use-effect!
      #(js/setTimeout
      #(js/setTimeout
@@ -481,7 +482,7 @@
 
 
 (rum/defc main <
 (rum/defc main <
   {:did-mount (fn [state]
   {:did-mount (fn [state]
-                (when-let [element (gdom/getElement "main-content-container")]
+                (when-let [element (gdom/getElement "app-container")]
                   (dnd/subscribe!
                   (dnd/subscribe!
                    element
                    element
                    :upload-files
                    :upload-files
@@ -492,19 +493,23 @@
                   (common-handler/listen-to-scroll! element)
                   (common-handler/listen-to-scroll! element)
                   (when (:margin-less-pages? (first (:rum/args state))) ;; makes sure full screen pages displaying without scrollbar
                   (when (:margin-less-pages? (first (:rum/args state))) ;; makes sure full screen pages displaying without scrollbar
                     (set! (.. element -scrollTop) 0)))
                     (set! (.. element -scrollTop) 0)))
-                state)}
+                state)
+   :will-unmount (fn [state]
+                   (when-let [el (gdom/getElement "app-container")]
+                     (dnd/unsubscribe! el :upload-files))
+                   state)}
   [{:keys [route-match margin-less-pages? route-name indexeddb-support? db-restoring? main-content show-action-bar? show-recording-bar?]}]
   [{:keys [route-match margin-less-pages? route-name indexeddb-support? db-restoring? main-content show-action-bar? show-recording-bar?]}]
-  (let [left-sidebar-open? (state/sub :ui/left-sidebar-open?)
+  (let [left-sidebar-open?   (state/sub :ui/left-sidebar-open?)
         onboarding-and-home? (and (or (nil? (state/get-current-repo)) (config/demo-graph?))
         onboarding-and-home? (and (or (nil? (state/get-current-repo)) (config/demo-graph?))
                                   (not config/publishing?)
                                   (not config/publishing?)
                                   (= :home route-name))
                                   (= :home route-name))
-        margin-less-pages? (or (and (mobile-util/native-platform?) onboarding-and-home?) margin-less-pages?)]
+        margin-less-pages?   (or (and (mobile-util/native-platform?) onboarding-and-home?) margin-less-pages?)]
     [:div#main-container.cp__sidebar-main-layout.flex-1.flex
     [:div#main-container.cp__sidebar-main-layout.flex-1.flex
      {:class (util/classnames [{:is-left-sidebar-open left-sidebar-open?}])}
      {:class (util/classnames [{:is-left-sidebar-open left-sidebar-open?}])}
 
 
      ;; desktop left sidebar layout
      ;; desktop left sidebar layout
      (left-sidebar {:left-sidebar-open? left-sidebar-open?
      (left-sidebar {:left-sidebar-open? left-sidebar-open?
-                    :route-match route-match})
+                    :route-match        route-match})
 
 
      [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row.outline-none.relative
      [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row.outline-none.relative
 
 
@@ -536,7 +541,7 @@
          db-restoring?
          db-restoring?
          [:div.mt-20
          [:div.mt-20
           [:div.ls-center
           [:div.ls-center
-           (ui/loading (t :loading))]]
+           (ui/loading)]]
 
 
          :else
          :else
          [:div
          [:div
@@ -730,6 +735,8 @@
         indexeddb-support? (state/sub :indexeddb/support?)
         indexeddb-support? (state/sub :indexeddb/support?)
         page? (= :page route-name)
         page? (= :page route-name)
         home? (= :home route-name)
         home? (= :home route-name)
+        native-titlebar? (state/sub [:electron/user-cfgs :window/native-titlebar?])
+        window-controls? (and (util/electron?) (not util/mac?) (not native-titlebar?))
         edit? (:editor/editing? @state/state)
         edit? (:editor/editing? @state/state)
         default-home (get-default-home-if-valid)
         default-home (get-default-home-if-valid)
         logged? (user-handler/logged-in?)
         logged? (user-handler/logged-in?)
@@ -759,6 +766,7 @@
       {: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?
+                                 :ls-window-controls      window-controls?
                                  :ls-fold-button-on-right fold-button-on-right?
                                  :ls-fold-button-on-right fold-button-on-right?
                                  :ls-hl-colored           ls-block-hl-colored?}])}
                                  :ls-hl-colored           ls-block-hl-colored?}])}
 
 
@@ -795,6 +803,9 @@
                :show-action-bar?    show-action-bar?
                :show-action-bar?    show-action-bar?
                :show-recording-bar? show-recording-bar?})]
                :show-recording-bar? show-recording-bar?})]
 
 
+       (when window-controls?
+         (window-controls/container))
+
        (right-sidebar/sidebar)
        (right-sidebar/sidebar)
 
 
        [:div#app-single-container]]
        [:div#app-single-container]]

+ 29 - 3
src/main/frontend/components/container.css

@@ -93,6 +93,7 @@
     height: calc(100vh - var(--ls-headbar-inner-top-padding) - 50px);
     height: calc(100vh - var(--ls-headbar-inner-top-padding) - 50px);
     margin-top: 30px;
     margin-top: 30px;
     width: 100%;
     width: 100%;
+    padding-top: var(--ls-win32-title-bar-height);
 
 
     > .fake-bar {
     > .fake-bar {
       @apply w-full px-5 pt-1 sm:hidden;
       @apply w-full px-5 pt-1 sm:hidden;
@@ -450,6 +451,28 @@
   }
   }
 }
 }
 
 
+.ls-window-controls {
+  &.ls-right-sidebar-open {
+    .cp__right-sidebar-topbar {
+      margin-right: 144px;
+
+      .is-fullscreen & {
+        margin-right: 48px;
+      }
+    }
+  }
+
+  &:not(.ls-right-sidebar-open) {
+    .cp__header > .r {
+      margin-right: 144px;
+
+      .is-fullscreen & {
+        margin-right: 48px;
+      }
+    }
+  }
+}
+
 .ls-wide-mode {
 .ls-wide-mode {
   .cp__sidebar-main-content {
   .cp__sidebar-main-content {
     max-width: var(--ls-main-content-max-width-wide);
     max-width: var(--ls-main-content-max-width-wide);
@@ -465,7 +488,8 @@ html[data-theme='dark'] {
 }
 }
 
 
 .settings-modal {
 .settings-modal {
-  margin: -15px;
+  @apply -m-8 rounded-lg;
+  /* box-shadow: inset 0 0 0 1px var(--ls-border-color); */
 }
 }
 
 
 .cp__sidebar-main-layout {
 .cp__sidebar-main-layout {
@@ -513,6 +537,7 @@ html[data-theme='dark'] {
 
 
   .resizer {
   .resizer {
     @apply absolute top-0 bottom-0;
     @apply absolute top-0 bottom-0;
+    touch-action: none;
     left: 0;
     left: 0;
     width: 4px;
     width: 4px;
     user-select: none;
     user-select: none;
@@ -537,7 +562,6 @@ html[data-theme='dark'] {
   }
   }
 
 
   &.open {
   &.open {
-    width: var(--ls-right-sidebar-width);
     max-width: 60vw;
     max-width: 60vw;
   }
   }
 
 
@@ -573,7 +597,9 @@ html[data-theme='dark'] {
     user-select: none;
     user-select: none;
     -webkit-app-region: drag;
     -webkit-app-region: drag;
 
 
-    a, svg {
+    a,
+    svg,
+    button {
       -webkit-app-region: no-drag;
       -webkit-app-region: no-drag;
     }
     }
   }
   }

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

@@ -149,7 +149,7 @@
              [:p (t :file-rn/need-action)])
              [:p (t :file-rn/need-action)])
            [:p
            [:p
             (ui/button
             (ui/button
-             (str (t :file-rn/all-action) " (" (count rename-items) ")")
+             (t :file-rn/all-action (count rename-items))
              :on-click <rename-all
              :on-click <rename-all
              :class "text-md p-2 mr-1")
              :class "text-md p-2 mr-1")
             (t :file-rn/or-select-actions)
             (t :file-rn/or-select-actions)

+ 6 - 4
src/main/frontend/components/editor.cljs

@@ -131,7 +131,7 @@
                                 nil
                                 nil
 
 
                                 (empty? matched-pages)
                                 (empty? matched-pages)
-                                (cons (str (t :new-page) ": " q) matched-pages)
+                                (cons q matched-pages)
 
 
                                ;; reorder, shortest and starts-with first.
                                ;; reorder, shortest and starts-with first.
                                 :else
                                 :else
@@ -142,8 +142,8 @@
                                                      matched-pages)]
                                                      matched-pages)]
                                   (if (gstring/caseInsensitiveStartsWith (first matched-pages) q)
                                   (if (gstring/caseInsensitiveStartsWith (first matched-pages) q)
                                     (cons (first matched-pages)
                                     (cons (first matched-pages)
-                                          (cons  (str (t :new-page) ": " q) (rest matched-pages)))
-                                    (cons (str (t :new-page) ": " q) matched-pages))))]
+                                          (cons q (rest matched-pages)))
+                                    (cons q matched-pages))))]
             (ui/auto-complete
             (ui/auto-complete
              matched-pages
              matched-pages
              {:on-chosen   (page-handler/on-chosen-handler input id q pos format)
              {:on-chosen   (page-handler/on-chosen-handler input id q pos format)
@@ -154,7 +154,9 @@
                                {:children
                                {:children
                                 [:div.flex
                                 [:div.flex
                                  (when (db-model/whiteboard-page? page-name) [:span.mr-1 (ui/icon "whiteboard" {:extension? true})])
                                  (when (db-model/whiteboard-page? page-name) [:span.mr-1 (ui/icon "whiteboard" {:extension? true})])
-                                 (search/highlight-exact-query page-name q)]
+                                 [:div.flex.space-x-1
+                                  [:div (when-not (db/page-exists? page-name) (t :new-page))]
+                                  (search/highlight-exact-query page-name q)]]
                                 :open?           chosen?
                                 :open?           chosen?
                                 :manual?         true
                                 :manual?         true
                                 :fixed-position? true
                                 :fixed-position? true

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

@@ -152,7 +152,7 @@
        ;; wait for content load
        ;; wait for content load
        (and format
        (and format
             (contains? (gp-config/text-formats) format))
             (contains? (gp-config/text-formats) format))
-       (ui/loading "Loading ...")
+       (ui/loading)
 
 
        :else
        :else
        [:div (t :file/format-not-supported (name format))])]))
        [:div (t :file/format-not-supported (name format))])]))

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

@@ -598,7 +598,7 @@
 
 
     [:div.version-list
     [:div.version-list
      (if loading?
      (if loading?
-       [:div.p-4 (ui/loading "Loading...")]
+       [:div.p-4 (ui/loading)]
        (for [version version-files]
        (for [version version-files]
          (let [version-uuid (get-version-key version)
          (let [version-uuid (get-version-key version)
                local?       (some? (:relative-path version))]
                local?       (some? (:relative-path version))]
@@ -704,7 +704,7 @@
 
 
      ;; ready loading
      ;; ready loading
      [:div.flex.items-center.h-full.justify-center.w-full.absolute.ready-loading
      [:div.flex.items-center.h-full.justify-center.w-full.absolute.ready-loading
-      (ui/loading "Loading...")]]))
+      (ui/loading)]]))
 
 
 (defn pick-page-histories-panel [graph-uuid page-name]
 (defn pick-page-histories-panel [graph-uuid page-name]
   (fn []
   (fn []
@@ -789,7 +789,7 @@
    [:div.cloud-tip.rounded-md.mt-6.py-4
    [:div.cloud-tip.rounded-md.mt-6.py-4
     [:div.items-center.opacity-90.flex.justify-center
     [:div.items-center.opacity-90.flex.justify-center
      [:span.pr-2.flex (ui/icon "bell-ringing" {:class "font-semibold"})]
      [:span.pr-2.flex (ui/icon "bell-ringing" {:class "font-semibold"})]
-     [:strong "Logseq Sync is still in Beta and we're working on a Pro plan!"]]
+     [:strong "Logseq Sync is still in Beta and we're working on a Pro plan!"]]]
 
 
     ;; [:ul.flex.py-6.px-4
     ;; [:ul.flex.py-6.px-4
     ;;  [:li.it
     ;;  [:li.it
@@ -802,7 +802,7 @@
     ;;  [:li.it
     ;;  [:li.it
     ;;   [:h1.dark:text-white "50G"]
     ;;   [:h1.dark:text-white "50G"]
     ;;   [:h2 "Total Storage"]]]
     ;;   [:h2 "Total Storage"]]]
-    ]
+    
 
 
    [:div.pt-6.flex.justify-end.space-x-2
    [:div.pt-6.flex.justify-end.space-x-2
     (ui/button "Done" :on-click close-fn)]])
     (ui/button "Done" :on-click close-fn)]])

+ 14 - 10
src/main/frontend/components/header.cljs

@@ -29,7 +29,7 @@
   []
   []
   (ui/with-shortcut :go/home "left"
   (ui/with-shortcut :go/home "left"
     [:button.button.icon.inline
     [:button.button.icon.inline
-     {:title "Home"
+     {:title (t :home)
       :on-click #(do
       :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))
@@ -58,7 +58,7 @@
   [{: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
-     {:title "Toggle left menu"
+     {:title (t :header/toggle-left-sidebar)
       :on-click on-click}
       :on-click on-click}
      (ui/icon "menu-2" {:size ui/icon-size})]))
      (ui/icon "menu-2" {:size ui/icon-size})]))
 
 
@@ -86,7 +86,7 @@
      (fn [{:keys [toggle-fn]}]
      (fn [{:keys [toggle-fn]}]
        [:button.button.icon.toolbar-dots-btn
        [:button.button.icon.toolbar-dots-btn
         {:on-click toggle-fn
         {:on-click toggle-fn
-         :title "More"}
+         :title (t :header/more)}
         (ui/icon "dots" {:size ui/icon-size})])
         (ui/icon "dots" {:size ui/icon-size})])
      (->>
      (->>
       [(when (state/enable-editing?)
       [(when (state/enable-editing?)
@@ -128,8 +128,13 @@
           :options {:href (rfe/href :bug-report)}
           :options {:href (rfe/href :bug-report)}
           :icon (ui/icon "bug")})
           :icon (ui/icon "bug")})
 
 
+       (when config/publishing?
+         {:title (t :toggle-theme)
+          :options {:on-click #(state/toggle-theme!)}
+          :icon (ui/icon "bulb")})
+
        (when (and (state/sub :auth/id-token) (user-handler/logged-in?))
        (when (and (state/sub :auth/id-token) (user-handler/logged-in?))
-         {:title (str (t :logout) " (" (user-handler/email) ")")
+         {:title (t :logout-user (user-handler/email))
           :options {:on-click #(user-handler/logout)}
           :options {:on-click #(user-handler/logout)}
           :icon  (ui/icon "logout")})]
           :icon  (ui/icon "logout")})]
       (concat page-menu-and-hr)
       (concat page-menu-and-hr)
@@ -143,12 +148,12 @@
 
 
    (ui/with-shortcut :go/backward "bottom"
    (ui/with-shortcut :go/backward "bottom"
      [:button.it.navigation.nav-left.button.icon
      [:button.it.navigation.nav-left.button.icon
-      {:title "Go back" :on-click #(js/window.history.back)}
+      {:title (t :header/go-back) :on-click #(js/window.history.back)}
       (ui/icon "arrow-left" {:size ui/icon-size})])
       (ui/icon "arrow-left" {:size ui/icon-size})])
 
 
    (ui/with-shortcut :go/forward "bottom"
    (ui/with-shortcut :go/forward "bottom"
      [:button.it.navigation.nav-right.button.icon
      [:button.it.navigation.nav-right.button.icon
-      {:title "Go forward" :on-click #(js/window.history.forward)}
+      {:title (t :header/go-forward) :on-click #(js/window.history.forward)}
       (ui/icon "arrow-right" {:size ui/icon-size})])])
       (ui/icon "arrow-right" {:size ui/icon-size})])])
 
 
 (rum/defc updater-tips-new-version
 (rum/defc updater-tips-new-version
@@ -213,13 +218,13 @@
          (when-not (or (state/home?) custom-home-page? (state/whiteboard-dashboard?))
          (when-not (or (state/home?) custom-home-page? (state/whiteboard-dashboard?))
            (ui/with-shortcut :go/backward "bottom"
            (ui/with-shortcut :go/backward "bottom"
              [:button.it.navigation.nav-left.button.icon.opacity-70
              [:button.it.navigation.nav-left.button.icon.opacity-70
-              {:title "Go back" :on-click #(js/window.history.back)}
+              {:title (t :header/go-back) :on-click #(js/window.history.back)}
               (ui/icon "chevron-left" {:size 26})]))
               (ui/icon "chevron-left" {:size 26})]))
          ;; search button for non-mobile
          ;; search button for non-mobile
          (when current-repo
          (when current-repo
            (ui/with-shortcut :go/search "right"
            (ui/with-shortcut :go/search "right"
              [:button.button.icon#search-button
              [:button.button.icon#search-button
-              {:title "Search"
+              {:title (t :header/search)
                :on-click #(do (when (or (mobile-util/native-android?)
                :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))
@@ -269,7 +274,6 @@
                       :current-repo current-repo
                       :current-repo current-repo
                       :default-home default-home})
                       :default-home default-home})
 
 
-      (when (not (state/sub :ui/sidebar-open?))
-        (sidebar/toggle))
+      (sidebar/toggle)
 
 
       (updater-tips-new-version t)]]))
       (updater-tips-new-version t)]]))

+ 5 - 3
src/main/frontend/components/header.css

@@ -1,7 +1,9 @@
 .cp__header {
 .cp__header {
-  @apply z-10;
-
-  padding-top: var(--ls-headbar-inner-top-padding);
+  @apply shadow z-10;
+  -webkit-app-region: drag;
+  
+  padding-top: calc(var(--ls-headbar-inner-top-padding));
+  margin-top: var(--ls-win32-title-bar-height);
   height: calc(var(--ls-headbar-height) + var(--ls-headbar-inner-top-padding));
   height: calc(var(--ls-headbar-height) + var(--ls-headbar-inner-top-padding));
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;

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

@@ -16,7 +16,7 @@
                               [:span.mr-1 (t :help/forum-community)]
                               [:span.mr-1 (t :help/forum-community)]
                               (ui/icon "message-circle" {:style {:font-size 20}})]
                               (ui/icon "message-circle" {:style {:font-size 20}})]
          list
          list
-         [{:title "Usage"
+         [{:title (t :help/title-usage)
            :children [[[:a
            :children [[[:a
                         {:on-click (fn [] (state/sidebar-add-block! (state/get-current-repo) "shortcut-settings" :shortcut-settings))}
                         {:on-click (fn [] (state/sidebar-add-block! (state/get-current-repo) "shortcut-settings" :shortcut-settings))}
                         [:div.flex-row.inline-flex.items-center
                         [:div.flex-row.inline-flex.items-center
@@ -26,21 +26,21 @@
                       [(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 (t :help/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"]
                       [discourse-with-icon "https://discuss.logseq.com"]]}
                       [discourse-with-icon "https://discuss.logseq.com"]]}
 
 
-          {:title "Development"
+          {:title (t :help/title-development)
            :children [[(t :help/roadmap) "https://trello.com/b/8txSM12G/roadmap"]
            :children [[(t :help/roadmap) "https://trello.com/b/8txSM12G/roadmap"]
                       [(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 (t :help/title-about)
            :children [[(t :help/about) "https://blog.logseq.com/about/"]]}
            :children [[(t :help/about) "https://blog.logseq.com/about/"]]}
 
 
-          {:title "Terms"
+          {:title (t :help/title-terms)
            :children [[(t :help/privacy) "https://blog.logseq.com/privacy-policy/"]
            :children [[(t :help/privacy) "https://blog.logseq.com/privacy-policy/"]
                       [(t :help/terms) "https://blog.logseq.com/terms/"]]}]]
                       [(t :help/terms) "https://blog.logseq.com/terms/"]]}]]
 
 

+ 24 - 24
src/main/frontend/components/onboarding/quick_tour.cljs

@@ -18,7 +18,7 @@
   [^js jsTour]
   [^js jsTour]
   (let [^js el (js/document.createElement "button")]
   (let [^js el (js/document.createElement "button")]
     (.add (.-classList el) "cp__onboarding-skip-quick-tour")
     (.add (.-classList el) "cp__onboarding-skip-quick-tour")
-    (set! (.-innerHTML el) (h/render-html [:span [:i.ti.ti-player-skip-forward] "Skip Quick Tour"]))
+    (set! (.-innerHTML el) (h/render-html [:span [:i.ti.ti-player-skip-forward] (t :on-boarding/quick-tour-btn-skip)]))
     (.addEventListener el "click" #(.cancel jsTour))
     (.addEventListener el "click" #(.cancel jsTour))
     [#(.appendChild js/document.body el)
     [#(.appendChild js/document.body el)
      #(.removeChild js/document.body el)]))
      #(.removeChild js/document.body el)]))
@@ -36,21 +36,21 @@
 
 
   (h/render-html
   (h/render-html
    [:div.steps
    [:div.steps
-    [:strong (str "STEP " current)]
+    [:strong (str (t :on-boarding/quick-tour-steps) current)]
     [:ul (for [i (range total)] [:li {:class (when (= current (inc i)) "active")} i])]]))
     [:ul (for [i (range total)] [:li {:class (when (= current (inc i)) "active")} i])]]))
 
 
 (defn- create-steps! [^js jsTour]
 (defn- create-steps! [^js jsTour]
   [
   [
    ;; step 1
    ;; step 1
    {:id                "nav-help"
    {:id                "nav-help"
-    :text              (h/render-html [:section [:h2 "❓ Help"]
-                                       [:p "You can always click here for help and other information about Logseq."]])
+    :text              (h/render-html [:section [:h2 (t :on-boarding/quick-tour-help-title)]
+                                       [:p (t :on-boarding/quick-tour-help-desc)]])
     :attachTo          {:element ".cp__sidebar-help-btn" :on "top"}
     :attachTo          {:element ".cp__sidebar-help-btn" :on "top"}
     :beforeShowPromise #(if (state/sub :ui/sidebar-open?)
     :beforeShowPromise #(if (state/sub :ui/sidebar-open?)
                           (wait-target state/hide-right-sidebar! 700)
                           (wait-target state/hide-right-sidebar! 700)
                           (p/resolved true))
                           (p/resolved true))
     :canClickTarget    true
     :canClickTarget    true
-    :buttons           [{:text "Next" :action (.-next jsTour)}]
+    :buttons           [{:text (t :on-boarding/quick-tour-btn-next) :action (.-next jsTour)}]
     :popperOptions     {:modifiers [{:name    "preventOverflow"
     :popperOptions     {:modifiers [{:name    "preventOverflow"
                                      :options {:padding 20}}
                                      :options {:padding 20}}
                                     {:name    "offset"
                                     {:name    "offset"
@@ -58,11 +58,11 @@
 
 
    ;; step 2
    ;; step 2
    {:id                "nav-journal-page"
    {:id                "nav-journal-page"
-    :text              (h/render-html [:section [:h2 "📆 Daily Journal Page"]
+    :text              (h/render-html [:section [:h2 (t :on-boarding/quick-tour-journal-page-title)]
                                        [:p
                                        [:p
-                                        [:span "This is today’s daily journal page. Here you can dump your thoughts, learnings and ideas. Don’t worry about organizing. Just write and"]
-                                        [:a "[[link]]"]
-                                        [:span "your thoughts."]]])
+                                        [:span (t :on-boarding/quick-tour-journal-page-desc-1)]
+                                        [:a (t :on-boarding/quick-tour-journal-page-desc-2)]
+                                        [:span (t :on-boarding/quick-tour-journal-page-desc-3)]]])
 
 
     :attachTo          {:element ".page.is-journals .page-title" :on "top-end"}
     :attachTo          {:element ".page.is-journals .page-title" :on "top-end"}
     :beforeShowPromise #(if-not (= (util/safe-lower-case (state/get-current-page))
     :beforeShowPromise #(if-not (= (util/safe-lower-case (state/get-current-page))
@@ -71,8 +71,8 @@
                                          (route-handler/redirect-to-page! (date/today))
                                          (route-handler/redirect-to-page! (date/today))
                                          (util/scroll-to-top)) 200)
                                          (util/scroll-to-top)) 200)
                           (p/resolved true))
                           (p/resolved true))
-    :buttons           [{:text "Back" :classes "back" :action (.-back jsTour)}
-                        {:text "Next" :action (.-next jsTour)}]
+    :buttons           [{:text (t :on-boarding/quick-tour-btn-back) :classes "back" :action (.-back jsTour)}
+                        {:text (t :on-boarding/quick-tour-btn-next) :action (.-next jsTour)}]
     :popperOptions     {:modifiers [{:name    "preventOverflow"
     :popperOptions     {:modifiers [{:name    "preventOverflow"
                                      :options {:padding 63}}
                                      :options {:padding 63}}
                                     {:name    "offset"
                                     {:name    "offset"
@@ -80,13 +80,13 @@
 
 
    ;; step 3
    ;; step 3
    {:id                "nav-left-sidebar"
    {:id                "nav-left-sidebar"
-    :text              (h/render-html [:section [:h2 "👀 Left Sidebar"]
-                                       [:p [:span "Open the left sidebar to explore important menu items in Logseq."]]])
+    :text              (h/render-html [:section [:h2 (t :on-boarding/quick-tour-left-sidebar-title)]
+                                       [:p [:span (t :on-boarding/quick-tour-left-sidebar-desc)]]])
 
 
     :attachTo          {:element "#left-menu" :on "top"}
     :attachTo          {:element "#left-menu" :on "top"}
     :beforeShowPromise #(p/resolved true)
     :beforeShowPromise #(p/resolved true)
-    :buttons           [{:text "Back" :classes "back" :action (.-back jsTour)}
-                        {:text "Next" :action (.-next jsTour)}]
+    :buttons           [{:text (t :on-boarding/quick-tour-btn-back) :classes "back" :action (.-back jsTour)}
+                        {:text (t :on-boarding/quick-tour-btn-next) :action (.-next jsTour)}]
     :popperOptions     {:modifiers [{:name    "preventOverflow"
     :popperOptions     {:modifiers [{:name    "preventOverflow"
                                      :options {:padding 20}}
                                      :options {:padding 20}}
                                     {:name    "offset"
                                     {:name    "offset"
@@ -94,15 +94,15 @@
 
 
    ;; step 4
    ;; step 4
    {:id                "nav-favorites"
    {:id                "nav-favorites"
-    :text              (h/render-html [:section [:h2 "⭐️ Favorites"]
-                                       [:p "Pin your favorite pages via the `... `menu on any page."]
-                                       [:p "We’ve also added some template pages here to help you get started. You can remove these once you start writing your own notes."]])
+    :text              (h/render-html [:section [:h2 (t :on-boarding/quick-tour-favorites-title)]
+                                       [:p (t :on-boarding/quick-tour-favorites-desc-1)]
+                                       [:p (t :on-boarding/quick-tour-favorites-desc-2)]])
     :beforeShowPromise #(if-not (state/sub :ui/left-sidebar-open?)
     :beforeShowPromise #(if-not (state/sub :ui/left-sidebar-open?)
                           (wait-target state/toggle-left-sidebar! 500)
                           (wait-target state/toggle-left-sidebar! 500)
                           (p/resolved true))
                           (p/resolved true))
     :attachTo          {:element ".nav-content-item.favorites" :on "right"}
     :attachTo          {:element ".nav-content-item.favorites" :on "right"}
-    :buttons           [{:text "Back" :classes "back" :action (.-back jsTour)}
-                        {:text "Finish" :action (.-complete jsTour)}]}
+    :buttons           [{:text (t :on-boarding/quick-tour-btn-back) :classes "back" :action (.-back jsTour)}
+                        {:text (t :on-boarding/quick-tour-btn-finish) :action (.-complete jsTour)}]}
    ])
    ])
 
 
 (defn- create-steps-file-sync! [^js jsTour]
 (defn- create-steps-file-sync! [^js jsTour]
@@ -164,7 +164,7 @@
                          (wait-target ".nav-header .whiteboard" 500)
                          (wait-target ".nav-header .whiteboard" 500)
                          (util/scroll-to-top))
                          (util/scroll-to-top))
     :canClickTarget    true
     :canClickTarget    true
-    :buttons           [{:text "Next" :action (.-next jsTour)}]
+    :buttons           [{:text (t :on-boarding/tour-whiteboard-btn-next) :action (.-next jsTour)}]
     :popperOptions     {:modifiers [{:name    "preventOverflow"
     :popperOptions     {:modifiers [{:name    "preventOverflow"
                                      :options {:padding 20}}
                                      :options {:padding 20}}
                                     {:name    "offset"
                                     {:name    "offset"
@@ -178,8 +178,8 @@
                          (route-handler/redirect-to-whiteboard-dashboard!)
                          (route-handler/redirect-to-whiteboard-dashboard!)
                          (wait-target ".dashboard-create-card" 500))
                          (wait-target ".dashboard-create-card" 500))
     :attachTo          {:element ".dashboard-create-card" :on "bottom"}
     :attachTo          {:element ".dashboard-create-card" :on "bottom"}
-    :buttons           [{:text "Back" :classes "back" :action (.-back jsTour)}
-                        {:text "Finish" :action (.-complete jsTour)}]
+    :buttons           [{:text (t :on-boarding/tour-whiteboard-btn-back) :classes "back" :action (.-back jsTour)}
+                        {:text (t :on-boarding/tour-whiteboard-btn-finish) :action (.-complete jsTour)}]
     :popperOptions     {:modifiers [{:name    "preventOverflow"
     :popperOptions     {:modifiers [{:name    "preventOverflow"
                                      :options {:padding 20}}
                                      :options {:padding 20}}
                                     {:name    "offset"
                                     {:name    "offset"
@@ -277,7 +277,7 @@
 
 
 (defn init []
 (defn init []
   (command-palette/register {:id     :document/quick-tour
   (command-palette/register {:id     :document/quick-tour
-                             :desc   "Quick tour for onboarding"
+                             :desc   (t :on-boarding/command-palette-quick-tour)
                              :action #(ready start)})
                              :action #(ready start)})
 
 
   ;; TODO: fix logic
   ;; TODO: fix logic

+ 20 - 20
src/main/frontend/components/onboarding/setups.cljs

@@ -30,13 +30,13 @@
 
 
       [:h1.text-xl
       [:h1.text-xl
        (if picker?
        (if picker?
-         [:span [:strong (ui/icon "heart")] "Welcome to " [:strong "Logseq!"]]
-         [:span [:strong (ui/icon "file-import")] "Import existing notes"])]
+         [:span [:strong (ui/icon "heart")] (t :on-boarding/main-title) [:strong "Logseq!"]]
+         [:span [:strong (ui/icon "file-import")] (t :on-boarding/importing-main-title)])]
 
 
       [:h2
       [:h2
        (if picker?
        (if picker?
-         "First you need to choose a folder where Logseq will store your thoughts, ideas, notes."
-         "You can also do this later in the app.")]
+         (t :on-boarding/main-desc)
+         (t :on-boarding/importing-main-desc))]
 
 
       content])])
       content])])
 
 
@@ -93,8 +93,8 @@
 
 
               (if parsing?
               (if parsing?
                 (ui/loading "")
                 (ui/loading "")
-                [[:strong "Choose a folder"]
-                 [:small "Open existing directory or Create a new one"]])]]]
+                [[:strong (t :on-boarding/section-btn-title)]
+                 [:small (t :on-boarding/section-btn-desc)]])]]]
            [:div.px-5
            [:div.px-5
             (ui/admonition :warning
             (ui/admonition :warning
                            (widgets/native-fs-api-alert))]))]
                            (widgets/native-fs-api-alert))]))]
@@ -102,22 +102,22 @@
        [:p.flex
        [:p.flex
         [:i.as-flex-center (ui/icon "zoom-question" {:style {:fontSize "22px"}})]
         [:i.as-flex-center (ui/icon "zoom-question" {:style {:fontSize "22px"}})]
         [:span.flex-1.flex.flex-col
         [:span.flex-1.flex.flex-col
-         [:strong "How Logseq saves your work"]
-         [:small.opacity-60 "Inside the directory you choose, Logseq will create 4 folders."]]]
+         [:strong (t :on-boarding/section-title)]
+         [:small.opacity-60 (t :on-boarding/section-desc)]]]
 
 
        [:p.text-sm.pt-5.tracking-wide
        [:p.text-sm.pt-5.tracking-wide
-        [:span (str "Each page is a file stored only on your " DEVICE ".")]
+        [:span (str (t :on-boarding/section-tip-1) DEVICE ".")]
         [:br]
         [:br]
-        [:span "You may choose to sync it later."]]
+        [:span (t :on-boarding/section-tip-2)]]
 
 
        [:ul
        [:ul
         (for [[title label icon]
         (for [[title label icon]
-              [["Graphics & Documents" "/assets" "whiteboard"]
-               ["Daily notes" "/journals" "calendar-plus"]
-               ["PAGES" "/pages" "page"]
+              [[(t :on-boarding/section-assets) "/assets" "whiteboard"]
+               [(t :on-boarding/section-journals) "/journals" "calendar-plus"]
+               [(t :on-boarding/section-pages) "/pages" "page"]
                []
                []
-               ["APP Internal" "/logseq" "tool"]
-               ["Config File" "/logseq/config.edn"]]]
+               [(t :on-boarding/section-app) "/logseq" "tool"]
+               [(t :on-boarding/section-config) "/logseq/config.edn"]]]
           (if-not title
           (if-not title
             [:li.hr]
             [:li.hr]
             [:li
             [:li
@@ -221,14 +221,14 @@
      :importer
      :importer
      [:article.flex.flex-col.items-center.importer.py-16.px-8
      [:article.flex.flex-col.items-center.importer.py-16.px-8
       [:section.c.text-center
       [:section.c.text-center
-       [:h1 "Do you already have notes that you want to import?"]
-       [:h2 "If they are in a JSON, EDN or Markdown format Logseq can work with them."]]
+       [:h1 (t :on-boarding/importing-title)]
+       [:h2 (t :on-boarding/importing-desc)]]
       [: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
         [: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
          [[:strong "RoamResearch"]
          [[:strong "RoamResearch"]
-          [:small "Import a JSON Export of your Roam graph"]]]
+          [:small (t :on-boarding/importing-roam-desc)]]]
         [:input.absolute.hidden
         [:input.absolute.hidden
          {:id        "import-roam"
          {:id        "import-roam"
           :type      "file"
           :type      "file"
@@ -238,7 +238,7 @@
         [:span.as-flex-center [:i (svg/logo 28)]]
         [:span.as-flex-center [:i (svg/logo 28)]]
         [:span.flex.flex-col
         [:span.flex.flex-col
          [[:strong "EDN / JSON"]
          [[:strong "EDN / JSON"]
-          [:small "Import an EDN or a JSON Export of your Logseq graph"]]]
+          [:small (t :on-boarding/importing-lsq-desc)]]]
         [:input.absolute.hidden
         [:input.absolute.hidden
          {:id        "import-lsq"
          {:id        "import-lsq"
           :type      "file"
           :type      "file"
@@ -248,7 +248,7 @@
         [: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
          [[:strong "OPML"]
          [[:strong "OPML"]
-          [:small " Import OPML files"]]]
+          [:small (t :on-boarding/importing-opml-desc)]]]
 
 
         [:input.absolute.hidden
         [:input.absolute.hidden
          {:id        "import-opml"
          {:id        "import-opml"

+ 18 - 20
src/main/frontend/components/page.cljs

@@ -202,8 +202,7 @@
          [:ul.mt-2
          [:ul.mt-2
           (for [[original-name name] (sort-by last pages)]
           (for [[original-name name] (sort-by last pages)]
             [:li {:key (str "tagged-page-" name)}
             [:li {:key (str "tagged-page-" name)}
-             [:a {:href (rfe/href :page {:name name})}
-              original-name]])]
+             (component-block/page-cp {} {:block/name name :block/original-name original-name})])]
          {:default-collapsed? false})]])))
          {:default-collapsed? false})]])))
 
 
 (rum/defc page-title-editor < rum/reactive
 (rum/defc page-title-editor < rum/reactive
@@ -213,6 +212,11 @@
                              (util/page-name-sanity-lc @*title-value))
                              (util/page-name-sanity-lc @*title-value))
                        (db/page-exists? page-name)
                        (db/page-exists? page-name)
                        (db/page-exists? @*title-value))
                        (db/page-exists? @*title-value))
+        rollback-fn #(do
+                       (reset! *title-value old-name)
+                       (gobj/set (rum/deref input-ref) "value" old-name)
+                       (reset! *edit? true)
+                       (.focus (rum/deref input-ref)))
         confirm-fn (fn []
         confirm-fn (fn []
                      (let [new-page-name (string/trim @*title-value)]
                      (let [new-page-name (string/trim @*title-value)]
                        (ui/make-confirm-modal
                        (ui/make-confirm-modal
@@ -223,16 +227,7 @@
                                           (close-fn)
                                           (close-fn)
                                           (page-handler/rename! (or title page-name) @*title-value)
                                           (page-handler/rename! (or title page-name) @*title-value)
                                           (reset! *edit? false))
                                           (reset! *edit? false))
-                         :on-cancel     (fn []
-                                          (reset! *title-value old-name)
-                                          (gobj/set (rum/deref input-ref) "value" old-name)
-                                          (reset! *edit? true)
-                                          (.focus (rum/deref input-ref)))})))
-        rollback-fn #(do
-                       (reset! *title-value old-name)
-                       (gobj/set (rum/deref input-ref) "value" old-name)
-                       (reset! *edit? false)
-                       (when-not untitled? (notification/show! "Illegal page name, can not rename!" :warning)))
+                         :on-cancel     rollback-fn})))
         blur-fn (fn [e]
         blur-fn (fn [e]
                   (when (gp-util/wrapped-by-quotes? @*title-value)
                   (when (gp-util/wrapped-by-quotes? @*title-value)
                     (swap! *title-value gp-util/unquote-string)
                     (swap! *title-value gp-util/unquote-string)
@@ -242,13 +237,16 @@
                     (reset! *edit? false)
                     (reset! *edit? false)
 
 
                     (string/blank? @*title-value)
                     (string/blank? @*title-value)
-                    (rollback-fn)
+                    (do (when-not untitled? (notification/show! (t :page/illegal-page-name) :warning))
+                        (rollback-fn))
 
 
-                    (and (collide?) whiteboard-page?)
-                    (notification/show! (str "Page “" @*title-value "” already exists!") :error)
+                    (and (collide?) (or whiteboard-page? (model/whiteboard-page? @*title-value)))
+                    (do (notification/show! (t :page/page-already-exists @*title-value) :error)
+                        (rollback-fn))
 
 
                     (and (date/valid-journal-title? @*title-value) whiteboard-page?)
                     (and (date/valid-journal-title? @*title-value) whiteboard-page?)
-                    (notification/show! (str "Whiteboard page cannot be renamed with journal titles!") :error)
+                    (do (notification/show! (t :page/whiteboard-to-journal-error) :error)
+                        (rollback-fn))
 
 
                     untitled?
                     untitled?
                     (page-handler/rename! (or title page-name) @*title-value)
                     (page-handler/rename! (or title page-name) @*title-value)
@@ -815,7 +813,7 @@
       [:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
       [:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
        [:h3#modal-headline.text-lg.leading-6.font-medium
        [:h3#modal-headline.text-lg.leading-6.font-medium
         (if orphaned-pages?
         (if orphaned-pages?
-          (str (t :remove-orphaned-pages) "?")
+          (t :remove-orphaned-pages)
           (t :page/delete-confirmation))]]]
           (t :page/delete-confirmation))]]]
 
 
      [:table.table-auto.cp__all_pages_table.mt-4
      [:table.table-auto.cp__all_pages_table.mt-4
@@ -851,7 +849,7 @@
                     (close-fn)
                     (close-fn)
                     (doseq [page-name (map :block/name pages)]
                     (doseq [page-name (map :block/name pages)]
                       (page-handler/delete! page-name #()))
                       (page-handler/delete! page-name #()))
-                    (notification/show! (str (t :tips/all-done) "!") :success)
+                    (notification/show! (t :tips/all-done) :success)
                     (js/setTimeout #(refresh-fn) 200)))]]))
                     (js/setTimeout #(refresh-fn) 200)))]]))
 
 
 (rum/defc pagination
 (rum/defc pagination
@@ -1036,7 +1034,7 @@
          [:div.r.flex.items-center.justify-between
          [:div.r.flex.items-center.justify-between
           [:div
           [:div
            (ui/tippy
            (ui/tippy
-            {:html  [:small (str (t :page/show-whiteboards) " ?")]
+            {:html  [:small (t :page/show-whiteboards)]
              :arrow true}
              :arrow true}
             [:a.button.whiteboard
             [:a.button.whiteboard
              {:class    (util/classnames [{:active (boolean @*whiteboard?)}])
              {:class    (util/classnames [{:active (boolean @*whiteboard?)}])
@@ -1044,7 +1042,7 @@
              (ui/icon "whiteboard" {:extension? true :style {:fontSize ui/icon-size}})])]
              (ui/icon "whiteboard" {:extension? true :style {:fontSize ui/icon-size}})])]
           [:div
           [:div
            (ui/tippy
            (ui/tippy
-            {:html  [:small (str (t :page/show-journals) " ?")]
+            {:html  [:small (t :page/show-journals)]
              :arrow true}
              :arrow true}
             [:a.button.journal
             [:a.button.journal
              {:class    (util/classnames [{:active (boolean @*journal?)}])
              {:class    (util/classnames [{:active (boolean @*journal?)}])

+ 100 - 27
src/main/frontend/components/plugins.cljs

@@ -5,6 +5,7 @@
             [frontend.context.i18n :refer [t]]
             [frontend.context.i18n :refer [t]]
             [frontend.ui :as ui]
             [frontend.ui :as ui]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.ui :as ui-handler]
+            [frontend.handler.editor :as editor-handler]
             [frontend.handler.plugin-config :as plugin-config-handler]
             [frontend.handler.plugin-config :as plugin-config-handler]
             [frontend.handler.common.plugin :as plugin-common-handler]
             [frontend.handler.common.plugin :as plugin-common-handler]
             [frontend.search :as search]
             [frontend.search :as search]
@@ -191,10 +192,7 @@
   (ui/admonition
   (ui/admonition
     :warning
     :warning
     [:p.text-md
     [:p.text-md
-     "Plugins can access your graph and your local files, issue network requests.
-       They can also cause data corruption or loss. We're working on proper access rules for your graphs.
-       Meanwhile, make sure you have regular backups of your graphs and only install the plugins when you can read and
-       understand the source code."]))
+     (t :plugin/security-warning)]))
 
 
 (rum/defc card-ctls-of-market < rum/static
 (rum/defc card-ctls-of-market < rum/static
   [item stat installed? installing-or-updating?]
   [item stat installed? installing-or-updating?]
@@ -273,7 +271,7 @@
         (if installing-or-updating?
         (if installing-or-updating?
           (t :plugin/updating)
           (t :plugin/updating)
           (if new-version
           (if new-version
-            (str (t :plugin/update) " 👉 " new-version)
+            [:span (t :plugin/update) " 👉 " new-version]
             (t :plugin/check-update)))]])
             (t :plugin/check-update)))]])
 
 
     (ui/toggle (not disabled?)
     (ui/toggle (not disabled?)
@@ -365,7 +363,7 @@
                     (.focus target))}
                     (.focus target))}
       (ui/icon "x")])
       (ui/icon "x")])
    [:input.form-input.is-small
    [:input.form-input.is-small
-    {:placeholder "Search plugins"
+    {:placeholder (t :plugin/search-plugin)
      :ref         *search-ref
      :ref         *search-ref
      :auto-focus  true
      :auto-focus  true
      :on-key-down (fn [^js e]
      :on-key-down (fn [^js e]
@@ -459,6 +457,22 @@
                                 (state/set-state! [:electron/user-cfgs :settings/agent] opts)
                                 (state/set-state! [:electron/user-cfgs :settings/agent] opts)
                                 (state/close-sub-modal! :https-proxy-panel))))]]]))
                                 (state/close-sub-modal! :https-proxy-panel))))]]]))
 
 
+(rum/defc auto-check-for-updates-control
+  []
+  (let [[enabled, set-enabled!] (rum/use-state (plugin-handler/get-enabled-auto-check-for-updates?))
+        text (t :plugin/auto-check-for-updates)]
+
+    [:div.flex.items-center.justify-between.px-4.py-2
+     {:on-click (fn []
+                  (let [t (not enabled)]
+                    (set-enabled! t)
+                    (plugin-handler/set-enabled-auto-check-for-updates t)
+                    (notification/show!
+                      [:span text [:strong.pl-1 (if t "ON" "OFF")] "!"]
+                      (if t :success :info))))}
+     [:span.pr-3.opacity-80 text]
+     (ui/toggle enabled #() true)]))
+
 (rum/defc ^:large-vars/cleanup-todo panel-control-tabs < rum/static
 (rum/defc ^:large-vars/cleanup-todo panel-control-tabs < rum/static
   [search-key *search-key category *category
   [search-key *search-key category *category
    sort-by *sort-by filter-by *filter-by total-nums
    sort-by *sort-by filter-by *filter-by total-nums
@@ -557,7 +571,7 @@
               :options {:on-click #(reset! *sort-by :stars)}
               :options {:on-click #(reset! *sort-by :stars)}
               :icon    (ui/icon (aim-icon :stars))}
               :icon    (ui/icon (aim-icon :stars))}
 
 
-             {:title   (str (t :plugin/title) " (A - Z)")
+             {:title   (t :plugin/title "A - Z")
               :options {:on-click #(reset! *sort-by :letters)}
               :options {:on-click #(reset! *sort-by :letters)}
               :icon    (ui/icon (aim-icon :letters))}])
               :icon    (ui/icon (aim-icon :letters))}])
           {}))
           {}))
@@ -585,14 +599,18 @@
 
 
                 (when (state/developer-mode?)
                 (when (state/developer-mode?)
                   [{:hr true}
                   [{:hr true}
-                   {:title   [:span.flex.items-center (ui/icon "file-code") "Open Preferences"]
+                   {:title   [:span.flex.items-center (ui/icon "file-code") (t :plugin/open-preferences)]
                     :options {:on-click
                     :options {:on-click
                               #(p/let [root (plugin-handler/get-ls-dotdir-root)]
                               #(p/let [root (plugin-handler/get-ls-dotdir-root)]
                                  (js/apis.openPath (str root "/preferences.json")))}}
                                  (js/apis.openPath (str root "/preferences.json")))}}
-                   {:title   [:span.flex.items-center (ui/icon "bug") "Open " [:code " ~/.logseq"]]
+                   {:title   [:span.flex.items-center.whitespace-nowrap.space-x-1 (ui/icon "bug") (t :plugin/open-logseq-dir) [:code "~/.logseq"]]
                     :options {:on-click
                     :options {:on-click
                               #(p/let [root (plugin-handler/get-ls-dotdir-root)]
                               #(p/let [root (plugin-handler/get-ls-dotdir-root)]
-                                 (js/apis.openPath root))}}]))
+                                 (js/apis.openPath root))}}])
+
+                [{:hr true :key "dropdown-more"}
+                 {:title   (auto-check-for-updates-control)
+                  :options {:no-padding? true}}])
         {})
         {})
 
 
       ;; developer
       ;; developer
@@ -730,7 +748,7 @@
        [:p.flex.justify-center.py-20 svg/loading]
        [:p.flex.justify-center.py-20 svg/loading]
 
 
        @*error
        @*error
-       [:p.flex.justify-center.pt-20.opacity-50 "Remote error: " (.-message @*error)]
+       [:p.flex.justify-center.pt-20.opacity-50 (t :plugin/remote-error) (.-message @*error)]
 
 
        :else
        :else
        [:div.cp__plugins-marketplace-cnt
        [:div.cp__plugins-marketplace-cnt
@@ -895,7 +913,7 @@
                 [:span.opacity-30.hover:opacity-80 (ui/icon "info-circle")]))]])]
                 [:span.opacity-30.hover:opacity-80 (ui/icon "info-circle")]))]])]
 
 
        ;; all done
        ;; all done
-       [:div.py-4 [:strong.text-4xl "\uD83C\uDF89 All updated!"]])
+       [:div.py-4 [:strong.text-4xl (str "\uD83C\uDF89 " (t :plugin/all-updated))]])
 
 
      ;; actions
      ;; actions
      (when (seq updates)
      (when (seq updates)
@@ -1047,7 +1065,11 @@
                       [:span (t :plugin/found-updates)] (ui/point "bg-red-600" 5 {:style {:margin-top 2}})]
                       [:span (t :plugin/found-updates)] (ui/point "bg-red-600" 5 {:style {:margin-top 2}})]
             :options {:on-click #(open-waiting-updates-modal!)
             :options {:on-click #(open-waiting-updates-modal!)
                       :class    "extra-item"}
                       :class    "extra-item"}
-            :icon    (ui/icon "download")})])
+            :icon    (ui/icon "download")})]
+
+        [{:hr true :key "dropdown-more"}
+         {:title (auto-check-for-updates-control)
+          :options {:no-padding? true}}])
       {:trigger-class "toolbar-plugins-manager-trigger"})))
       {:trigger-class "toolbar-plugins-manager-trigger"})))
 
 
 (rum/defc header-ui-items-list-wrap
 (rum/defc header-ui-items-list-wrap
@@ -1116,14 +1138,61 @@
             (let [updates-coming (state/sub :plugin/updates-coming)]
             (let [updates-coming (state/sub :plugin/updates-coming)]
               (toolbar-plugins-manager-list updates-coming items)))]]))))
               (toolbar-plugins-manager-list updates-coming items)))]]))))
 
 
-(rum/defcs hook-ui-fenced-code < rum/reactive
-  [_state content {:keys [render edit] :as _opts}]
+(rum/defc hook-ui-fenced-code
+  [block content {:keys [render edit] :as _opts}]
 
 
-  [:div
-   {:on-mouse-down (fn [e] (when (false? edit) (util/stop e)))
-    :class         (util/classnames [{:not-edit (false? edit)}])}
-   (when (fn? render)
-     (js/React.createElement render #js {:content content}))])
+  (let [[content1 set-content1!] (rum/use-state content)
+        [editor-active? set-editor-active!] (rum/use-state (string/blank? content))
+        *cm (rum/use-ref nil)
+        *el (rum/use-ref nil)]
+
+    (rum/use-effect!
+      #(set-content1! content)
+      [content])
+
+    (rum/use-effect!
+      (fn []
+        (some-> (rum/deref *el)
+                (.closest ".ui-fenced-code-wrap")
+                (.-classList)
+                (#(if editor-active?
+                    (.add % "is-active")
+                    (.remove % "is-active"))))
+        (when-let [cm (rum/deref *cm)]
+          (.refresh cm)
+          (.focus cm)
+          (.setCursor cm (.lineCount cm) (count (.getLine cm (.lastLine cm))))))
+      [editor-active?])
+
+    (rum/use-effect!
+      (fn []
+        (let [t (js/setTimeout
+                  #(when-let [^js cm (some-> (rum/deref *el)
+                                             (.closest ".ui-fenced-code-wrap")
+                                             (.querySelector ".CodeMirror")
+                                             (.-CodeMirror))]
+                     (rum/set-ref! *cm cm)
+                     (doto cm
+                       (.on "change" (fn []
+                                       (some-> cm (.getDoc) (.getValue) (set-content1!))))))
+                  ;; wait for the cm loaded
+                  1000)]
+          #(js/clearTimeout t)))
+      [])
+
+    [:div.ui-fenced-code-result
+     {:on-mouse-down (fn [e] (when (false? edit) (util/stop e)))
+      :class         (util/classnames [{:not-edit (false? edit)}])
+      :ref           *el}
+     [:<>
+      [:span.actions
+       {:on-mouse-down #(util/stop %)}
+       (ui/button (ui/icon "square-toggle-horizontal" {:size 14})
+                  :on-click #(set-editor-active! (not editor-active?)))
+       (ui/button (ui/icon "source-code" {:size 14})
+                  :on-click #(editor-handler/edit-block! block (count content1) (:block/uuid block)))]
+      (when (fn? render)
+        (js/React.createElement render #js {:content content1}))]]))
 
 
 (rum/defc plugins-page
 (rum/defc plugins-page
   []
   []
@@ -1183,7 +1252,7 @@
         [sub-content, _set-sub-content!] (rum-utils/use-atom *updates-sub-content)
         [sub-content, _set-sub-content!] (rum-utils/use-atom *updates-sub-content)
         notify! (fn [content status]
         notify! (fn [content status]
                   (if auto-checking?
                   (if auto-checking?
-                    (println "Plugin Updates: " content)
+                    (println (t :plugin/list-of-updates) content)
                     (let [cb #(plugin-handler/cancel-user-checking!)]
                     (let [cb #(plugin-handler/cancel-user-checking!)]
                       (try
                       (try
                         (set-uid (notification/show! content status false uid nil cb))
                         (set-uid (notification/show! content status false uid nil cb))
@@ -1195,7 +1264,7 @@
         (if check-pending?
         (if check-pending?
           (notify!
           (notify!
             [:div
             [:div
-             [:div (str "Checking for plugin updates ...")]
+             [:div (t :plugin/checking-for-updates)]
              (when sub-content [:p.opacity-60 sub-content])]
              (when sub-content [:p.opacity-60 sub-content])]
             (ui/loading ""))
             (ui/loading ""))
           (when uid (notification/clear! uid))))
           (when uid (notification/clear! uid))))
@@ -1206,9 +1275,11 @@
       (fn []
       (fn []
         (when online?
         (when online?
           (let [last-updates (storage/get :lsp-last-auto-updates)]
           (let [last-updates (storage/get :lsp-last-auto-updates)]
-            (when (or (not (number? last-updates))
-                      ;; interval 12 hours
-                      (> (- (js/Date.now) last-updates) (* 60 60 12 1000)))
+            (when (and (not (false? last-updates))
+                       (or (true? last-updates)
+                           (not (number? last-updates))
+                           ;; interval 12 hours
+                           (> (- (js/Date.now) last-updates) (* 60 60 12 1000))))
               (js/setTimeout
               (js/setTimeout
                 (fn []
                 (fn []
                   (plugin-handler/auto-check-enabled-for-updates!)
                   (plugin-handler/auto-check-enabled-for-updates!)
@@ -1237,7 +1308,8 @@
 
 
     [:div.cp__plugins-settings.cp__settings-main
     [:div.cp__plugins-settings.cp__settings-main
      [:header
      [:header
-      [:h1.title (ui/icon "puzzle") (str " " (or title (t :settings-of-plugins)))]]
+      [:h1.title (ui/icon "puzzle" {:size 22})
+       [:strong (or title (t :settings-of-plugins))]]]
 
 
      [:div.cp__settings-inner.md:flex
      [:div.cp__settings-inner.md:flex
       {:class (util/classnames [{:no-aside (not nav?)}])}
       {:class (util/classnames [{:no-aside (not nav?)}])}
@@ -1247,7 +1319,7 @@
            [:ul.settings-plugin-list
            [:ul.settings-plugin-list
             (for [{:keys [id name title icon]} plugins]
             (for [{:keys [id name title icon]} plugins]
               [:li
               [:li
-               {:class (util/classnames [{:active (= id focused)}])}
+               {:key id :class (util/classnames [{:active (= id focused)}])}
                [:a.flex.items-center.settings-plugin-item
                [:a.flex.items-center.settings-plugin-item
                 {:data-id  id
                 {:data-id  id
                  :on-click #(do (state/set-state! :plugin/focused-settings id))}
                  :on-click #(do (state/set-state! :plugin/focused-settings id))}
@@ -1332,4 +1404,5 @@
       [:div.settings-modal.of-plugins
       [:div.settings-modal.of-plugins
        (focused-settings-content title)])
        (focused-settings-content title)])
     {:center? false
     {:center? false
+     :label   "plugin-settings-modal"
      :id      "ls-focused-settings-modal"}))
      :id      "ls-focused-settings-modal"}))

+ 65 - 16
src/main/frontend/components/plugins.css

@@ -482,6 +482,15 @@
   }
   }
 
 
   &-settings {
   &-settings {
+    > header {
+      padding: 8px 12px;
+      border-bottom: 1px solid var(--ls-quaternary-background-color);
+
+      h1 {
+        @apply flex items-center text-[22px] m-0 space-x-1;
+      }
+    }
+
     &-inner {
     &-inner {
       position: relative;
       position: relative;
       padding: 10px 0 20px;
       padding: 10px 0 20px;
@@ -598,25 +607,23 @@
       }
       }
     }
     }
 
 
-    aside {
-      max-height: 70vh;
-      overflow: auto;
-      margin-bottom: -17px;
+    .cp__settings-inner {
+      aside {
+        @apply max-h-[70vh] overflow-auto mb-[-17px] p-3;
 
 
-      ul {
-        img.icon {
-          height: 24px;
-          width: 24px;
-        }
+        ul {
+          @apply list-none p-0 m-0;
 
 
-        li {
-          strong {
-            font-weight: 400;
-            overflow: hidden;
-            height: 22px;
+          img.icon {
+            @apply w-[24px] h-[24px];
+          }
 
 
-            text-overflow: ellipsis;
-            white-space: nowrap;
+          li {
+            @apply p-1.5 rounded;
+
+            strong {
+              @apply overflow-hidden text-ellipsis whitespace-nowrap;
+            }
           }
           }
         }
         }
       }
       }
@@ -898,6 +905,42 @@
   }
   }
 }
 }
 
 
+.ui-fenced-code {
+  &-wrap {
+    &:not(.is-active) {
+      .ui-fenced-code-editor {
+        display: none !important;
+      }
+    }
+
+    &.is-active {
+      .ui-fenced-code-result {
+        @apply pt-2;
+      }
+    }
+  }
+
+  &-result {
+    @apply relative;
+
+    > .actions {
+      @apply absolute top-2 right-1 z-10 opacity-0 transition-opacity;
+
+      .ui__button {
+        font-size: 13px;
+        padding: 4px;
+        margin-left: 6px;
+      }
+    }
+
+    &:hover {
+      > .actions {
+        @apply opacity-100;
+      }
+    }
+  }
+}
+
 .lsp-frame-readme {
 .lsp-frame-readme {
   margin: -2rem;
   margin: -2rem;
   min-height: 75vh;
   min-height: 75vh;
@@ -939,6 +982,12 @@ html[data-theme='dark'] {
   }
   }
 }
 }
 
 
+.ui__modal[label=plugin-settings-modal] {
+  .ui__modal-close-wrap {
+    padding-top: 14px;
+  }
+}
+
 .ui__modal[label=plugins-dashboard] {
 .ui__modal[label=plugins-dashboard] {
   .panel-content {
   .panel-content {
     overflow-y: auto;
     overflow-y: auto;

+ 10 - 13
src/main/frontend/components/query.cljs

@@ -59,7 +59,6 @@
                                      (:block/name (ffirst result))
                                      (:block/name (ffirst result))
                                      (:block/uuid (first (second (first result))))
                                      (:block/uuid (first (second (first result))))
                                      true)]
                                      true)]
-    (println "this should be a function" inline)
     (if @*query-error
     (if @*query-error
       (do
       (do
         (log/error :exception @*query-error)
         (log/error :exception @*query-error)
@@ -142,10 +141,8 @@
              (when-not (or built-in? dsl-query?)
              (when-not (or built-in? dsl-query?)
                (when collapsed?
                (when collapsed?
                  (editor-handler/collapse-block! current-block-uuid))))
                  (editor-handler/collapse-block! current-block-uuid))))
-           state)}
-  (rum/local nil ::query-result)
-  {:init (fn [state] (assoc state :query-error (atom nil)))}
-  [state config {:keys [title builder query view collapsed? table-view?] :as q} *query-triggered?]
+           (assoc state :query-error (atom nil)))}
+  [state config {:keys [title builder query view collapsed? table-view?] :as q}]
   (let [*query-error (:query-error state)
   (let [*query-error (:query-error state)
         built-in? (built-in-custom-query? title)
         built-in? (built-in-custom-query? title)
         dsl-query? (:dsl-query? config)
         dsl-query? (:dsl-query? config)
@@ -166,11 +163,12 @@
         view-f (and view-fn (sci/eval-string (pr-str view-fn)))
         view-f (and view-fn (sci/eval-string (pr-str view-fn)))
         dsl-page-query? (and dsl-query?
         dsl-page-query? (and dsl-query?
                              (false? (:blocks? (query-dsl/parse-query query))))
                              (false? (:blocks? (query-dsl/parse-query query))))
+        ;; FIXME: This isn't getting set for full-text searches
         full-text-search? (and dsl-query?
         full-text-search? (and dsl-query?
                                (util/electron?)
                                (util/electron?)
                                (symbol? (gp-util/safe-read-string query)))
                                (symbol? (gp-util/safe-read-string query)))
         result (when (or built-in-collapsed? (not collapsed?'))
         result (when (or built-in-collapsed? (not collapsed?'))
-                 (query-result/get-query-result state config *query-error *query-triggered? current-block-uuid q {:table? table?}))
+                 (query-result/get-query-result config q *query-error current-block-uuid {:table? table?}))
         query-time (:query-time (meta result))
         query-time (:query-time (meta result))
         page-list? (and (seq result)
         page-list? (and (seq result)
                         (some? (:block/name (first result))))
                         (some? (:block/name (first result))))
@@ -209,13 +207,13 @@
                  (if table?
                  (if table?
                    [:a.flex.ml-1.fade-link {:title "Switch to list view"
                    [:a.flex.ml-1.fade-link {:title "Switch to list view"
                                             :on-click (fn [] (editor-property/set-block-property! current-block-uuid
                                             :on-click (fn [] (editor-property/set-block-property! current-block-uuid
-                                                                                                 "query-table"
-                                                                                                 false))}
+                                                                                                  "query-table"
+                                                                                                  false))}
                     (ui/icon "list" {:style {:font-size 20}})]
                     (ui/icon "list" {:style {:font-size 20}})]
                    [:a.flex.ml-1.fade-link {:title "Switch to table view"
                    [:a.flex.ml-1.fade-link {:title "Switch to table view"
                                             :on-click (fn [] (editor-property/set-block-property! current-block-uuid
                                             :on-click (fn [] (editor-property/set-block-property! current-block-uuid
-                                                                                                 "query-table"
-                                                                                                 true))}
+                                                                                                  "query-table"
+                                                                                                  true))}
                     (ui/icon "table" {:style {:font-size 20}})]))
                     (ui/icon "table" {:style {:font-size 20}})]))
 
 
                [:a.flex.ml-1.fade-link
                [:a.flex.ml-1.fade-link
@@ -231,7 +229,7 @@
                   (query-refresh-button query-time {:full-text-search? full-text-search?
                   (query-refresh-button query-time {:full-text-search? full-text-search?
                                                     :on-mouse-down (fn [e]
                                                     :on-mouse-down (fn [e]
                                                                      (util/stop e)
                                                                      (util/stop e)
-                                                                     (query-result/trigger-custom-query! state *query-error *query-triggered?))}))]])])
+                                                                     (query-result/trigger-custom-query! config q *query-error))}))]])])
 
 
          (when dsl-query? builder)
          (when dsl-query? builder)
 
 
@@ -248,11 +246,10 @@
               (custom-query-inner config q opts))])]))))
               (custom-query-inner config q opts))])]))))
 
 
 (rum/defcs custom-query < rum/static
 (rum/defcs custom-query < rum/static
-  (rum/local false ::query-triggered?)
   [state config q]
   [state config q]
   (ui/catch-error
   (ui/catch-error
    (ui/block-error "Query Error:" {:content (:query q)})
    (ui/block-error "Query Error:" {:content (:query q)})
    (ui/lazy-visible
    (ui/lazy-visible
     (fn []
     (fn []
-      (custom-query* config q (::query-triggered? state)))
+      (custom-query* config q))
     {:debug-id q})))
     {:debug-id q})))

+ 29 - 29
src/main/frontend/components/query/result.cljs

@@ -13,9 +13,8 @@
             [frontend.modules.outliner.tree :as tree]))
             [frontend.modules.outliner.tree :as tree]))
 
 
 (defn trigger-custom-query!
 (defn trigger-custom-query!
-  [state *query-error *query-triggered?]
-  (let [[config query] (:rum/args state)
-        repo (state/get-current-repo)
+  [config query *query-error]
+  (let [repo (state/get-current-repo)
         result-atom (atom nil)
         result-atom (atom nil)
         current-block-uuid (or (:block/uuid (:block config))
         current-block-uuid (or (:block/uuid (:block config))
                                (:block/uuid config))
                                (:block/uuid config))
@@ -45,40 +44,41 @@
                      (catch :default e
                      (catch :default e
                        (reset! *query-error e)
                        (reset! *query-error e)
                        (atom nil)))]
                        (atom nil)))]
-    (when *query-triggered?
-      (reset! *query-triggered? true))
     (if (instance? Atom query-atom)
     (if (instance? Atom query-atom)
       query-atom
       query-atom
       result-atom)))
       result-atom)))
 
 
-(defn get-group-by-page [{:keys [result-transform query] :as q}
+(defn get-group-by-page [{:keys [result-transform query] :as query-m}
                          {:keys [table?]}]
                          {:keys [table?]}]
   (if table?
   (if table?
     false ;; Immediately return false as table view can't handle grouping
     false ;; Immediately return false as table view can't handle grouping
-    (get q :group-by-page?
+    (get query-m :group-by-page?
          (and (not result-transform)
          (and (not result-transform)
               (not (and (string? query) (string/includes? query "(by-page false)")))))))
               (not (and (string? query) (string/includes? query "(by-page false)")))))))
 
 
 (defn get-query-result
 (defn get-query-result
-  [state config *query-error *query-triggered? current-block-uuid q options]
-  (or (when-let [*result (:query-result config)] @*result)
-      (let [query-atom (trigger-custom-query! state *query-error *query-triggered?)
-            query-result (and query-atom (rum/react query-atom))
-            ;; exclude the current one, otherwise it'll loop forever
-            remove-blocks (if current-block-uuid [current-block-uuid] nil)
-            transformed-query-result (when query-result
-                                       (let [result (db/custom-query-result-transform query-result remove-blocks q)]
-                                         (if (and query-result (coll? result) (:block/uuid (first result)))
-                                           (cond-> result
-                                            (get q :remove-block-children? true)
-                                            tree/filter-top-level-blocks)
-                                           result)))
-            group-by-page? (get-group-by-page q options)
-            result (if (and group-by-page? (:block/uuid (first transformed-query-result)))
-                     (let [result (db-utils/group-by-page transformed-query-result)]
-                       (if (map? result)
-                         (dissoc result nil)
-                         result))
-                     transformed-query-result)]
-        (when query-atom
-          (util/safe-with-meta result (meta @query-atom))))))
+  "Fetches a query's result, transforms it as needed and saves the result into
+  an atom that is passed in as an argument"
+  [config query-m *query-error current-block-uuid options]
+  (let [query-atom (trigger-custom-query! config query-m *query-error)
+        query-result (and query-atom (rum/react query-atom))
+        ;; exclude the current one, otherwise it'll loop forever
+        remove-blocks (if current-block-uuid [current-block-uuid] nil)
+        transformed-query-result (when query-result
+                                   (let [result (db/custom-query-result-transform query-result remove-blocks query-m)]
+                                     (if (and query-result (coll? result) (:block/uuid (first result)))
+                                       (cond-> result
+                                         (get query-m :remove-block-children? true)
+                                         tree/filter-top-level-blocks)
+                                       result)))
+        group-by-page? (get-group-by-page query-m options)
+        result (if (and group-by-page? (:block/uuid (first transformed-query-result)))
+                 (let [result (db-utils/group-by-page transformed-query-result)]
+                   (if (map? result)
+                     (dissoc result nil)
+                     result))
+                 transformed-query-result)]
+    (when-let [query-result (:query-result config)]
+      (reset! query-result result))
+    (when query-atom
+      (util/safe-with-meta result (meta @query-atom)))))

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

@@ -107,7 +107,7 @@
        [:div.pl-1.content.mt-3
        [:div.pl-1.content.mt-3
 
 
         [:div
         [:div
-         [:h2.text-lg.font-medium.my-4 (str (t :graph/local-graphs) ":")]
+         [:h2.text-lg.font-medium.my-4 (t :graph/local-graphs)]
          (when (seq local-graphs)
          (when (seq local-graphs)
            (repos-inner local-graphs))
            (repos-inner local-graphs))
 
 
@@ -123,7 +123,7 @@
           [:div
           [:div
            [:hr]
            [:hr]
            [:div.flex.align-items.justify-between
            [:div.flex.align-items.justify-between
-            [:h2.text-lg.font-medium.my-4 (str (t :graph/remote-graphs) ":")]
+            [:h2.text-lg.font-medium.my-4 (t :graph/remote-graphs)]
             [:div
             [:div
              (ui/button
              (ui/button
               [:span.flex.items-center "Refresh"
               [:span.flex.items-center "Refresh"

+ 23 - 17
src/main/frontend/components/right_sidebar.cljs

@@ -29,7 +29,7 @@
   (when-not (util/sm-breakpoint?)
   (when-not (util/sm-breakpoint?)
     (ui/with-shortcut :ui/toggle-right-sidebar "left"
     (ui/with-shortcut :ui/toggle-right-sidebar "left"
       [:button.button.icon.toggle-right-sidebar
       [:button.button.icon.toggle-right-sidebar
-       {:title "Toggle right sidebar"
+       {:title (t :right-side-bar/toggle-right-sidebar)
         :on-click ui-handler/toggle-right-sidebar!}
         :on-click ui-handler/toggle-right-sidebar!}
        (ui/icon "layout-sidebar-right" {:size 20})])))
        (ui/icon "layout-sidebar-right" {:size 20})])))
 
 
@@ -56,7 +56,7 @@
 (rum/defc shortcut-settings
 (rum/defc shortcut-settings
   []
   []
   [:div.contents.flex-col.flex.ml-3
   [:div.contents.flex-col.flex.ml-3
-   (shortcut/shortcut {:show-title? false})])
+   (shortcut/shortcut-page {:show-title? false})])
 
 
 (defn- block-with-breadcrumb
 (defn- block-with-breadcrumb
   [repo block idx sidebar-key ref?]
   [repo block idx sidebar-key ref?]
@@ -73,6 +73,9 @@
   (when v [:.ml-4 (ui/foldable
   (when v [:.ml-4 (ui/foldable
                    [:div (str k)]
                    [:div (str k)]
                    [:.ml-4 (case k
                    [:.ml-4 (case k
+                             :tx-id
+                             [:.my-1 [:pre.code.pre-wrap-white-space.bg-base-4 (str v)]]
+
                              :blocks
                              :blocks
                              (map (fn [block]
                              (map (fn [block]
                                     [:.my-1 [:pre.code.pre-wrap-white-space.bg-base-4 (str block)]]) v)
                                     [:.my-1 [:pre.code.pre-wrap-white-space.bg-base-4 (str block)]]) v)
@@ -111,7 +114,7 @@
 
 
 (defn build-sidebar-item
 (defn build-sidebar-item
   [repo idx db-id block-type]
   [repo idx db-id block-type]
-  (case block-type
+  (case (keyword block-type)
     :contents
     :contents
     [(t :right-side-bar/contents)
     [(t :right-side-bar/contents)
      (contents)]
      (contents)]
@@ -120,11 +123,11 @@
     [(t :right-side-bar/help) (onboarding/help)]
     [(t :right-side-bar/help) (onboarding/help)]
 
 
     :page-graph
     :page-graph
-    [(str (t :right-side-bar/page-graph))
+    [(t :right-side-bar/page-graph)
      (page/page-graph)]
      (page/page-graph)]
 
 
     :history
     :history
-    [(str (t :right-side-bar/history))
+    [(t :right-side-bar/history)
      (history)]
      (history)]
 
 
     :block-ref
     :block-ref
@@ -220,6 +223,7 @@
 (rum/defc sidebar-resizer
 (rum/defc sidebar-resizer
   [sidebar-open? sidebar-id handler-position]
   [sidebar-open? sidebar-id handler-position]
   (let [el-ref (rum/use-ref nil)
   (let [el-ref (rum/use-ref nil)
+        min-px-width 144 ; Custom window controls width
         min-ratio 0.1
         min-ratio 0.1
         max-ratio 0.7
         max-ratio 0.7
         keyboard-step 5
         keyboard-step 5
@@ -227,12 +231,12 @@
         remove-resizing-class (fn []
         remove-resizing-class (fn []
                                 (.. js/document.documentElement -classList (remove "is-resizing-buf"))
                                 (.. js/document.documentElement -classList (remove "is-resizing-buf"))
                                 (reset! ui-handler/*right-sidebar-resized-at (js/Date.now)))
                                 (reset! ui-handler/*right-sidebar-resized-at (js/Date.now)))
-        set-width! (fn [ratio element]
-                     (when (and el-ref element)
-                       (let [width (str (* ratio 100) "%")]
-                         (#(.setProperty (.-style element) "width" width)
-                          (.setAttribute (rum/deref el-ref) "aria-valuenow" ratio)
-                          (ui-handler/persist-right-sidebar-width!)))))]
+        set-width! (fn [ratio]
+                     (when el-ref
+                       (let [value (* ratio 100)
+                             width (str value "%")]
+                         (.setAttribute (rum/deref el-ref) "aria-valuenow" value)
+                         (ui-handler/persist-right-sidebar-width! width))))]
     (rum/use-effect!
     (rum/use-effect!
      (fn []
      (fn []
        (when-let [el (and (fn? js/window.interact) (rum/deref el-ref))]
        (when-let [el (and (fn? js/window.interact) (rum/deref el-ref))]
@@ -243,6 +247,7 @@
                 {:move
                 {:move
                  (fn [^js/MouseEvent e]
                  (fn [^js/MouseEvent e]
                    (let [width js/document.documentElement.clientWidth
                    (let [width js/document.documentElement.clientWidth
+                         min-ratio (max min-ratio (/ min-px-width width))
                          sidebar-el (js/document.getElementById sidebar-id)
                          sidebar-el (js/document.getElementById sidebar-id)
                          offset (.-pageX e)
                          offset (.-pageX e)
                          ratio (.toFixed (/ offset width) 6)
                          ratio (.toFixed (/ offset width) 6)
@@ -259,7 +264,7 @@
                          (and (< ratio max-ratio) sidebar-el)
                          (and (< ratio max-ratio) sidebar-el)
                          (when sidebar-el
                          (when sidebar-el
                            (#(.. js/document.documentElement -classList (remove cursor-class))
                            (#(.. js/document.documentElement -classList (remove cursor-class))
-                            (set-width! ratio sidebar-el)))
+                            (set-width! ratio)))
                          :else
                          :else
                          #(.. js/document.documentElement -classList (remove cursor-class)))
                          #(.. js/document.documentElement -classList (remove cursor-class)))
                        (when (> ratio (/ min-ratio 2)) (state/open-right-sidebar!)))))}}))
                        (when (> ratio (/ min-ratio 2)) (state/open-right-sidebar!)))))}}))
@@ -269,6 +274,7 @@
              (.on "keydown" (fn [e]
              (.on "keydown" (fn [e]
                               (when-let [sidebar-el (js/document.getElementById sidebar-id)]
                               (when-let [sidebar-el (js/document.getElementById sidebar-id)]
                                 (let [width js/document.documentElement.clientWidth
                                 (let [width js/document.documentElement.clientWidth
+                                      min-ratio (max min-ratio (/ min-px-width width))
                                       keyboard-step (case (.-code e)
                                       keyboard-step (case (.-code e)
                                                       "ArrowLeft" (- keyboard-step)
                                                       "ArrowLeft" (- keyboard-step)
                                                       "ArrowRight" keyboard-step
                                                       "ArrowRight" keyboard-step
@@ -278,7 +284,7 @@
                                       ratio (if (= handler-position :west) (- 1 ratio) ratio)]
                                       ratio (if (= handler-position :west) (- 1 ratio) ratio)]
                                   (when (and (> ratio min-ratio) (< ratio max-ratio) (not (zero? keyboard-step)))
                                   (when (and (> ratio min-ratio) (< ratio max-ratio) (not (zero? keyboard-step)))
                                     ((add-resizing-class)
                                     ((add-resizing-class)
-                                     (set-width! ratio sidebar-el)))))))
+                                     (set-width! ratio)))))))
              (.on "keyup" remove-resizing-class)))
              (.on "keyup" remove-resizing-class)))
        #())
        #())
      [])
      [])
@@ -333,9 +339,7 @@
         (when config/dev? [:div.text-sm
         (when config/dev? [:div.text-sm
                            [:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
                            [:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
                                                                                        (state/sidebar-add-block! repo "history" :history))}
                                                                                        (state/sidebar-add-block! repo "history" :history))}
-                            (t :right-side-bar/history)]])]
-
-       (toggle)]
+                            (t :right-side-bar/history)]])]]
 
 
       [:.sidebar-item-list.flex-1.scrollbar-spacing.flex.flex-col.gap-2
       [:.sidebar-item-list.flex-1.scrollbar-spacing.flex.flex-col.gap-2
        (if @*anim-finished?
        (if @*anim-finished?
@@ -353,9 +357,11 @@
                  [[(state/get-current-repo) "contents" :contents nil]]
                  [[(state/get-current-repo) "contents" :contents nil]]
                  blocks)
                  blocks)
         sidebar-open? (state/sub :ui/sidebar-open?)
         sidebar-open? (state/sub :ui/sidebar-open?)
+        width (state/sub :ui/sidebar-width)
         repo (state/sub :git/current-repo)]
         repo (state/sub :git/current-repo)]
     [:div#right-sidebar.cp__right-sidebar.h-screen
     [:div#right-sidebar.cp__right-sidebar.h-screen
-     {:class (if sidebar-open? "open" "closed")}
+     {:class (if sidebar-open? "open" "closed")
+      :style {:width width}}
      (sidebar-resizer sidebar-open? "right-sidebar" :west)
      (sidebar-resizer sidebar-open? "right-sidebar" :west)
      (when sidebar-open?
      (when sidebar-open?
        (sidebar-inner repo t blocks))]))
        (sidebar-inner repo t blocks))]))

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

@@ -257,8 +257,8 @@
    {:name icon
    {:name icon
     :class "highlight"
     :class "highlight"
     :extension? true}
     :extension? true}
-   [:div.text.font-bold (str label ": ")
-    [:span.ml-1 name]]))
+   [:div.text.font-bold label
+    [:span.ml-2 name]]))
 
 
 (defn- search-item-render
 (defn- search-item-render
   [search-q {:keys [type data alias]}]
   [search-q {:keys [type data alias]}]
@@ -312,7 +312,7 @@
 
 
                                 :else
                                 :else
                                 (do (log/error "search result with non-existing uuid: " data)
                                 (do (log/error "search result with non-existing uuid: " data)
-                                    (str "Cache is outdated. Please click the 'Re-index' button in the graph's dropdown menu."))))])
+                                    (t :search/cache-outdated))))])
 
 
        :page-content
        :page-content
        (let [{:block/keys [snippet uuid]} data  ;; content here is normalized
        (let [{:block/keys [snippet uuid]} data  ;; content here is normalized
@@ -327,7 +327,7 @@
                                 (if page
                                 (if page
                                   (page-content-search-result-item repo uuid format snippet search-q search-mode)
                                   (page-content-search-result-item repo uuid format snippet search-q search-mode)
                                   (do (log/error "search result with non-existing uuid: " data)
                                   (do (log/error "search result with non-existing uuid: " data)
-                                      (str "Cache is outdated. Please click the 'Re-index' button in the graph's dropdown menu."))))]))
+                                      (t :search/cache-outdated))))]))
 
 
        nil)]))
        nil)]))
 
 
@@ -396,11 +396,11 @@
   [in-page-search?]
   [in-page-search?]
   [:div.recent-search
   [:div.recent-search
    [:div.wrap.px-4.pb-2.text-sm.opacity-70.flex.flex-row.justify-between.align-items.mx-1.sm:mx-0
    [:div.wrap.px-4.pb-2.text-sm.opacity-70.flex.flex-row.justify-between.align-items.mx-1.sm:mx-0
-    [:div "Recent search:"]
+    [:div (t :search/recent)]
     [:div.hidden.md:flex
     [:div.hidden.md:flex
      (ui/with-shortcut :go/search-in-page "bottom"
      (ui/with-shortcut :go/search-in-page "bottom"
        [:div.flex-row.flex.align-items
        [:div.flex-row.flex.align-items
-        [:div.mr-3.flex "Search blocks in page:"]
+        [:div.mr-3.flex (t :search/blocks-in-page)]
         [:div.flex.items-center
         [:div.flex.items-center
          (ui/toggle in-page-search?
          (ui/toggle in-page-search?
                     (fn [_value]
                     (fn [_value]
@@ -408,7 +408,7 @@
                     true)]
                     true)]
         (ui/tippy {:html [:div
         (ui/tippy {:html [:div
                           ;; TODO: fetch from config
                           ;; TODO: fetch from config
-                          "Tip: " [:code (util/->platform-shortcut "Ctrl + Shift + p")] " to open the commands palette"]
+                          (t :search/command-palette-tip-1) [:code (util/->platform-shortcut "Ctrl + Shift + p")] (t :search/command-palette-tip-2)]
                    :interactive     true
                    :interactive     true
                    :arrow           true
                    :arrow           true
                    :theme       "monospace"}
                    :theme       "monospace"}

+ 269 - 50
src/main/frontend/components/settings.cljs

@@ -1,37 +1,39 @@
 (ns frontend.components.settings
 (ns frontend.components.settings
   (:require [clojure.string :as string]
   (:require [clojure.string :as string]
-            [frontend.components.svg :as svg]
-            [frontend.components.plugins :as plugins]
+            [electron.ipc :as ipc]
             [frontend.components.assets :as assets]
             [frontend.components.assets :as assets]
+            [frontend.components.conversion :as conversion-component]
+            [frontend.components.file-sync :as fs]
+            [frontend.components.plugins :as plugins]
+            [frontend.components.svg :as svg]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.context.i18n :refer [t]]
-            [frontend.storage :as storage]
-            [frontend.spec.storage :as storage-spec]
             [frontend.date :as date]
             [frontend.date :as date]
+            [frontend.db :as db]
             [frontend.dicts :as dicts]
             [frontend.dicts :as dicts]
             [frontend.handler :as handler]
             [frontend.handler :as handler]
             [frontend.handler.config :as config-handler]
             [frontend.handler.config :as config-handler]
+            [frontend.handler.file-sync :as file-sync-handler]
+            [frontend.handler.global-config :as global-config-handler]
             [frontend.handler.notification :as notification]
             [frontend.handler.notification :as notification]
+            [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.user :as user-handler]
             [frontend.handler.user :as user-handler]
-            [frontend.handler.plugin :as plugin-handler]
-            [frontend.handler.file-sync :as file-sync-handler]
-            [frontend.handler.global-config :as global-config-handler]
+            [frontend.mobile.util :as mobile-util]
             [frontend.modules.instrumentation.core :as instrument]
             [frontend.modules.instrumentation.core :as instrument]
             [frontend.modules.shortcut.data-helper :as shortcut-helper]
             [frontend.modules.shortcut.data-helper :as shortcut-helper]
+            [frontend.spec.storage :as storage-spec]
             [frontend.state :as state]
             [frontend.state :as state]
+            [frontend.storage :as storage]
             [frontend.ui :as ui]
             [frontend.ui :as ui]
-            [electron.ipc :as ipc]
-            [promesa.core :as p]
             [frontend.util :refer [classnames web-platform?] :as util]
             [frontend.util :refer [classnames web-platform?] :as util]
             [frontend.version :refer [version]]
             [frontend.version :refer [version]]
             [goog.object :as gobj]
             [goog.object :as gobj]
+            [goog.string :as gstring]
+            [promesa.core :as p]
             [reitit.frontend.easy :as rfe]
             [reitit.frontend.easy :as rfe]
-            [rum.core :as rum]
-            [frontend.mobile.util :as mobile-util]
-            [frontend.db :as db]
-            [frontend.components.conversion :as conversion-component]))
+            [rum.core :as rum]))
 
 
 (defn toggle
 (defn toggle
   [label-for name state on-toggle & [detail-text]]
   [label-for name state on-toggle & [detail-text]]
@@ -57,19 +59,19 @@
        [:div (cond
        [:div (cond
                (mobile-util/native-android?)
                (mobile-util/native-android?)
                (ui/button
                (ui/button
-                "Check for updates"
+                (t :settings-page/check-for-updates)
                 :class "text-sm p-1 mr-1"
                 :class "text-sm p-1 mr-1"
-                :href "https://github.com/logseq/logseq/releases" )
+                :href "https://github.com/logseq/logseq/releases")
 
 
                (mobile-util/native-ios?)
                (mobile-util/native-ios?)
                (ui/button
                (ui/button
-                "Check for updates"
+                (t :settings-page/check-for-updates)
                 :class "text-sm p-1 mr-1"
                 :class "text-sm p-1 mr-1"
-                :href "https://apps.apple.com/app/logseq/id1601013908" )
+                :href "https://apps.apple.com/app/logseq/id1601013908")
 
 
                (util/electron?)
                (util/electron?)
                (ui/button
                (ui/button
-                (if update-pending? "Checking ..." "Check for updates")
+                (if update-pending? (t :settings-page/checking) (t :settings-page/check-for-updates))
                 :class "text-sm p-1 mr-1"
                 :class "text-sm p-1 mr-1"
                 :disabled update-pending?
                 :disabled update-pending?
                 :on-click #(js/window.apis.checkForUpdates false))
                 :on-click #(js/window.apis.checkForUpdates false))
@@ -78,7 +80,7 @@
                nil)]
                nil)]
 
 
        [:div.text-sm.cursor
        [:div.text-sm.cursor
-        {:title (str "Revision: " config/revision)
+        {:title (str (t :settings-page/revision) config/revision)
          :on-click (fn []
          :on-click (fn []
                      (notification/show! [:div "Current Revision: "
                      (notification/show! [:div "Current Revision: "
                                           [:a {:target "_blank"
                                           [:a {:target "_blank"
@@ -91,18 +93,18 @@
        [:a.text-sm.fade-link.underline.inline
        [:a.text-sm.fade-link.underline.inline
         {:target "_blank"
         {:target "_blank"
          :href "https://docs.logseq.com/#/page/changelog"}
          :href "https://docs.logseq.com/#/page/changelog"}
-        "What's new?"]]]
+        (t :settings-page/changelog)]]]
 
 
      (when-not (or update-pending?
      (when-not (or update-pending?
                    (string/blank? type))
                    (string/blank? type))
        [:div.update-state.text-sm
        [:div.update-state.text-sm
         (case type
         (case type
           "update-not-available"
           "update-not-available"
-          [:p "Your app is up-to-date 🎉"]
+          [:p (t :settings-page/app-updated)]
 
 
           "update-available"
           "update-available"
           (let [{:keys [name url]} payload]
           (let [{:keys [name url]} payload]
-            [:p (str "Found new release ")
+            [:p (str (t :settings-page/update-available))
              [:a.link
              [:a.link
               {:on-click
               {:on-click
                (fn [e]
                (fn [e]
@@ -111,7 +113,7 @@
               svg/external-link name " 🎉"]])
               svg/external-link name " 🎉"]])
 
 
           "error"
           "error"
-          [:p "⚠️ Oops, Something Went Wrong!" [:br] " Please check out the "
+          [:p (t :settings-page/update-error-1) [:br] (t :settings-page/update-error-2)
            [:a.link
            [:a.link
             {:on-click
             {:on-click
              (fn [e]
              (fn [e]
@@ -125,10 +127,10 @@
    {:style {:box-shadow "0 4px 20px 4px rgba(0, 20, 60, .1), 0 4px 80px -8px rgba(0, 20, 60, .2)"}}
    {:style {:box-shadow "0 4px 20px 4px rgba(0, 20, 60, .1), 0 4px 80px -8px rgba(0, 20, 60, .2)"}}
    [:div {:style {:margin "12px" :max-width "500px"}}
    [:div {:style {:margin "12px" :max-width "500px"}}
     [:p.text-sm
     [:p.text-sm
-     "The left side shows outdenting with the default setting, and the right shows outdenting with logical outdenting enabled. "
+     (t :settings-page/preferred-outdenting-tip)
      [:a.text-sm
      [:a.text-sm
       {:target "_blank" :href "https://discuss.logseq.com/t/whats-your-preferred-outdent-behavior-the-direct-one-or-the-logical-one/978"}
       {:target "_blank" :href "https://discuss.logseq.com/t/whats-your-preferred-outdent-behavior-the-direct-one-or-the-logical-one/978"}
-      "→ Learn more"]]
+      (t :settings-page/preferred-outdenting-tip-more)]]
     [:img {:src    "https://discuss.logseq.com/uploads/default/original/1X/e8ea82f63a5e01f6d21b5da827927f538f3277b9.gif"
     [:img {:src    "https://discuss.logseq.com/uploads/default/original/1X/e8ea82f63a5e01f6d21b5da827927f538f3277b9.gif"
            :width  500
            :width  500
            :height 500}]]])
            :height 500}]]])
@@ -139,7 +141,7 @@
    {:style {:box-shadow "0 4px 20px 4px rgba(0, 20, 60, .1), 0 4px 80px -8px rgba(0, 20, 60, .2)"}}
    {:style {:box-shadow "0 4px 20px 4px rgba(0, 20, 60, .1), 0 4px 80px -8px rgba(0, 20, 60, .2)"}}
    [:div {:style {:margin "12px" :max-width "500px"}}
    [:div {:style {:margin "12px" :max-width "500px"}}
     [:p.text-sm
     [:p.text-sm
-     "This option controls whether to expand the block references automatically when zoom-in."]
+     (t :settings-page/auto-expand-block-refs-tip)]
     [:img {:src    "https://user-images.githubusercontent.com/28241963/225818326-118deda9-9d1e-477d-b0ce-771ca0bcd976.gif"
     [:img {:src    "https://user-images.githubusercontent.com/28241963/225818326-118deda9-9d1e-477d-b0ce-771ca0bcd976.gif"
            :width  500
            :width  500
            :height 500}]]])
            :height 500}]]])
@@ -293,12 +295,12 @@
 
 
 (defn theme-modes-row [t switch-theme system-theme? dark?]
 (defn theme-modes-row [t switch-theme system-theme? dark?]
   (let [pick-theme [:ul.theme-modes-options
   (let [pick-theme [:ul.theme-modes-options
-                    [:li {:on-click (partial state/use-theme-mode! "light")
-                          :class    (classnames [{:active (and (not system-theme?) (not dark?))}])} [:i.mode-light] [:strong "light"]]
-                    [:li {:on-click (partial state/use-theme-mode! "dark")
-                          :class    (classnames [{:active (and (not system-theme?) dark?)}])} [:i.mode-dark] [:strong "dark"]]
-                    [:li {:on-click (partial state/use-theme-mode! "system")
-                          :class    (classnames [{:active system-theme?}])} [:i.mode-system] [:strong "system"]]]]
+                    [:li {:on-click (partial state/use-theme-mode! (t :settings-page/theme-light))
+                          :class    (classnames [{:active (and (not system-theme?) (not dark?))}])} [:i.mode-light] [:strong (t :settings-page/theme-light)]]
+                    [:li {:on-click (partial state/use-theme-mode! (t :settings-page/theme-dark))
+                          :class    (classnames [{:active (and (not system-theme?) dark?)}])} [:i.mode-dark] [:strong (t :settings-page/theme-dark)]]
+                    [:li {:on-click (partial state/use-theme-mode! (t :settings-page/theme-system))
+                          :class    (classnames [{:active system-theme?}])} [:i.mode-system] [:strong (t :settings-page/theme-system)]]]]
     (row-with-button-action {:left-label (t :right-side-bar/switch-theme (string/capitalize switch-theme))
     (row-with-button-action {:left-label (t :right-side-bar/switch-theme (string/capitalize switch-theme))
                              :-for       "toggle_theme"
                              :-for       "toggle_theme"
                              :action     pick-theme
                              :action     pick-theme
@@ -340,7 +342,7 @@
                       (when-not (string/blank? format)
                       (when-not (string/blank? format)
                         (config-handler/set-config! :journal/page-title-format format)
                         (config-handler/set-config! :journal/page-title-format format)
                         (notification/show!
                         (notification/show!
-                          [:div "You must re-index your graph for this change to take effect"]
+                          [:div (t :settings-page/custom-date-format-notification)]
                           :warning false)
                           :warning false)
                         (state/close-modal!)
                         (state/close-modal!)
                         (route-handler/redirect! {:to :repos}))))}
                         (route-handler/redirect! {:to :repos}))))}
@@ -385,8 +387,13 @@
 
 
 (defn preferred-pasting-file [t preferred-pasting-file?]
 (defn preferred-pasting-file [t preferred-pasting-file?]
   (toggle "preferred_pasting_file"
   (toggle "preferred_pasting_file"
-          (t :settings-page/preferred-pasting-file)
-          preferred-pasting-file?
+          [(t :settings-page/preferred-pasting-file)
+           (ui/tippy {:html        (t :settings-page/preferred-pasting-file-hint)
+                      :class       "tippy-hover ml-2"
+                      :interactive true
+                      :disabled    false}
+                     (svg/info))]
+          preferred-pasting-file? 
           config-handler/toggle-preferred-pasting-file!))
           config-handler/toggle-preferred-pasting-file!))
 
 
 (defn auto-expand-row [t auto-expand-block-refs?]
 (defn auto-expand-row [t auto-expand-block-refs?]
@@ -610,6 +617,19 @@
                 (fn [_] (conversion-component/files-breaking-changed))
                 (fn [_] (conversion-component/files-breaking-changed))
                 {:id :filename-format-panel :center? true})}))
                 {:id :filename-format-panel :center? true})}))
 
 
+(rum/defcs native-titlebar-row < rum/reactive
+  [state t]
+  (let [enabled? (state/sub [:electron/user-cfgs :window/native-titlebar?])]
+    (toggle
+     "native-titlebar"
+     (t :settings-page/native-titlebar)
+     enabled?
+     #(when (js/confirm (t :relaunch-confirm-to-work))
+        (state/set-state! [:electron/user-cfgs :window/native-titlebar?] (not enabled?))
+        (ipc/ipc :userAppCfgs :window/native-titlebar? (not enabled?))
+        (js/logseq.api.relaunch))
+     [:span.text-sm.opacity-50 (t :settings-page/native-titlebar-desc)])))
+
 (rum/defcs settings-general < rum/reactive
 (rum/defcs settings-general < rum/reactive
   [_state current-repo]
   [_state current-repo]
   (let [preferred-language (state/sub [:preferred-language])
   (let [preferred-language (state/sub [:preferred-language])
@@ -621,6 +641,7 @@
      (version-row t version)
      (version-row t version)
      (language-row t preferred-language)
      (language-row t preferred-language)
      (theme-modes-row t switch-theme system-theme? dark?)
      (theme-modes-row t switch-theme system-theme? dark?)
+     (when (and (util/electron?) (not util/mac?)) (native-titlebar-row t))
      (when (config/global-config-enabled?) (edit-global-config-edn))
      (when (config/global-config-enabled?) (edit-global-config-edn))
      (when current-repo (edit-config-edn))
      (when current-repo (edit-config-edn))
      (when current-repo (edit-custom-css))
      (when current-repo (edit-custom-css))
@@ -669,18 +690,16 @@
    [:div.text-sm.my-4
    [:div.text-sm.my-4
     (ui/admonition
     (ui/admonition
      :tip
      :tip
-     [:p "If you have Logseq Sync enabled, you can view a page's edit history directly. This section is for tech-savvy only."])
+     [:p (t :settings-page/git-tip)])
     [:span.text-sm.opacity-50.my-4 
     [:span.text-sm.opacity-50.my-4 
-     "To view page's edit history, click the three horizontal dots in the top-right corner and select \"View page history\"."]
+     (t :settings-page/git-desc-1)]
     [:br][:br]
     [:br][:br]
     [:span.text-sm.opacity-50.my-4
     [:span.text-sm.opacity-50.my-4
-     "For professional users, Logseq also supports using "]
+     (t :settings-page/git-desc-2)]
     [:a {:href "https://git-scm.com/" :target "_blank"}
     [:a {:href "https://git-scm.com/" :target "_blank"}
      "Git"]
      "Git"]
     [:span.text-sm.opacity-50.my-4
     [:span.text-sm.opacity-50.my-4
-     " for version control."]
-    [:span.text-sm.opacity-50.my-4
-     "Use Git at your own risk as general Git issues are not supported by the Logseq team"]]
+     (t :settings-page/git-desc-3)]]
    [:br]
    [:br]
    (switch-git-auto-commit-row t)
    (switch-git-auto-commit-row t)
    (git-auto-commit-seconds t)
    (git-auto-commit-seconds t)
@@ -731,6 +750,196 @@
    {:left-label (t :settings-page/enable-whiteboards)
    {:left-label (t :settings-page/enable-whiteboards)
     :action (whiteboards-enabled-switcher enabled?)}))
     :action (whiteboards-enabled-switcher enabled?)}))
 
 
+(rum/defc settings-account-usage-description [pro-account? graph-usage]
+  (let [count-usage (count graph-usage)
+        count-limit (if pro-account? 10 1)
+        count-percent (js/Math.round (/ count-usage count-limit 0.01))
+        storage-usage (->> (map :used-gbs graph-usage)
+                           (reduce + 0)) 
+        storage-usage-formatted (cond 
+                                  (zero? storage-usage) "0.0"
+                                  (< storage-usage 0.01) "Less than 0.01"
+                                  :else (gstring/format "%.2f" storage-usage))
+        ;; TODO: check logic on this. What are the rules around storage limits?  
+        ;; do we, and should we be able to, give individual users more storage?
+        ;; should that be on a per graph or per user basis?
+        default-storage-limit (if pro-account? 10 0.05)
+        storage-limit (->> (range 0 count-limit)
+                           (map #(get-in graph-usage [% :limit-gbs] default-storage-limit))
+                           (reduce + 0))
+        storage-percent (/ storage-usage storage-limit 0.01)
+        storage-percent-formatted (gstring/format "%.1f" storage-percent)]
+    [:div.text-sm
+     (when pro-account?
+       [:<>
+        (gstring/format "%s of %s synced graphs " count-usage count-limit)
+        [:strong.text-white (gstring/format "(%s%%)" count-percent)]
+        ", "]) 
+     (gstring/format "%sGB of %sGB total storage " storage-usage-formatted storage-limit)
+     [:strong.text-white (gstring/format "(%s%%)" storage-percent-formatted)]]))
+     ; storage-usage-formatted "GB of " storage-limit "GB total storage"
+     ; [:strong.text-white " (" storage-percent-formatted "%)"]]))
+
+
+(rum/defc settings-account-usage-graphs [_pro-account? graph-usage]
+  (when (< 0 (count graph-usage))
+   [:div.grid.gap-3 {:style {:grid-template-columns (str "repeat(" (count graph-usage) ", 1fr)")}}
+    (for [{:keys [name used-percent]} graph-usage
+          :let [color (if (<= 100 used-percent) "bg-red-500" "bg-blue-500")]]
+     [:div.rounded-full.w-full.h-2 {:class "bg-black/50" 
+                                    :tooltip name}
+      [:div.rounded-full.h-2 {:class color
+                              :style {:width (str used-percent "%") 
+                                      :min-width "0.5rem" 
+                                      :max-width "100%"}}]])]))
+  
+(rum/defc ^:large-vars/cleanup-todo settings-account < rum/reactive
+  []
+  (let [current-graph-uuid (state/sub-current-file-sync-graph-uuid)
+        graph-usage (state/get-remote-graph-usage)
+        current-graph-is-remote? ((set (map :uuid graph-usage)) current-graph-uuid)
+        logged-in? (user-handler/logged-in?)
+        user-info (state/get-user-info)
+        paid-user? (#{"active" "on_trial" "cancelled"} (:LemonStatus user-info))
+        gift-user? (some #{"pro"} (:UserGroups user-info))
+        pro-account? (or paid-user? gift-user?)
+        expiration-date (some-> user-info :LemonEndsAt date/parse-iso)
+        renewal-date (some-> user-info :LemonRenewsAt date/parse-iso)
+        has-subscribed? (some? (:LemonStatus user-info))]
+    [:div.panel-wrap.is-features.mb-8
+     [:div.mt-1.sm:mt-0.sm:col-span-2
+      (cond
+        logged-in?
+        [:div.grid.grid-cols-3.gap-8.pt-2
+         [:div "Current plan"]
+         [:div.col-span-2 
+          [:div {:class "w-full bg-gray-500/10 rounded-lg p-4 flex flex-col gap-4"}
+           [:div.flex.gap-4.items-center
+            (if pro-account?
+              [:div.flex-1 "Pro"]
+              [:div.flex-1 "Free"])
+            (cond 
+              has-subscribed?
+              (ui/button "Manage plan" {:class "p-1 h-8 justify-center"
+                                        :disabled true
+                                        :icon "upload"})
+                                         ; :on-click user-handler/upgrade})
+              (not pro-account?)
+              (ui/button "Upgrade plan" {:class "p-1 h-8 justify-center"
+                                         :icon "upload"
+                                         :on-click user-handler/upgrade})
+              :else nil)]
+           (settings-account-usage-graphs pro-account? graph-usage)
+           (settings-account-usage-description pro-account? graph-usage)
+           (if current-graph-is-remote?
+             (ui/button "Deactivate syncing" {:class "p-1 h-8 justify-center"
+                                              :disabled true
+                                              :background "gray"
+                                              :icon "cloud-off"})
+             (ui/button "Activate syncing" {:class "p-1 h-8 justify-center"
+                                            :background "blue"
+                                            :icon "cloud"
+                                            :on-click #(fs/maybe-onboarding-show :sync-initiate)}))]]
+         (when has-subscribed?
+          [:<>
+           [:div "Billing"]
+           [:div.col-span-2.flex.flex-col.gap-4
+            (cond 
+              ;; If there is no expiration date, print the renewal date
+              (and renewal-date (nil? expiration-date)) 
+              [:div 
+               [:strong.font-semibold "Next billing date: " 
+                (date/get-locale-string renewal-date)]]
+              ;; If the expiration date is in the future, word it as such
+              (< (js/Date.) expiration-date) 
+              [:div
+               [:strong.font-semibold "Pro plan expires on: " 
+                (date/get-locale-string expiration-date)]]
+              ;; Otherwise, ind
+              :else 
+              [:div 
+               [:strong.font-semibold "Pro plan expired on: " 
+                (date/get-locale-string expiration-date)]])
+                               
+            [:div (ui/button "Open invoices" {:class "w-full h-8 p-1 justify-center" 
+                                              :disabled true 
+                                              :background "gray" 
+                                              :icon "receipt"})]]])
+         [:div "Profile"]
+         [:div.col-span-2.grid.grid-cols-2.gap-4
+          [:div.flex.flex-col.gap-2.box-border {:class "basis-1/2"}
+           [:label.text-sm.font-semibold "First name"]
+           [:input.rounded.border.px-2.py-1.box-border {:class "border-blue-500 bg-black/25 w-full"}]]
+          [:div.flex.flex-col.gap-2 {:class "basis-1/2"}
+           [:label.text-sm.font-semibold "Last name"]
+           [:input.rounded.border.px-2.py-1.box-border {:class "border-blue-500 bg-black/25 w-full"}]]
+          [:div.flex-1.flex.flex-col.gap-2.col-span-2
+           [:label.text-sm.font-semibold "Username"]
+           [:input.rounded.border.px-2.py-1.box-border {:class "border-blue-500 bg-black/25" 
+                                                        :value (user-handler/email)}]]]
+         [:div "Authentication"]
+         [:div.col-span-2
+          [:div.grid.grid-cols-2.gap-4
+           [:div (ui/button (t :logout) {:class "p-1 h-8 justify-center w-full"
+                                         :background "gray"
+                                         :icon "logout"
+                                         :on-click user-handler/logout})]
+           [:div (ui/button "Reset password" {:class "p-1 h-8 justify-center w-full"
+                                              :disabled true
+                                              :background "gray"
+                                              :icon "key"
+                                              :on-click user-handler/logout})]
+           [:div.col-span-2 (ui/button "Delete Account" {:class "p-1 h-8 justify-center w-full" 
+                                                         :disabled true
+                                                         :background "red"})]]]] 
+                                            
+        (not logged-in?)
+        [:div.grid.grid-cols-3.gap-8.pt-2
+         [:div "Authentication"]
+         [:div.col-span-2.flex.flex-wrap.gap-4
+          [:div.w-full.text-white "With a Logseq account, you can access cloud-based services like Logseq Sync and alpha/beta features."]
+          [:div.flex-1 (ui/button "Sign up" {:class "h-8 w-full text-center justify-center"
+                                             :on-click (fn []
+                                                         (state/close-settings!)
+                                                         (state/pub-event! [:user/login]))})]
+          [:div.flex-1 (ui/button (t :login) {:icon "login" 
+                                              :class "h-8 w-full text-center justify-center" 
+                                              :background "gray"
+                                              :on-click (fn []
+                                                          (state/close-settings!)
+                                                          (state/pub-event! [:user/login]))})]]
+         [:div.col-span-3.flex.flex-col.gap-4 {:class "bg-black/20 p-4 rounded-lg"}
+          [:div.flex.w-full.items-center
+           [:div {:class "w-1/2 text-lg"} 
+            "Discover the power of " 
+            [:strong {:class "text-white/80"} "Logseq Sync"]]
+           [:div {:class "w-1/2 bg-gradient-to-r from-white/10 to-transparent p-3 rounded-lg flex items-center gap-2 px-5 ml-5"} 
+            [:div.w-3.h-3.rounded-full.bg-green-500]
+            "Synced"]]
+          [:div.flex.w-full.gap-4
+           [:div {:class "w-1/2 bg-black/50 rounded-lg p-4 pt-10 relative flex flex-col gap-4"}
+            [:div.absolute.top-0.left-4.bg-gray-700.uppercase.px-2.py-1.rounded-b-lg.font-bold.text-xs "Free"]
+            [:div
+             [:strong.text-white.text-xl.font-normal "$0"]] 
+            [:div.text-white.font-bold {:class "h-[2.5rem] "} "Get started with basic syncing"]
+            [:ul.text-xs.list-none.m-0.flex.flex-col.gap-0.5
+             [:li "Unlimited unsynced graphs"]
+             [:li "1 synced graph (up to 50MB, notes only)"]
+             [:li "No asset syncing"]
+             [:li "Access to core Logseq features"]]]
+           [:div {:class "w-1/2 bg-black/50 rounded-lg p-4 pt-10 relative flex flex-col gap-4"}
+            [:div.absolute.top-0.left-4.bg-blue-700.uppercase.px-2.py-1.rounded-b-lg.font-bold.text-xs "Pro"]
+            [:div
+             [:strong.text-white.text-xl.font-normal "$10"] 
+             [:span.text-xs.font-base {:class "ml-0.5"} "/ month"]]
+            [:div.text-white.font-bold {:class "h-[2.5rem]"} "Unlock advanced syncing and more"]
+            [:ul.text-xs.list-none.m-0.flex.flex-col.gap-0.5
+             [:li "Unlimited unsynced graphs"]
+             [:li "10 synced graphs (up to 5GB each)"]
+             [:li "Sync assets up to 100MB per file"]
+             [:li "Early access to alpha/beta features"]
+             [:li "Upcoming cloud-based features, including Logseq Publish"]]]]]])]]))
+
 (rum/defc settings-features < rum/reactive
 (rum/defc settings-features < rum/reactive
   []
   []
   (let [current-repo (state/get-current-repo)
   (let [current-repo (state/get-current-repo)
@@ -786,11 +995,11 @@
          {:class (when-not user-handler/alpha-or-beta-user? "opacity-50 pointer-events-none cursor-not-allowed")}
          {:class (when-not user-handler/alpha-or-beta-user? "opacity-50 pointer-events-none cursor-not-allowed")}
          (sync-switcher-row enable-sync?)
          (sync-switcher-row enable-sync?)
          [:div.text-sm
          [:div.text-sm
-          "Click"
+          (t :settings-page/sync-desc-1)
           [:a.mx-1 {:href "https://blog.logseq.com/how-to-setup-and-use-logseq-sync/"
           [:a.mx-1 {:href "https://blog.logseq.com/how-to-setup-and-use-logseq-sync/"
                     :target "_blank"}
                     :target "_blank"}
-           "here"]
-          "for instructions on how to set up and use Sync."]]])
+           (t :settings-page/sync-desc-2)]
+          (t :settings-page/sync-desc-3)]]])]))
 
 
      ;; (when-not web-platform?
      ;; (when-not web-platform?
      ;;   [:<>
      ;;   [:<>
@@ -801,10 +1010,12 @@
      ;;     {:class (when-not user-handler/alpha-user? "opacity-50 pointer-events-none cursor-not-allowed")}
      ;;     {:class (when-not user-handler/alpha-user? "opacity-50 pointer-events-none cursor-not-allowed")}
      ;;     ;; features
      ;;     ;; features
      ;;     ]])
      ;;     ]])
-     ]))
+     
+
+(def DEFAULT-ACTIVE-TAB-STATE (if config/ENABLE-SETTINGS-ACCOUNT-TAB [:account :account] [:general :general]))
 
 
 (rum/defcs settings
 (rum/defcs settings
-  < (rum/local [:general :general] ::active)
+  < (rum/local DEFAULT-ACTIVE-TAB-STATE ::active)
     {:will-mount
     {:will-mount
      (fn [state]
      (fn [state]
        (state/load-app-user-cfgs)
        (state/load-app-user-cfgs)
@@ -822,15 +1033,18 @@
         *active (::active state)]
         *active (::active state)]
 
 
     [:div#settings.cp__settings-main
     [:div#settings.cp__settings-main
-     [:header
-      [:h1.title (t :settings)]]
 
 
      [:div.cp__settings-inner
      [:div.cp__settings-inner
 
 
       [:aside.md:w-64 {:style {:min-width "10rem"}}
       [:aside.md:w-64 {:style {:min-width "10rem"}}
+       [:header.cp__settings-header
+        (ui/icon "settings")
+        [:h1.cp__settings-modal-title (t :settings)]]
        [:ul.settings-menu
        [:ul.settings-menu
         (for [[label id text icon]
         (for [[label id text icon]
-              [[:general "general" (t :settings-page/tab-general) (ui/icon "adjustments")]
+              [(when config/ENABLE-SETTINGS-ACCOUNT-TAB
+                [:account "account" (t :settings-page/tab-account) (ui/icon "user-circle")])
+               [:general "general" (t :settings-page/tab-general) (ui/icon "adjustments")]
                [:editor "editor" (t :settings-page/tab-editor) (ui/icon "writing")]
                [:editor "editor" (t :settings-page/tab-editor) (ui/icon "writing")]
 
 
                (when (util/electron?)
                (when (util/electron?)
@@ -852,11 +1066,13 @@
               :on-click #(reset! *active [label (first @*active)])}
               :on-click #(reset! *active [label (first @*active)])}
 
 
              [:a.flex.items-center.settings-menu-link
              [:a.flex.items-center.settings-menu-link
-             {:data-id id}
+              {:data-id id}
               icon
               icon
               [:strong text]]]))]]
               [:strong text]]]))]]
 
 
       [:article
       [:article
+       [:header.cp__settings-header
+        [:h1.cp__settings-category-title (name (first @*active))]]
 
 
        (case (first @*active)
        (case (first @*active)
 
 
@@ -866,6 +1082,9 @@
            (reset! *active [label label])
            (reset! *active [label label])
            nil)
            nil)
 
 
+         :account 
+         (settings-account)
+
          :general
          :general
          (settings-general current-repo)
          (settings-general current-repo)
 
 

+ 57 - 40
src/main/frontend/components/settings.css

@@ -1,44 +1,76 @@
 .cp__settings {
 .cp__settings {
-
   &-main {
   &-main {
-    > header {
-      padding: 10px;
-      padding-top: 0;
-      border-bottom: 1px solid var(--ls-quaternary-background-color);
+    aside {
+      @apply bg-gray-400/5 p-4;
+    }
 
 
-      h1 {
-        font-size: 22px;
-        margin: 0;
+    article {
+      @apply p-4 flex-1 min-h-[12rem] w-auto overflow-y-auto;
+      @apply md:max-h-[70vh] md:w-[40rem];
+      /* margin-right: -17px; */
+      /* margin-bottom: -17px; */
+
+      @screen md {
+        /* max-height: 70vh; */
+        /* width: 680px; */
       }
       }
     }
     }
+
+    aside > .cp__settings-header,
+    article > .cp__settings-header {
+      @apply h-10 py-2 flex flex-row items-center justify-start gap-2;
+    }
+
+    aside > .cp__settings-header {
+      @apply px-2;
+    }
+
+    aside > .cp__settings-header > .ui__icon {
+      @apply h-8 w-8 bg-gray-700/10 rounded grid place-items-center;
+    }
+
+    aside > .cp__settings-header > .ui__icon > svg {
+      @apply h-6 w-6;
+    }
+
+    h1.cp__settings-modal-title {
+      @apply text-2xl font-semibold lowercase;
+    }
+
+    h1.cp__settings-category-title {
+      @apply text-xl lowercase;
+    }
+
+    h1.cp__settings-modal-title:first-letter, 
+    h1.cp__settings-category-title:first-letter {
+      @apply uppercase;
+    }
+
+    .settings-menu {
+      @apply p-0 m-0 mt-4 pr-3; 
+    }
+
+    .settings-menu-item {
+      @apply list-none p-0 my-1.5 rounded;
+      @apply hover:bg-black/10;
+    }
+
+    .settings-menu-link {
+      @apply px-2 py-1.5 select-none; 
+      color: var(--ls-primary-text-color);
+    }
   }
   }
 
 
   &-inner {
   &-inner {
     @apply flex flex-col md:flex-row;
     @apply flex flex-col md:flex-row;
 
 
     > aside {
     > aside {
-      border-right: 0 solid var(--ls-quaternary-background-color);
-      border-bottom: 1px solid var(--ls-quaternary-background-color);
-
-      @screen md {
-        border-right: 1px solid var(--ls-quaternary-background-color);
-        border-bottom: 0 solid var(--ls-quaternary-background-color);
-      }
 
 
       ul {
       ul {
-        padding: 12px 12px 12px 5px;
-        margin: 0;
 
 
         > li {
         > li {
-          list-style: none;
-          padding: 0;
-          margin: 5px 0;
-          border-radius: 4px;
 
 
           > a {
           > a {
-            padding: 10px;
-            user-select: none;
-            color: var(--ls-primary-text-color);
 
 
             > i {
             > i {
               overflow: hidden;
               overflow: hidden;
@@ -65,21 +97,6 @@
       }
       }
     }
     }
 
 
-    > article {
-      flex: 1;
-      padding: 0 12px 12px;
-      min-height: 380px;
-      width: auto;
-      overflow: auto;
-      margin-right: -17px;
-      margin-bottom: -17px;
-
-      @screen md {
-        max-height: 70vh;
-        width: 680px;
-      }
-    }
-
     &.no-aside {
     &.no-aside {
       > article {
       > article {
         padding-left: 0;
         padding-left: 0;
@@ -87,7 +104,7 @@
     }
     }
 
 
     .panel-wrap {
     .panel-wrap {
-      padding: 12px;
+      @apply p-1;
 
 
       @screen sm {
       @screen sm {
         width: 600px;
         width: 600px;

+ 90 - 44
src/main/frontend/components/shortcut.cljs

@@ -13,12 +13,15 @@
 
 
 (rum/defcs customize-shortcut-dialog-inner <
 (rum/defcs customize-shortcut-dialog-inner <
   (rum/local "")
   (rum/local "")
+  (rum/local nil :rum/action)
   (shortcut/record!)
   (shortcut/record!)
   [state k action-name current-binding]
   [state k action-name current-binding]
-  (let [keypress (:rum/local state)
-        keyboard-shortcut (if (= "" @keypress) current-binding @keypress)]
-    [:div
-     [:div
+  (let [*keypress         (:rum/local state)
+        *action           (:rum/action state)
+        keypressed?       (not= "" @*keypress)
+        keyboard-shortcut (if-not keypressed? current-binding @*keypress)]
+    [:<>
+     [:div.sm:w-lsm
       [:p.mb-4 "Press any sequence of keys to set the shortcut for the " [:b action-name] " action."]
       [:p.mb-4 "Press any sequence of keys to set the shortcut for the " [:b action-name] " action."]
       [:p.mb-4.mt-4
       [:p.mb-4.mt-4
        (ui/render-keyboard-shortcut (-> keyboard-shortcut
        (ui/render-keyboard-shortcut (-> keyboard-shortcut
@@ -26,26 +29,30 @@
                                         (str/lower-case)
                                         (str/lower-case)
                                         (str/split  #" |\+")))
                                         (str/split  #" |\+")))
        " "
        " "
-       [:a.text-sm
-        {:style {:margin-left "12px"}
-         :on-click (fn []
-                     (dh/remove-shortcut k)
-                     (shortcut/refresh!)
-                     (swap! keypress (fn [] "")) ;; Clear local state
-                     )}
-        "Reset"]]]
+       (when keypressed?
+         [:a.text-sm
+          {:style    {:margin-left "12px"}
+           :on-click (fn []
+                       (dh/remove-shortcut k)
+                       (shortcut/refresh!)
+                       (swap! *keypress (constantly ""))          ;; Clear local state
+                       )}
+          "Reset"])]]
      [:div.cancel-save-buttons.text-right.mt-4
      [:div.cancel-save-buttons.text-right.mt-4
-      (ui/button "Save" :on-click state/close-modal!)
+      (ui/button "Save" :on-click (fn []
+                                    (reset! *action :save)
+                                    (state/close-modal!)))
       [:a.ml-4
       [:a.ml-4
        {:on-click (fn []
        {:on-click (fn []
-                    (reset! keypress (dh/binding-for-storage current-binding))
+                    (reset! *keypress (dh/binding-for-storage current-binding))
+                    (reset! *action :cancel)
                     (state/close-modal!))} "Cancel"]]]))
                     (state/close-modal!))} "Cancel"]]]))
 
 
 (defn customize-shortcut-dialog [k action-name displayed-binding]
 (defn customize-shortcut-dialog [k action-name displayed-binding]
   (fn [_]
   (fn [_]
     (customize-shortcut-dialog-inner k action-name displayed-binding)))
     (customize-shortcut-dialog-inner k action-name displayed-binding)))
 
 
-(rum/defc shortcut-col [k binding configurable? action-name]
+(rum/defc shortcut-col [_category k binding configurable? action-name]
   (let [conflict?         (dh/potential-conflict? k)
   (let [conflict?         (dh/potential-conflict? k)
         displayed-binding (dh/binding-for-display k binding)
         displayed-binding (dh/binding-for-display k binding)
         disabled?         (str/includes? displayed-binding "system default")]
         disabled?         (str/includes? displayed-binding "system default")]
@@ -61,28 +68,48 @@
                  (if disabled? "Cannot override system default" "Click to modify"))
                  (if disabled? "Cannot override system default" "Click to modify"))
         :background (if conflict? "pink" (when disabled? "gray"))
         :background (if conflict? "pink" (when disabled? "gray"))
         :on-click (when-not disabled?
         :on-click (when-not disabled?
-                    #(state/set-modal! (customize-shortcut-dialog k action-name displayed-binding))))])))
-
-(rum/defc shortcut-table < rum/reactive
-  ([name]
-   (shortcut-table name false))
-  ([name configurable?]
-   (let [shortcut-config (rum/cursor-in
-                          state/state
-                          [:config (state/get-current-repo) :shortcuts])
-         _ (rum/react shortcut-config)]
-     [:div
-      [:table
-       [:thead
-        [:tr
-         [:th.text-left [:b (t name)]]
-         [:th.text-right]]]
-       [:tbody
-        (map (fn [[k {:keys [binding]}]]
-               [:tr {:key (str k)}
-                [:td.text-left (t (dh/decorate-namespace k))]
-                (shortcut-col k binding configurable? (t (dh/decorate-namespace k)))])
-          (dh/binding-by-category name))]]])))
+                    #(state/set-sub-modal!
+                       (customize-shortcut-dialog k action-name displayed-binding)
+                       {:center? true})))])))
+
+(rum/defcs shortcut-table
+  < rum/reactive
+    (rum/local true ::folded?)
+    {:will-mount (fn [state]
+                   (let [name (first (:rum/args state))]
+                     (cond-> state
+                             (contains? #{:shortcut.category/basics}
+                                        name)
+                             (-> ::folded? (reset! false) (do state)))))}
+  [state category configurable?]
+  (let [*folded? (::folded? state)
+        plugin?  (= category :shortcut.category/plugins)
+        _        (state/sub [:config (state/get-current-repo) :shortcuts])]
+    [:div.cp__shortcut-table-wrap
+     [:a.fold
+      {:on-click #(reset! *folded? (not @*folded?))}
+      (ui/icon (if @*folded? "chevron-left" "chevron-down"))]
+     [:table
+      [:thead
+       [:tr
+        [:th.text-left [:b (t category)]]
+        [:th.text-right]]]
+      (when-not @*folded?
+        [:tbody
+         (map (fn [[k {:keys [binding]}]]
+                (let [cmd   (dh/shortcut-cmd k)
+                      label (cond
+                              (string? (:desc cmd))
+                              [:<>
+                               [:code.text-xs (namespace k)]
+                               [:small.pl-1 (:desc cmd)]]
+
+                              (not plugin?) (-> k (dh/decorate-namespace) (t))
+                              :else (str k))]
+                  [:tr {:key (str k)}
+                   [:td.text-left.flex.items-center label]
+                   (shortcut-col category k binding configurable? label)]))
+              (dh/binding-by-category category))])]]))
 
 
 (rum/defc trigger-table []
 (rum/defc trigger-table []
   [:table
   [:table
@@ -167,13 +194,9 @@
               [:td.text-right (get rendered name)]])
               [:td.text-right (get rendered name)]])
         list)]]))
         list)]]))
 
 
-(rum/defc shortcut
-  [{:keys [show-title?]
-    :or {show-title? true}}]
-  [:div
-   (when show-title? [:h1.title (t :help/shortcut-page-title)])
-   (trigger-table)
-   (markdown-and-orgmode-syntax)
+(rum/defc keymap-tables
+  []
+  [:div.cp__keymap-tables
    (shortcut-table :shortcut.category/basics true)
    (shortcut-table :shortcut.category/basics true)
    (shortcut-table :shortcut.category/navigating true)
    (shortcut-table :shortcut.category/navigating true)
    (shortcut-table :shortcut.category/block-editing true)
    (shortcut-table :shortcut.category/block-editing true)
@@ -182,4 +205,27 @@
    (shortcut-table :shortcut.category/formatting true)
    (shortcut-table :shortcut.category/formatting true)
    (shortcut-table :shortcut.category/toggle true)
    (shortcut-table :shortcut.category/toggle true)
    (when (state/enable-whiteboards?) (shortcut-table :shortcut.category/whiteboard true))
    (when (state/enable-whiteboards?) (shortcut-table :shortcut.category/whiteboard true))
+   (shortcut-table :shortcut.category/plugins true)
    (shortcut-table :shortcut.category/others true)])
    (shortcut-table :shortcut.category/others true)])
+
+(rum/defc keymap-pane
+  []
+  (let [[ready?, set-ready!] (rum/use-state false)]
+    (rum/use-effect!
+      (fn [] (js/setTimeout #(set-ready! true) 32))
+      [])
+
+    [:div.cp__keymap-pane
+     [:h1.pb-2.text-3xl.pt-2 "Keymap"]
+     (if ready?
+       (keymap-tables)
+       [:p.flex.justify-center.py-20 (ui/loading "")])]))
+
+(rum/defc shortcut-page
+  [{:keys [show-title?]
+    :or {show-title? true}}]
+  [:div.cp__shortcut-page
+   (when show-title? [:h1.title (t :help/shortcut-page-title)])
+   (trigger-table)
+   (markdown-and-orgmode-syntax)
+   (keymap-tables)])

+ 28 - 0
src/main/frontend/components/shortcut.css

@@ -0,0 +1,28 @@
+.ui__modal {
+  &[label="keymap-manager"] {
+    .panel-content {
+      @apply m-[-16px];
+    }
+
+    @screen lg {
+      .panel-content {
+        width: 980px;
+      }
+    }
+  }
+}
+
+.cp__shortcut {
+  &-table-wrap {
+    @apply relative;
+
+    a.fold {
+      @apply absolute right-0 top-0 w-full pt-3 pr-3
+      pb-3 flex items-center justify-end select-none;
+
+      &:active {
+        @apply bg-white/50 opacity-60;
+      }
+    }
+  }
+}

+ 26 - 0
src/main/frontend/components/svg.cljs

@@ -386,3 +386,29 @@
    [:path
    [:path
     {:d
     {:d
      "M256 0C114.6 0 0 114.6 0 256c0 141.4 114.6 256 256 256s256-114.6 256-256C512 114.6 397.4 0 256 0zM352 328c0 13.2-10.8 24-24 24h-144C170.8 352 160 341.2 160 328v-144C160 170.8 170.8 160 184 160h144C341.2 160 352 170.8 352 184V328z"}]])
      "M256 0C114.6 0 0 114.6 0 256c0 141.4 114.6 256 256 256s256-114.6 256-256C512 114.6 397.4 0 256 0zM352 328c0 13.2-10.8 24-24 24h-144C170.8 352 160 341.2 160 328v-144C160 170.8 170.8 160 184 160h144C341.2 160 352 170.8 352 184V328z"}]])
+
+;; Titlebar icons from https://github.com/microsoft/vscode-codicons
+(defn window-minimize
+  ([] (window-minimize 16))
+  ([size]
+   [:svg.icon {:width size :height size :viewBox "0 0 16 16" :fill "currentColor"}
+    [:path {:d "M14 8v1H3V8h11z"}]]))
+
+(defn window-maximize
+  ([] (window-maximize 16))
+  ([size]
+   [:svg.icon {:width size :height size :viewBox "0 0 16 16" :fill "currentColor"}
+    [:path {:d "M3 3v10h10V3H3zm9 9H4V4h8v8z"}]]))
+
+(defn window-restore
+  ([] (window-restore 16))
+  ([size]
+   [:svg.icon {:width size :height size :viewBox "0 0 16 16" :fill "currentColor"}
+    [:path {:d "M3 5v9h9V5H3zm8 8H4V6h7v7z"}]
+    [:path {:fill-rule "evenodd" :clip-rule "evenodd" :d "M5 5h1V4h7v7h-1v1h2V3H5v2z"}]]))
+
+(defn window-close
+  ([] (window-close 16))
+  ([size]
+   [:svg.icon {:width size :height size :viewBox "0 0 16 16" :fill "currentColor"}
+    [:path {:fill-rule "evenodd" :clip-rule "evenodd" :d "M7.116 8l-4.558 4.558.884.884L8 8.884l4.558 4.558.884-.884L8.884 8l4.558-4.558-.884-.884L8 7.116 3.442 2.558l-.884.884L7.116 8z"}]]))

+ 0 - 2
src/main/frontend/components/theme.css

@@ -8,8 +8,6 @@
   --ls-z-index-level-3: 999;
   --ls-z-index-level-3: 999;
   --ls-z-index-level-4: 9999;
   --ls-z-index-level-4: 9999;
   --ls-z-index-level-5: 99999;
   --ls-z-index-level-5: 99999;
-
-  --ls-right-sidebar-width: 40%;
 }
 }
 
 
 html {
 html {

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

@@ -146,7 +146,7 @@
   [page-name]
   [page-name]
   (let [page-entity (model/get-page page-name)
   (let [page-entity (model/get-page page-name)
         {:block/keys [updated-at created-at]} page-entity]
         {:block/keys [updated-at created-at]} page-entity]
-    (str (if (= created-at updated-at) "Created " "Edited ")
+    (str (if (= created-at updated-at) (t :whiteboard/dashboard-card-created) (t :whiteboard/dashboard-card-edited))
          (util/time-ago (js/Date. updated-at)))))
          (util/time-ago (js/Date. updated-at)))))
 
 
 (rum/defc dashboard-preview-card
 (rum/defc dashboard-preview-card
@@ -190,7 +190,7 @@
       (whiteboard-handler/create-new-whiteboard-and-redirect!))}
       (whiteboard-handler/create-new-whiteboard-and-redirect!))}
    (ui/icon "plus")
    (ui/icon "plus")
    [:span.dashboard-create-card-caption.select-none
    [:span.dashboard-create-card-caption.select-none
-    "New whiteboard"]])
+    (t :whiteboard/dashboard-card-new-whiteboard)]])
 
 
 (rum/defc whiteboard-dashboard
 (rum/defc whiteboard-dashboard
   []
   []

+ 52 - 0
src/main/frontend/components/window_controls.cljs

@@ -0,0 +1,52 @@
+(ns frontend.components.window-controls
+  (:require [electron.ipc :as ipc]
+            [frontend.components.svg :as svg]
+            [frontend.context.i18n :refer [t]]
+            [frontend.state :as state]
+            [frontend.ui :as ui]
+            [rum.core :as rum]))
+
+(defn minimize
+  []
+  (ipc/ipc "window-minimize"))
+
+(defn toggle-maximized
+  []
+  (ipc/ipc "window-toggle-maximized"))
+
+(defn close
+  []
+  (ipc/ipc "window-close"))
+
+(defn toggle-fullscreen
+  []
+  (ipc/ipc "window-toggle-fullscreen"))
+
+(rum/defc container < rum/reactive
+  []
+  (let [maximized?  (state/sub :electron/window-maximized?)
+        fullscreen? (state/sub :electron/window-fullscreen?)]
+    [:div.window-controls.flex
+     (if fullscreen?
+       [:button.button.icon.fullscreen-toggle
+        {:title (t :window/exit-fullscreen)
+         :on-click toggle-fullscreen}
+        (ui/icon "arrows-minimize")]
+       [:<>
+        [:button.button.icon.minimize
+         {:title (t :window/minimize)
+          :on-click minimize}
+         (svg/window-minimize)]
+
+        [:button.button.icon.maximize-toggle
+         {:title (if maximized? (t :window/restore) (t :window/maximize))
+          :class (if maximized? "restore" "maximize")
+          :on-click toggle-maximized}
+         (if maximized?
+           (svg/window-restore)
+           (svg/window-maximize))]
+
+        [:button.button.icon.close
+         {:title (t :window/close)
+          :on-click close}
+         (svg/window-close)]])]))

+ 14 - 0
src/main/frontend/components/window_controls.css

@@ -0,0 +1,14 @@
+.window-controls {
+  position: fixed;
+  top: 0;
+  right: 0;
+  z-index: 10;
+
+  .button {
+    -webkit-app-region: no-drag;
+    background: transparent;
+    border-radius: 0;
+    width: 48px;
+    height: 48px;
+  }
+}

+ 6 - 2
src/main/frontend/config.cljs

@@ -29,6 +29,10 @@
 
 
 (goog-define ENABLE-FILE-SYNC-PRODUCTION false)
 (goog-define ENABLE-FILE-SYNC-PRODUCTION false)
 
 
+;; this is a feature flag to enable the account tab
+;; when it launches (when pro plan launches) it should be removed
+(def ENABLE-SETTINGS-ACCOUNT-TAB false)
+
 (if ENABLE-FILE-SYNC-PRODUCTION
 (if ENABLE-FILE-SYNC-PRODUCTION
   (do (def FILE-SYNC-PROD? true)
   (do (def FILE-SYNC-PROD? true)
       (def LOGIN-URL
       (def LOGIN-URL
@@ -339,7 +343,7 @@
 (def custom-css-file "custom.css")
 (def custom-css-file "custom.css")
 (def export-css-file "export.css")
 (def export-css-file "export.css")
 (def custom-js-file "custom.js")
 (def custom-js-file "custom.js")
-(def config-default-content (rc/inline "config.edn"))
+(def config-default-content (rc/inline "templates/config.edn"))
 (def config-default-content-md5 (let [md5 (new crypt/Md5)]
 (def config-default-content-md5 (let [md5 (new crypt/Md5)]
                                   (.update md5 (crypt/stringToUtf8ByteArray config-default-content))
                                   (.update md5 (crypt/stringToUtf8ByteArray config-default-content))
                                   (crypt/byteArrayToHex (.digest md5))))
                                   (crypt/byteArrayToHex (.digest md5))))
@@ -482,7 +486,7 @@
 (defn get-current-repo-assets-root
 (defn get-current-repo-assets-root
   []
   []
   (when-let [repo-dir (and (local-file-based-graph? (state/get-current-repo))
   (when-let [repo-dir (and (local-file-based-graph? (state/get-current-repo))
-                            (get-repo-dir (state/get-current-repo)))]
+                           (get-repo-dir (state/get-current-repo)))]
     (path/path-join repo-dir "assets")))
     (path/path-join repo-dir "assets")))
 
 
 (defn get-custom-js-path
 (defn get-custom-js-path

+ 17 - 5
src/main/frontend/date.cljs

@@ -62,11 +62,13 @@
    (tf/unparse custom-formatter date-time)))
    (tf/unparse custom-formatter date-time)))
 
 
 (defn get-locale-string
 (defn get-locale-string
-  [s]
+  "Accepts a :date-time-no-ms string representation, or a cljs-time date object"
+  [input]
   (try
   (try
-    (->> (tf/parse (tf/formatters :date-time-no-ms) s)
-        (t/to-default-time-zone)
-        (tf/unparse (tf/formatter "MMM do, yyyy")))
+    (->> (cond->> input
+          (string? input) (tf/parse (tf/formatters :date-time-no-ms)))
+         (t/to-default-time-zone)
+         (tf/unparse (tf/formatter "MMM do, yyyy")))
     (catch :default _e
     (catch :default _e
       nil)))
       nil)))
 
 
@@ -209,12 +211,22 @@
    (tf/formatter "yyyy-MM-dd HH:mm")
    (tf/formatter "yyyy-MM-dd HH:mm")
    (t/to-default-time-zone (tc/from-long n))))
    (t/to-default-time-zone (tc/from-long n))))
 
 
+(def iso-parser (tf/formatter "yyyy-MM-dd'T'HH:mm:ss.SSSS'Z'"))
+(defn parse-iso [string]
+  (tf/parse iso-parser string))
+
 (comment
 (comment
   (def default-formatter (tf/formatter "MMM do, yyyy"))
   (def default-formatter (tf/formatter "MMM do, yyyy"))
   (def zh-formatter (tf/formatter "YYYY年MM月dd日"))
   (def zh-formatter (tf/formatter "YYYY年MM月dd日"))
 
 
-  (tf/show-formatters))
+  (tf/show-formatters)
 
 
   ;; :date 2020-05-31
   ;; :date 2020-05-31
   ;; :rfc822 Sun, 31 May 2020 03:00:57 Z
   ;; :rfc822 Sun, 31 May 2020 03:00:57 Z
 
 
+  (let [info {:ExpireTime 1680781356,
+              :UserGroups [],
+              :LemonRenewsAt "2024-04-11T07:28:00.000000Z",
+              :LemonEndsAt nil,
+              :LemonStatus "active"}]
+    (->> info :LemonRenewsAt (tf/parse iso-parser) (< (js/Date.))))) 

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

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

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

@@ -48,10 +48,10 @@
 (defonce query-state (atom {}))
 (defonce query-state (atom {}))
 
 
 ;; Current dynamic component
 ;; Current dynamic component
-(def ^:dynamic *query-component*)
+(def ^:dynamic *query-component* nil)
 
 
 ;; Which reactive queries are triggered by the current component
 ;; Which reactive queries are triggered by the current component
-(def ^:dynamic *reactive-queries*)
+(def ^:dynamic *reactive-queries* nil)
 
 
 ;; component -> query-key
 ;; component -> query-key
 (defonce query-components (atom {}))
 (defonce query-components (atom {}))

+ 2 - 4
src/main/frontend/extensions/excalidraw.cljs

@@ -17,8 +17,7 @@
             [goog.object :as gobj]
             [goog.object :as gobj]
             [goog.functions :refer [debounce]]
             [goog.functions :refer [debounce]]
             [rum.core :as rum]
             [rum.core :as rum]
-            [frontend.mobile.util :as mobile-util]
-            [frontend.context.i18n :refer [t]]))
+            [frontend.mobile.util :as mobile-util]))
 
 
 (def excalidraw (r/adapt-class Excalidraw))
 (def excalidraw (r/adapt-class Excalidraw))
 
 
@@ -148,8 +147,7 @@
     (when (:file option)
     (when (:file option)
       (cond
       (cond
         db-restoring?
         db-restoring?
-        [:div.ls-center
-         (ui/loading (t :loading))]
+        [:div.ls-center (ui/loading)]
 
 
         (false? loading?)
         (false? loading?)
         (draw-inner data option)
         (draw-inner data option)

+ 2 - 3
src/main/frontend/extensions/latex.cljs

@@ -6,8 +6,7 @@
             [frontend.util :as util]
             [frontend.util :as util]
             [frontend.handler.plugin :refer [hook-extensions-enhancer-by-type] :as plugin-handler]
             [frontend.handler.plugin :refer [hook-extensions-enhancer-by-type] :as plugin-handler]
             [promesa.core :as p]
             [promesa.core :as p]
-            [goog.dom :as gdom]
-            [frontend.context.i18n :refer [t]]))
+            [goog.dom :as gdom]))
 
 
 ;; TODO: extracted to a rum mixin
 ;; TODO: extracted to a rum mixin
 (defn loaded? []
 (defn loaded? []
@@ -62,7 +61,7 @@
   [id s block? _display?]
   [id s block? _display?]
   (let [loading? (rum/react *loading?)]
   (let [loading? (rum/react *loading?)]
     (if loading?
     (if loading?
-      (ui/loading (t :loading))
+      (ui/loading)
       (let [element (if block?
       (let [element (if block?
                       :div.latex
                       :div.latex
                       :span.latex-inline)]
                       :span.latex-inline)]

+ 107 - 95
src/main/frontend/extensions/pdf/core.cljs

@@ -347,9 +347,8 @@
                                                     (.setAttribute target "data-x" ax)
                                                     (.setAttribute target "data-x" ax)
                                                     (.setAttribute target "data-y" ay))
                                                     (.setAttribute target "data-y" ay))
                                                   ))}
                                                   ))}
-                           :modifiers [;; minimum
-                                       (js/interact.modifiers.restrictSize
-                                        (bean/->js {:min {:width 60 :height 25}}))]
+                           :modifiers [(js/interact.modifiers.restrict
+                                         (bean/->js {:restriction (.closest el ".page")}))]
                            :inertia   true})
                            :inertia   true})
                          ))]
                          ))]
          ;; destroy
          ;; destroy
@@ -376,10 +375,10 @@
    (for [hl page-hls]
    (for [hl page-hls]
      (let [vw-hl (update-in hl [:position] #(pdf-utils/scaled-to-vw-pos viewer %))]
      (let [vw-hl (update-in hl [:position] #(pdf-utils/scaled-to-vw-pos viewer %))]
        (rum/with-key
        (rum/with-key
-        (if (get-in hl [:content :image])
-          (pdf-highlight-area-region viewer vw-hl hl ops)
-          (pdf-highlights-text-region viewer vw-hl hl ops))
-        (:id hl))
+         (if (get-in hl [:content :image])
+           (pdf-highlight-area-region viewer vw-hl hl ops)
+           (pdf-highlights-text-region viewer vw-hl hl ops))
+         (:id hl))
        ))])
        ))])
 
 
 (rum/defc ^:large-vars/cleanup-todo pdf-highlight-area-selection
 (rum/defc ^:large-vars/cleanup-todo pdf-highlight-area-selection
@@ -388,41 +387,59 @@
   (let [^js viewer-clt          (.. viewer -viewer -classList)
   (let [^js viewer-clt          (.. viewer -viewer -classList)
         ^js cnt-el              (.-container viewer)
         ^js cnt-el              (.-container viewer)
         *el                     (rum/use-ref nil)
         *el                     (rum/use-ref nil)
-        *sta-el                 (rum/use-ref nil)
+        *start-el               (rum/use-ref nil)
         *cnt-rect               (rum/use-ref nil)
         *cnt-rect               (rum/use-ref nil)
+        *page-el                (rum/use-ref nil)
+        *page-rect              (rum/use-ref nil)
+        *start-xy               (rum/use-ref nil)
 
 
-        [start-coord, set-start-coord!] (rum/use-state nil)
-        [end-coord, set-end-coord!] (rum/use-state nil)
+        [start, set-start!] (rum/use-state nil)
+        [end, set-end!] (rum/use-state nil)
         [_ set-area-mode!] (use-atom *area-mode?)
         [_ set-area-mode!] (use-atom *area-mode?)
 
 
         should-start            (fn [^js e]
         should-start            (fn [^js e]
                                   (let [^js target (.-target e)]
                                   (let [^js target (.-target e)]
-                                    (when (and (not
-                                                (.contains (.-classList target) "extensions__pdf-hls-area-region"))
+                                    (when (and (not (.contains (.-classList target) "extensions__pdf-hls-area-region"))
                                                (.closest target ".page"))
                                                (.closest target ".page"))
                                       (and e (or (.-metaKey e)
                                       (and e (or (.-metaKey e)
                                                  (and util/win32? (.-shiftKey e))
                                                  (and util/win32? (.-shiftKey e))
                                                  @*area-mode?)))))
                                                  @*area-mode?)))))
 
 
-        reset-coords            #(do
-                                   (set-start-coord! nil)
-                                   (set-end-coord! nil)
-                                   (rum/set-ref! *sta-el nil))
+        reset-coords!           #(do
+                                   (set-start! nil)
+                                   (set-end! nil)
+                                   (rum/set-ref! *start-xy nil)
+                                   (rum/set-ref! *start-el nil)
+                                   (rum/set-ref! *cnt-rect nil)
+                                   (rum/set-ref! *page-el nil)
+                                   (rum/set-ref! *page-rect nil))
 
 
-        calc-coords             (fn [page-x page-y]
+        calc-coords!            (fn [page-x page-y]
                                   (when cnt-el
                                   (when cnt-el
-                                    (let [cnt-rect (rum/deref *cnt-rect)
-                                          cnt-rect (or cnt-rect (bean/->clj (.toJSON (.getBoundingClientRect cnt-el))))
-                                          _        (rum/set-ref! *cnt-rect cnt-rect)]
+                                    (let [cnt-rect    (rum/deref *cnt-rect)
+                                          cnt-rect    (or cnt-rect (bean/->clj (.toJSON (.getBoundingClientRect cnt-el))))
+                                          page-rect   (rum/deref *page-rect)
+                                          [start-x, start-y] (rum/deref *start-xy)
+                                          dx-left?    (> start-x page-x)
+                                          dy-top?     (> start-y page-y)
+                                          page-left   (:left page-rect)
+                                          page-right  (:right page-rect)
+                                          page-top    (:top page-rect)
+                                          page-bottom (:bottom page-rect)
+                                          _           (rum/set-ref! *cnt-rect cnt-rect)]
 
 
                                       {:x (-> page-x
                                       {:x (-> page-x
-                                              (- (:left cnt-rect))
+                                              (#(if dx-left?
+                                                  (if (< % page-left) page-left %)
+                                                  (if (> % page-right) page-right %)))
                                               (+ (.-scrollLeft cnt-el)))
                                               (+ (.-scrollLeft cnt-el)))
                                        :y (-> page-y
                                        :y (-> page-y
-                                              (- (:top cnt-rect))
+                                              (#(if dy-top?
+                                                  (if (< % page-top) page-top %)
+                                                  (if (> % page-bottom) page-bottom %)))
                                               (+ (.-scrollTop cnt-el)))})))
                                               (+ (.-scrollTop cnt-el)))})))
 
 
-        calc-pos                (fn [start end]
+        calc-rect               (fn [start end]
                                   {:left   (min (:x start) (:x end))
                                   {:left   (min (:x start) (:x end))
                                    :top    (min (:y start) (:y end))
                                    :top    (min (:y start) (:y end))
                                    :width  (js/Math.abs (- (:x end) (:x start)))
                                    :width  (js/Math.abs (- (:x end) (:x start)))
@@ -431,81 +448,78 @@
         disable-text-selection! #(js-invoke viewer-clt (if % "add" "remove") "disabled-text-selection")
         disable-text-selection! #(js-invoke viewer-clt (if % "add" "remove") "disabled-text-selection")
 
 
         fn-move                 (rum/use-callback
         fn-move                 (rum/use-callback
-                                 (fn [^js/MouseEvent e]
-                                   (set-end-coord! (calc-coords (.-pageX e) (.-pageY e))))
-                                 [])]
+                                  (fn [^js/MouseEvent e]
+                                    (set-end! (calc-coords! (.-pageX e) (.-pageY e))))
+                                  [])]
 
 
     (rum/use-effect!
     (rum/use-effect!
-     (fn []
-       (when-let [^js/HTMLElement root cnt-el]
-         (let [fn-start (fn [^js/MouseEvent e]
-                          (if (should-start e)
-                            (do
-                              (rum/set-ref! *sta-el (.-target e))
-                              (set-start-coord! (calc-coords (.-pageX e) (.-pageY e)))
-                              (disable-text-selection! true)
-
-                              (.addEventListener root "mousemove" fn-move))
-
-                            ;; reset
-                            (reset-coords)))
-
-               fn-end   (fn [^js/MouseEvent e]
-                          (when-let [start-el (rum/deref *sta-el)]
-                            (let [end (calc-coords (.-pageX e) (.-pageY e))
-                                  pos (calc-pos start-coord end)]
-
-                              (if (and (> (:width pos) 10)
-                                       (> (:height pos) 10))
-
-                                (when-let [^js page-el (.closest start-el ".page")]
-                                  (let [page-number (int (.-pageNumber (.-dataset page-el)))
-                                        page-pos    (merge pos {:top  (- (:top pos) (.-offsetTop page-el))
-                                                                :left (- (:left pos) (.-offsetLeft page-el))})
-                                        vw-pos      {:bounding page-pos :rects [] :page page-number}
-                                        sc-pos      (pdf-utils/vw-to-scaled-pos viewer vw-pos)
-
-                                        point       {:x (.-clientX e) :y (.-clientY e)}
-                                        hl          {:id         nil
-                                                     :page       page-number
-                                                     :position   sc-pos
-                                                     :content    {:text "[:span]" :image (js/Date.now)}
-                                                     :properties {}}]
-
-                                    ;; ctx tips
-                                    (show-ctx-menu! viewer hl point {:reset-fn #(reset-coords)})
-
-                                    ;; export area highlight
-                                    ;;(dd "[selection end] :start"
-                                    ;;    start-coord ":end" end ":pos" pos
-                                    ;;    ":page" page-number
-                                    ;;    ":offset" page-pos
-                                    ;;    ":vw-pos" vw-pos
-                                    ;;    ":sc-pos" sc-pos)
-                                    )
-
-                                  (set-area-mode! false))
-
-                                ;; reset
-                                (reset-coords)))
-
-                            (disable-text-selection! false)
-                            (.removeEventListener root "mousemove" fn-move)))]
-
-           (doto root
-             (.addEventListener "mousedown" fn-start)
-             (.addEventListener "mouseup" fn-end #js {:once true}))
-
-           ;; destroy
-           #(doto root
-              (.removeEventListener "mousedown" fn-start)
-              (.removeEventListener "mouseup" fn-end)))))
-     [start-coord])
+      (fn []
+        (when-let [^js/HTMLElement root cnt-el]
+          (let [fn-start (fn [^js/MouseEvent e]
+                           (if (should-start e)
+                             (let [target (.-target e)
+                                   page-el (.closest target ".page")
+                                   [x y] [(.-pageX e) (.-pageY e)]]
+                               (rum/set-ref! *start-el target)
+                               (rum/set-ref! *start-xy [x y])
+                               (rum/set-ref! *page-el page-el)
+                               (rum/set-ref! *page-rect (some-> page-el (.getBoundingClientRect) (.toJSON) (bean/->clj)))
+                               (set-start! (calc-coords! x y))
+                               (disable-text-selection! true)
+
+                               (.addEventListener root "mousemove" fn-move))
+
+                             ;; reset
+                             (do (reset-coords!)
+                                 (disable-text-selection! false))))
+
+                fn-end   (fn [^js/MouseEvent e]
+                           (when-let [start-el (rum/deref *start-el)]
+                             (let [end  (calc-coords! (.-pageX e) (.-pageY e))
+                                   rect (calc-rect start end)]
+
+                               (if (and (> (:width rect) 10)
+                                        (> (:height rect) 10))
+
+                                 (when-let [^js page-el (.closest start-el ".page")]
+                                   (let [page-number (int (.-pageNumber (.-dataset page-el)))
+                                         page-pos    (merge rect {:top  (- (:top rect) (.-offsetTop page-el))
+                                                                  :left (- (:left rect) (.-offsetLeft page-el))})
+                                         vw-pos      {:bounding page-pos :rects [] :page page-number}
+                                         sc-pos      (pdf-utils/vw-to-scaled-pos viewer vw-pos)
+
+                                         point       {:x (.-clientX e) :y (.-clientY e)}
+                                         hl          {:id         nil
+                                                      :page       page-number
+                                                      :position   sc-pos
+                                                      :content    {:text "[:span]" :image (js/Date.now)}
+                                                      :properties {}}]
+
+                                     ;; ctx tips
+                                     (show-ctx-menu! viewer hl point {:reset-fn #(reset-coords!)}))
+
+                                   (set-area-mode! false))
+
+                                 ;; reset
+                                 (reset-coords!)))
+
+                             (disable-text-selection! false)
+                             (.removeEventListener root "mousemove" fn-move)))]
+
+            (doto root
+              (.addEventListener "mousedown" fn-start)
+              (.addEventListener "mouseup" fn-end #js {:once true}))
+
+            ;; destroy
+            #(doto root
+               (.removeEventListener "mousedown" fn-start)
+               (.removeEventListener "mouseup" fn-end)))))
+      [start])
 
 
     [:div.extensions__pdf-area-selection
     [:div.extensions__pdf-area-selection
      {:ref *el}
      {:ref *el}
-     (when (and start-coord end-coord)
-       [:div.shadow-rect {:style (calc-pos start-coord end-coord)}])]))
+     (when (and start end)
+       [:div.shadow-rect {:style (calc-rect start end)}])]))
 
 
 (rum/defc ^:large-vars/cleanup-todo pdf-highlights
 (rum/defc ^:large-vars/cleanup-todo pdf-highlights
   [^js el ^js viewer initial-hls loaded-pages {:keys [set-dirty-hls!]}]
   [^js el ^js viewer initial-hls loaded-pages {:keys [set-dirty-hls!]}]
@@ -641,8 +655,6 @@
     ;; render hls
     ;; render hls
     (rum/use-effect!
     (rum/use-effect!
      (fn []
      (fn []
-       ;;(dd "=== rebuild highlights ===" (count highlights))
-
        (when-let [grouped-hls (and (sequential? highlights) (group-by :page highlights))]
        (when-let [grouped-hls (and (sequential? highlights) (group-by :page highlights))]
          (doseq [page loaded-pages]
          (doseq [page loaded-pages]
            (when-let [^js/HTMLDivElement hls-layer (pdf-utils/resolve-hls-layer! viewer page)]
            (when-let [^js/HTMLDivElement hls-layer (pdf-utils/resolve-hls-layer! viewer page)]

+ 1 - 0
src/main/frontend/extensions/pdf/toolbar.cljs

@@ -21,6 +21,7 @@
 (def *area-dashed? (atom ((fnil identity false) (storage/get (str "ls-pdf-area-is-dashed")))))
 (def *area-dashed? (atom ((fnil identity false) (storage/get (str "ls-pdf-area-is-dashed")))))
 (def *area-mode? (atom false))
 (def *area-mode? (atom false))
 (def *highlight-mode? (atom false))
 (def *highlight-mode? (atom false))
+#_:clj-kondo/ignore
 (rum/defcontext *highlights-ctx*)
 (rum/defcontext *highlights-ctx*)
 
 
 (rum/defc pdf-settings
 (rum/defc pdf-settings

+ 27 - 26
src/main/frontend/extensions/srs.cljs

@@ -28,7 +28,8 @@
             [clojure.string :as string]
             [clojure.string :as string]
             [rum.core :as rum]
             [rum.core :as rum]
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.modules.shortcut.core :as shortcut]
-            [medley.core :as medley]))
+            [medley.core :as medley]
+            [frontend.context.i18n :refer [t]]))
 
 
 ;;; ================================================================
 ;;; ================================================================
 ;;; Commentary
 ;;; Commentary
@@ -409,7 +410,7 @@
     (reset! *phase 1)))
     (reset! *phase 1)))
 
 
 (def review-finished
 (def review-finished
-  [:p.p-2 "Congrats, you've reviewed all the cards for this query, see you next time! 💯"])
+  [:p.p-2 (t :flashcards/modal-finished)])
 
 
 (defn- btn-with-shortcut [{:keys [shortcut id btn-text background on-click class]}]
 (defn- btn-with-shortcut [{:keys [shortcut id btn-text background on-click class]}]
   (ui/button
   (ui/button
@@ -457,9 +458,9 @@
            [:div.flex.my-4.justify-between
            [:div.flex.my-4.justify-between
             (when-not (and (not preview?) (= next-phase 1))
             (when-not (and (not preview?) (= next-phase 1))
               (btn-with-shortcut {:btn-text (case next-phase
               (btn-with-shortcut {:btn-text (case next-phase
-                                              1 "Hide answers"
-                                              2 "Show answers"
-                                              3 "Show clozes")
+                                              1 (t :flashcards/modal-btn-hide-answers)
+                                              2 (t :flashcards/modal-btn-show-answers)
+                                              3 (t :flashcards/modal-btn-show-clozes))
                                   :shortcut  "s"
                                   :shortcut  "s"
                                   :id "card-answers"
                                   :id "card-answers"
                                   :class "mr-2"
                                   :class "mr-2"
@@ -467,7 +468,7 @@
             (when (and (not= @card-index (count blocks))
             (when (and (not= @card-index (count blocks))
                        cards?
                        cards?
                        preview?)
                        preview?)
-              (btn-with-shortcut {:btn-text "Next"
+              (btn-with-shortcut {:btn-text (t :flashcards/modal-btn-next-card)
                                   :shortcut "n"
                                   :shortcut "n"
                                   :id       "card-next"
                                   :id       "card-next"
                                   :class    "mr-2"
                                   :class    "mr-2"
@@ -477,7 +478,7 @@
 
 
             (when (and (not preview?) (= 1 next-phase))
             (when (and (not preview?) (= 1 next-phase))
               [:<>
               [:<>
-               (btn-with-shortcut {:btn-text   "Forgotten"
+               (btn-with-shortcut {:btn-text   (t :flashcards/modal-btn-forgotten)
                                    :shortcut   "f"
                                    :shortcut   "f"
                                    :id         "card-forgotten"
                                    :id         "card-forgotten"
                                    :background "red"
                                    :background "red"
@@ -486,12 +487,12 @@
                                                  (let [tomorrow (tc/to-string (t/plus (t/today) (t/days 1)))]
                                                  (let [tomorrow (tc/to-string (t/plus (t/today) (t/days 1)))]
                                                    (editor-property/set-block-property! root-block-id card-next-schedule-property tomorrow)))})
                                                    (editor-property/set-block-property! root-block-id card-next-schedule-property tomorrow)))})
 
 
-               (btn-with-shortcut {:btn-text (if (util/mobile?) "Hard" "Took a while to recall")
+               (btn-with-shortcut {:btn-text (if (util/mobile?) "Hard" (t :flashcards/modal-btn-recall))
                                    :shortcut "t"
                                    :shortcut "t"
                                    :id       "card-recall"
                                    :id       "card-recall"
                                    :on-click #(score-and-next-card 3 card card-index finished? phase review-records cb)})
                                    :on-click #(score-and-next-card 3 card card-index finished? phase review-records cb)})
 
 
-               (btn-with-shortcut {:btn-text   "Remembered"
+               (btn-with-shortcut {:btn-text   (t :flashcards/modal-btn-remembered)
                                    :shortcut   "r"
                                    :shortcut   "r"
                                    :id         "card-remembered"
                                    :id         "card-remembered"
                                    :background "green"
                                    :background "green"
@@ -499,10 +500,10 @@
 
 
             (when preview?
             (when preview?
               (ui/tippy {:html [:div.text-sm
               (ui/tippy {:html [:div.text-sm
-                                "Reset this card so that you can review it immediately."]
+                                (t :flashcards/modal-btn-reset-tip)]
                          :class "tippy-hover"
                          :class "tippy-hover"
                          :interactive true}
                          :interactive true}
-                        (ui/button [:span "Reset"]
+                        (ui/button [:span (t :flashcards/modal-btn-reset)]
                                    :id "card-reset"
                                    :id "card-reset"
                                    :class (util/hiccup->class "opacity-60.hover:opacity-100.card-reset")
                                    :class (util/hiccup->class "opacity-60.hover:opacity-100.card-reset")
                                    :on-click (fn [e]
                                    :on-click (fn [e]
@@ -589,11 +590,11 @@
   (let [cards (db-model/get-macro-blocks (state/get-current-repo) "cards")
   (let [cards (db-model/get-macro-blocks (state/get-current-repo) "cards")
         items (->> (map (comp :logseq.macro-arguments :block/properties) cards)
         items (->> (map (comp :logseq.macro-arguments :block/properties) cards)
                    (map (fn [col] (string/join " " col))))
                    (map (fn [col] (string/join " " col))))
-        items (concat items ["All"])]
+        items (concat items [(t :flashcards/modal-select-all)])]
     (component-select/select {:items items
     (component-select/select {:items items
                               :on-chosen on-chosen
                               :on-chosen on-chosen
                               :close-modal? false
                               :close-modal? false
-                              :input-default-placeholder "Switch to"
+                              :input-default-placeholder (t :flashcards/modal-select-switch)
                               :extract-fn nil})))
                               :extract-fn nil})))
 
 
 ;;; register cards macro
 ;;; register cards macro
@@ -626,12 +627,12 @@
                {:on-mouse-down (fn [e]
                {:on-mouse-down (fn [e]
                                  (util/stop e)
                                  (util/stop e)
                                  (toggle-fn))}
                                  (toggle-fn))}
-               [:span.flex (if (string/blank? query-string) "All" query-string)
+               [:span.flex (if (string/blank? query-string) (t :flashcards/modal-select-all) query-string)
                 [:span {:style {:margin-top 2}}
                 [:span {:style {:margin-top 2}}
                  (svg/caret-down)]]])
                  (svg/caret-down)]]])
             (fn [{:keys [toggle-fn]}]
             (fn [{:keys [toggle-fn]}]
               (cards-select {:on-chosen (fn [query]
               (cards-select {:on-chosen (fn [query]
-                                          (let [query' (if (= query "All") "" query)]
+                                          (let [query' (if (= query (t :flashcards/modal-select-all)) "" query)]
                                             (reset! query-atom query')
                                             (reset! query-atom query')
                                             (toggle-fn)))}))
                                             (toggle-fn)))}))
             {:modal-class (util/hiccup->class
             {:modal-class (util/hiccup->class
@@ -641,13 +642,13 @@
 
 
            ;; FIXME: CSS issue
            ;; FIXME: CSS issue
            (if @*preview-mode?
            (if @*preview-mode?
-             (ui/tippy {:html [:div.text-sm "current/total"]
+             (ui/tippy {:html [:div.text-sm (t :flashcards/modal-current-total)]
                         :interactive true}
                         :interactive true}
                        [:div.opacity-60.text-sm.mr-3
                        [:div.opacity-60.text-sm.mr-3
                         @*card-index
                         @*card-index
                         [:span "/"]
                         [:span "/"]
                         total])
                         total])
-             (ui/tippy {:html [:div.text-sm "overdue/total"]
+             (ui/tippy {:html [:div.text-sm (t :flashcards/modal-overdue-total)]
                         ;; :class "tippy-hover"
                         ;; :class "tippy-hover"
                         :interactive true}
                         :interactive true}
                        [:div.opacity-60.text-sm.mr-3
                        [:div.opacity-60.text-sm.mr-3
@@ -656,7 +657,7 @@
                         total]))
                         total]))
 
 
            (ui/tippy
            (ui/tippy
-            {:html [:div.text-sm "Toggle preview mode"]
+            {:html [:div.text-sm (t :flashcards/modal-toggle-preview-mode)]
              :delay [1000, 100]
              :delay [1000, 100]
              :class "tippy-hover"
              :class "tippy-hover"
              :interactive true
              :interactive true
@@ -671,7 +672,7 @@
              "A"])
              "A"])
 
 
            (ui/tippy
            (ui/tippy
-            {:html [:div.text-sm "Toggle random mode"]
+            {:html [:div.text-sm (t :flashcards/modal-toggle-random-mode)]
              :delay [1000, 100]
              :delay [1000, 100]
              :class "tippy-hover"
              :class "tippy-hover"
              :interactive true}
              :interactive true}
@@ -701,15 +702,15 @@
                      *card-index))]])
                      *card-index))]])
       (if (:global? config)
       (if (:global? config)
         [:div.ls-card.content
         [:div.ls-card.content
-         [:h1.title "Time to create a card!"]
+         [:h1.title (t :flashcards/modal-welcome-title)]
 
 
          [:div
          [:div
-          [:p "You can add \"#card\" to any block to turn it into a card or trigger \"/cloze\" to add some clozes."]
+          [:p (t :flashcards/modal-welcome-desc-1)]
           [:img.my-4 {:src "https://docs.logseq.com/assets/2021-07-22_22.28.02_1626964258528_0.gif"}]
           [:img.my-4 {:src "https://docs.logseq.com/assets/2021-07-22_22.28.02_1626964258528_0.gif"}]
-          [:p "You can "
-           [:a {:href "https://docs.logseq.com/#/page/cards" :target "_blank"}
-            "click this link"]
-           " to check the documentation."]]]
+          [:p (t :flashcards/modal-welcome-desc-2)
+           [:a {:href "https://docs.logseq.com/#/page/Flashcards" :target "_blank"}
+            (t :flashcards/modal-welcome-desc-3)]
+           (t :flashcards/modal-welcome-desc-4)]]]
         [:div.opacity-60.custom-query-title.ls-card.content
         [:div.opacity-60.custom-query-title.ls-card.content
          [:div.w-full.flex-1
          [:div.w-full.flex-1
           [:code.p-1 (str "Cards: " query-string)]]
           [:code.p-1 (str "Cards: " query-string)]]
@@ -809,4 +810,4 @@
       (when (nil? @*due-cards-interval)
       (when (nil? @*due-cards-interval)
         ;; refresh every hour
         ;; refresh every hour
         (let [interval (js/setInterval f (* 3600 1000))]
         (let [interval (js/setInterval f (* 3600 1000))]
-          (reset! *due-cards-interval interval))))))
+          (reset! *due-cards-interval interval))))))

+ 4 - 2
src/main/frontend/extensions/tldraw.cljs

@@ -5,6 +5,7 @@
             [frontend.components.export :as export]
             [frontend.components.export :as export]
             [frontend.components.page :as page]
             [frontend.components.page :as page]
             [frontend.config :as config]
             [frontend.config :as config]
+            [frontend.context.i18n :refer [t]]
             [frontend.db.model :as model]
             [frontend.db.model :as model]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.route :as route-handler]
@@ -93,7 +94,8 @@
 (def undo (fn [] (history/undo! nil)))
 (def undo (fn [] (history/undo! nil)))
 (def redo (fn [] (history/redo! nil)))
 (def redo (fn [] (history/redo! nil)))
 (defn get-tldraw-handlers [current-whiteboard-name]
 (defn get-tldraw-handlers [current-whiteboard-name]
-  {:search search-handler
+  {:t (fn [key] (t (keyword key)))
+   :search search-handler
    :queryBlockByUUID (fn [block-uuid]
    :queryBlockByUUID (fn [block-uuid]
                        (clj->js
                        (clj->js
                         (model/query-block-by-uuid (parse-uuid block-uuid))))
                         (model/query-block-by-uuid (parse-uuid block-uuid))))
@@ -128,7 +130,7 @@
 
 
 (rum/defc tldraw-app
 (rum/defc tldraw-app
   [page-name block-id]
   [page-name block-id]
-  (let [populate-onboarding?  (whiteboard-handler/should-populate-onboarding-whiteboard? page-name)
+  (let [populate-onboarding? (whiteboard-handler/should-populate-onboarding-whiteboard? page-name)
         data (whiteboard-handler/page-name->tldr! page-name)
         data (whiteboard-handler/page-name->tldr! page-name)
         [loaded-app set-loaded-app] (rum/use-state nil)
         [loaded-app set-loaded-app] (rum/use-state nil)
         on-mount (fn [^js tln]
         on-mount (fn [^js tln]

+ 4 - 1
src/main/frontend/handler/code.cljs

@@ -18,9 +18,12 @@
     (when editor
     (when editor
       (.save editor)
       (.save editor)
       (let [textarea (.getTextArea editor)
       (let [textarea (.getTextArea editor)
+            ds (.-dataset textarea)
             value (gobj/get textarea "value")
             value (gobj/get textarea "value")
-            default-value (gobj/get textarea "defaultValue")]
+            default-value (or (.-v ds) (gobj/get textarea "defaultValue"))]
         (when (not= value default-value)
         (when (not= value default-value)
+          ;; update default value for the editor initial state
+          (set! ds -v value)
           (cond
           (cond
             ;; save block content
             ;; save block content
             (:block/uuid config)
             (:block/uuid config)

+ 1 - 1
src/main/frontend/handler/common/config_edn.cljs

@@ -92,7 +92,7 @@ nested keys or positional errors e.g. tuples"
   (let [body (try (edn/read-string content)
   (let [body (try (edn/read-string content)
                (catch :default _ ::failed-to-detect))
                (catch :default _ ::failed-to-detect))
         warnings {:editor/command-trigger
         warnings {:editor/command-trigger
-                  "Will no longer be supported soon. Please use '/' and report bugs on it."}]
+                  "is no longer supported. Please use '/' and report bugs on it."}]
     (cond
     (cond
       (= body ::failed-to-detect)
       (= body ::failed-to-detect)
       (log/info :msg "Skip deprecation check since config is not valid edn")
       (log/info :msg "Skip deprecation check since config is not valid edn")

+ 49 - 36
src/main/frontend/handler/editor.cljs

@@ -169,7 +169,6 @@
   ([text]
   ([text]
    (when-let [m (get-selection-and-format)]
    (when-let [m (get-selection-and-format)]
      (let [{:keys [selection-start selection-end format selection value edit-id input]} m
      (let [{:keys [selection-start selection-end format selection value edit-id input]} m
-           cur-pos (cursor/pos input)
            empty-selection? (= selection-start selection-end)
            empty-selection? (= selection-start selection-end)
            selection-link? (and selection (gp-mldoc/mldoc-link? format selection))
            selection-link? (and selection (gp-mldoc/mldoc-link? format selection))
            [content forward-pos] (cond
            [content forward-pos] (cond
@@ -191,7 +190,7 @@
                       (subs value 0 selection-start)
                       (subs value 0 selection-start)
                       content
                       content
                       (subs value selection-end))
                       (subs value selection-end))
-           cur-pos (or selection-start cur-pos)]
+           cur-pos (or selection-start (cursor/pos input))]
        (state/set-edit-content! edit-id new-value)
        (state/set-edit-content! edit-id new-value)
        (cursor/move-cursor-to input (+ cur-pos forward-pos))))))
        (cursor/move-cursor-to input (+ cur-pos forward-pos))))))
 
 
@@ -301,10 +300,10 @@
            (save-block-inner! block value opts)))))))
            (save-block-inner! block value opts)))))))
 
 
 (defn- compute-fst-snd-block-text
 (defn- compute-fst-snd-block-text
-  [value pos]
+  [value selection-start selection-end]
   (when (string? value)
   (when (string? value)
-    (let [fst-block-text (subs value 0 pos)
-          snd-block-text (string/triml (subs value pos))]
+    (let [fst-block-text (subs value 0 selection-start)
+          snd-block-text (string/triml (subs value selection-end))]
       [fst-block-text snd-block-text])))
       [fst-block-text snd-block-text])))
 
 
 (declare save-current-block!)
 (declare save-current-block!)
@@ -342,13 +341,21 @@
     (= uuid block-id)))
     (= uuid block-id)))
 
 
 (defn insert-new-block-before-block-aux!
 (defn insert-new-block-before-block-aux!
-  [config block _value {:keys [ok-handler]}]
-  (let [new-m {:block/uuid (db/new-block-id)
+  [config block value {:keys [ok-handler]}]
+  (let [edit-input-id (state/get-edit-input-id)
+        input (gdom/getElement edit-input-id)
+        input-text-selected? (util/input-text-selected? input)
+        new-m {:block/uuid (db/new-block-id)
                :block/content ""}
                :block/content ""}
         prev-block (-> (merge (select-keys block [:block/parent :block/left :block/format
         prev-block (-> (merge (select-keys block [:block/parent :block/left :block/format
                                                   :block/page :block/journal?]) new-m)
                                                   :block/page :block/journal?]) new-m)
                        (editor-impl/wrap-parse-block))
                        (editor-impl/wrap-parse-block))
         left-block (db/pull (:db/id (:block/left block)))]
         left-block (db/pull (:db/id (:block/left block)))]
+    (when input-text-selected?
+      (let [selection-start (util/get-selection-start input)
+            selection-end (util/get-selection-end input)
+            [_ new-content] (compute-fst-snd-block-text value selection-start selection-end)]
+        (state/set-edit-content! edit-input-id new-content)))
     (profile
     (profile
      "outliner insert block"
      "outliner insert block"
      (let [sibling? (not= (:db/id left-block) (:db/id (:block/parent block)))]
      (let [sibling? (not= (:db/id left-block) (:db/id (:block/parent block)))]
@@ -365,8 +372,9 @@
     :as _opts}]
     :as _opts}]
   (let [block-self? (block-self-alone-when-insert? config uuid)
   (let [block-self? (block-self-alone-when-insert? config uuid)
         input (gdom/getElement (state/get-edit-input-id))
         input (gdom/getElement (state/get-edit-input-id))
-        pos (cursor/pos input)
-        [fst-block-text snd-block-text] (compute-fst-snd-block-text value pos)
+        selection-start (util/get-selection-start input)
+        selection-end (util/get-selection-end input)
+        [fst-block-text snd-block-text] (compute-fst-snd-block-text value selection-start selection-end)
         current-block (assoc block :block/content fst-block-text)
         current-block (assoc block :block/content fst-block-text)
         current-block (apply dissoc current-block db-schema/retract-attributes)
         current-block (apply dissoc current-block db-schema/retract-attributes)
         new-m {:block/uuid (db/new-block-id)
         new-m {:block/uuid (db/new-block-id)
@@ -419,8 +427,9 @@
                        block)
                        block)
              block-self? (block-self-alone-when-insert? config block-id)
              block-self? (block-self-alone-when-insert? config block-id)
              input (gdom/getElement (state/get-edit-input-id))
              input (gdom/getElement (state/get-edit-input-id))
-             pos (cursor/pos input)
-             [fst-block-text snd-block-text] (compute-fst-snd-block-text value pos)
+             selection-start (util/get-selection-start input)
+             selection-end (util/get-selection-end input)
+             [fst-block-text snd-block-text] (compute-fst-snd-block-text value selection-start selection-end)
              insert-fn (cond
              insert-fn (cond
                          block-self?
                          block-self?
                          insert-new-block-aux!
                          insert-new-block-aux!
@@ -691,7 +700,7 @@
    (delete-block! repo true))
    (delete-block! repo true))
   ([repo delete-children?]
   ([repo delete-children?]
    (state/set-editor-op! :delete)
    (state/set-editor-op! :delete)
-   (let [{:keys [id block-id block-parent-id value format]} (get-state)]
+   (let [{:keys [id block-id block-parent-id value format config]} (get-state)]
      (when block-id
      (when block-id
        (let [page-id (:db/id (:block/page (db/entity [:block/uuid block-id])))
        (let [page-id (:db/id (:block/page (db/entity [:block/uuid block-id])))
              page-blocks-count (and page-id (db/get-page-blocks-count repo page-id))]
              page-blocks-count (and page-id (db/get-page-blocks-count repo page-id))]
@@ -707,24 +716,28 @@
              (when-not (and has-children? left-has-children?)
              (when-not (and has-children? left-has-children?)
                (when block-parent-id
                (when block-parent-id
                  (let [block-parent (gdom/getElement block-parent-id)
                  (let [block-parent (gdom/getElement block-parent-id)
-                       sibling-block (util/get-prev-block-non-collapsed-non-embed block-parent)
+                       sibling-block (if (:embed? config)
+                                       (util/get-prev-block-non-collapsed
+                                        block-parent
+                                        {:container (util/rec-get-blocks-container block-parent)})
+                                       (util/get-prev-block-non-collapsed-non-embed block-parent))
                        {:keys [prev-block new-content move-fn]} (move-to-prev-block repo sibling-block format id value false)
                        {:keys [prev-block new-content move-fn]} (move-to-prev-block repo sibling-block format id value false)
                        concat-prev-block? (boolean (and prev-block new-content))
                        concat-prev-block? (boolean (and prev-block new-content))
                        transact-opts (cond->
                        transact-opts (cond->
-                                       {:outliner-op :delete-blocks}
+                                      {:outliner-op :delete-blocks}
                                        concat-prev-block?
                                        concat-prev-block?
                                        (assoc :concat-data
                                        (assoc :concat-data
                                               {:last-edit-block (:block/uuid block)}))]
                                               {:last-edit-block (:block/uuid block)}))]
                    (outliner-tx/transact! transact-opts
                    (outliner-tx/transact! transact-opts
-                     (if concat-prev-block?
-                       (let [prev-block' (if (seq (:block/_refs block-e))
-                                           (assoc prev-block
-                                                  :block/uuid (:block/uuid block)
-                                                  :block.temp/additional-properties (:block/properties block))
-                                           prev-block)]
-                         (delete-block-aux! block delete-children?)
-                         (save-block! repo prev-block' new-content {:editor/op :delete}))
-                       (delete-block-aux! block delete-children?)))
+                                          (if concat-prev-block?
+                                            (let [prev-block' (if (seq (:block/_refs block-e))
+                                                                (assoc prev-block
+                                                                       :block/uuid (:block/uuid block)
+                                                                       :block.temp/additional-properties (:block/properties block))
+                                                                prev-block)]
+                                              (delete-block-aux! block delete-children?)
+                                              (save-block! repo prev-block' new-content {:editor/op :delete}))
+                                            (delete-block-aux! block delete-children?)))
                    (move-fn)))))))))
                    (move-fn)))))))))
    (state/set-editor-op! nil)))
    (state/set-editor-op! nil)))
 
 
@@ -1416,7 +1429,7 @@
                                        (if file-obj (.-name file-obj) (if image? "image" "asset"))
                                        (if file-obj (.-name file-obj) (if image? "image" "asset"))
                                        image?)
                                        image?)
                   format
                   format
-                  {:last-pattern (if drop-or-paste? "" (state/get-editor-command-trigger))
+                  {:last-pattern (if drop-or-paste? "" commands/command-trigger)
                    :restore?     true
                    :restore?     true
                    :command      :insert-asset})))))
                    :command      :insert-asset})))))
           (p/finally
           (p/finally
@@ -1555,7 +1568,7 @@
           last-command (and last-slash-caret-pos (subs edit-content last-slash-caret-pos pos))]
           last-command (and last-slash-caret-pos (subs edit-content last-slash-caret-pos pos))]
       (when (> pos 0)
       (when (> pos 0)
         (or
         (or
-         (and (= (state/get-editor-command-trigger) (util/nth-safe edit-content (dec pos)))
+         (and (= commands/command-trigger (util/nth-safe edit-content (dec pos)))
               @commands/*initial-commands)
               @commands/*initial-commands)
          (and last-command
          (and last-command
               (commands/get-matched-commands last-command)))))
               (commands/get-matched-commands last-command)))))
@@ -1673,7 +1686,7 @@
                id
                id
                (get-link format link label)
                (get-link format link label)
                format
                format
-               {:last-pattern (str (state/get-editor-command-trigger) "link")
+               {:last-pattern (str commands/command-trigger "link")
                 :command :link})))
                 :command :link})))
 
 
     :image-link (let [{:keys [link label]} m]
     :image-link (let [{:keys [link label]} m]
@@ -1682,7 +1695,7 @@
                      id
                      id
                      (get-image-link format link label)
                      (get-image-link format link label)
                      format
                      format
-                     {:last-pattern (str (state/get-editor-command-trigger) "link")
+                     {:last-pattern (str commands/command-trigger "link")
                       :command :image-link})))
                       :command :image-link})))
 
 
     nil)
     nil)
@@ -1748,12 +1761,11 @@
     (cond
     (cond
       (and (= content "1. ") (= last-input-char " ") input-id edit-block
       (and (= content "1. ") (= last-input-char " ") input-id edit-block
            (not (own-order-number-list? edit-block)))
            (not (own-order-number-list? edit-block)))
-      (do
-        (state/set-edit-content! input-id "")
-        (-> (p/delay 10)
-            (p/then #(state/pub-event! [:editor/toggle-own-number-list edit-block]))))
+      (p/do!
+       (state/pub-event! [:editor/toggle-own-number-list edit-block])
+       (state/set-edit-content! input-id ""))
 
 
-      (and (= last-input-char (state/get-editor-command-trigger))
+      (and (= last-input-char commands/command-trigger)
            (or (re-find #"(?m)^/" (str (.-value input))) (start-of-new-word? input pos)))
            (or (re-find #"(?m)^/" (str (.-value input))) (start-of-new-word? input pos)))
       (do
       (do
         (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
         (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
@@ -1935,6 +1947,7 @@
                                                                          :keep-uuid? keep-uuid?})]
                                                                          :keep-uuid? keep-uuid?})]
          (edit-last-block-after-inserted! result))))))
          (edit-last-block-after-inserted! result))))))
 
 
+
 (defn- block-tree->blocks
 (defn- block-tree->blocks
   "keep-uuid? - maintain the existing :uuid in tree vec"
   "keep-uuid? - maintain the existing :uuid in tree vec"
   [repo tree-vec format keep-uuid? page-name]
   [repo tree-vec format keep-uuid? page-name]
@@ -2605,7 +2618,7 @@
             (delete-block! repo false))))
             (delete-block! repo false))))
 
 
       (and (> current-pos 1)
       (and (> current-pos 1)
-           (= (util/nth-safe value (dec current-pos)) (state/get-editor-command-trigger)))
+           (= (util/nth-safe value (dec current-pos)) commands/command-trigger))
       (do
       (do
         (util/stop e)
         (util/stop e)
         (commands/restore-state)
         (commands/restore-state)
@@ -2891,8 +2904,8 @@
                (util/event-is-composing? e true)])]
                (util/event-is-composing? e true)])]
         (cond
         (cond
           ;; When you type something after /
           ;; When you type something after /
-          (and (= :commands (state/get-editor-action)) (not= k (state/get-editor-command-trigger)))
-          (if (= (state/get-editor-command-trigger) (second (re-find #"(\S+)\s+$" value)))
+          (and (= :commands (state/get-editor-action)) (not= k commands/command-trigger))
+          (if (= commands/command-trigger (second (re-find #"(\S+)\s+$" value)))
             (state/clear-editor-action!)
             (state/clear-editor-action!)
             (let [matched-commands (get-matched-commands input)]
             (let [matched-commands (get-matched-commands input)]
               (if (seq matched-commands)
               (if (seq matched-commands)
@@ -3495,7 +3508,7 @@
         edit-block (state/get-edit-block)
         edit-block (state/get-edit-block)
         target-element (.-nodeName (.-target e))]
         target-element (.-nodeName (.-target e))]
     (cond
     (cond
-      (whiteboard?)
+      (and (whiteboard?) (not edit-input))
       (do
       (do
         (util/stop e)
         (util/stop e)
         (.selectAll (.-api ^js (state/active-tldraw-app))))
         (.selectAll (.-api ^js (state/active-tldraw-app))))

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

@@ -27,7 +27,7 @@
       (js/setTimeout #(util/scroll-editor-cursor element) 50)))
       (js/setTimeout #(util/scroll-editor-cursor element) 50)))
   state)
   state)
 
 
-(defn did-remount!
+(defn will-remount!
   [_old-state state]
   [_old-state state]
   (keyboards-handler/esc-save! state)
   (keyboards-handler/esc-save! state)
   state)
   state)
@@ -45,5 +45,5 @@
 
 
 (def lifecycle
 (def lifecycle
   {:did-mount did-mount!
   {:did-mount did-mount!
-   :did-remount did-remount!
+   :will-remount will-remount!
    :will-unmount will-unmount})
    :will-unmount will-unmount})

+ 22 - 15
src/main/frontend/handler/events.cljs

@@ -23,6 +23,7 @@
             [frontend.components.shell :as shell]
             [frontend.components.shell :as shell]
             [frontend.components.whiteboard :as whiteboard]
             [frontend.components.whiteboard :as whiteboard]
             [frontend.components.user.login :as login]
             [frontend.components.user.login :as login]
+            [frontend.components.shortcut :as shortcut]
             [frontend.components.repo :as repo]
             [frontend.components.repo :as repo]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.context.i18n :refer [t]]
@@ -167,12 +168,8 @@
 
 
 ;; Parameters for the `persist-db` function, to show the notification messages
 ;; Parameters for the `persist-db` function, to show the notification messages
 (def persist-db-noti-m
 (def persist-db-noti-m
-  {:before     #(notification/show!
-                 (ui/loading (t :graph/persist))
-                 :warning)
-   :on-error   #(notification/show!
-                 (t :graph/persist-error)
-                 :error)})
+  {:before     #(ui/notify-graph-persist!)
+   :on-error   #(ui/notify-graph-persist-error!)})
 
 
 (defn- graph-switch-on-persisted
 (defn- graph-switch-on-persisted
   "Logic for keeping db sync when switching graphs
   "Logic for keeping db sync when switching graphs
@@ -949,6 +946,11 @@
 (defmethod handle :editor/quick-capture [[_ args]]
 (defmethod handle :editor/quick-capture [[_ args]]
   (quick-capture/quick-capture args))
   (quick-capture/quick-capture args))
 
 
+(defmethod handle :modal/keymap-manager [[_]]
+  (state/set-modal!
+    #(shortcut/keymap-pane)
+    {:label "keymap-manager"}))
+
 (defmethod handle :editor/toggle-own-number-list [[_ blocks]]
 (defmethod handle :editor/toggle-own-number-list [[_ blocks]]
   (let [batch? (sequential? blocks)
   (let [batch? (sequential? blocks)
         blocks (cond->> blocks
         blocks (cond->> blocks
@@ -973,15 +975,20 @@
   []
   []
   (let [chan (state/get-events-chan)]
   (let [chan (state/get-events-chan)]
     (async/go-loop []
     (async/go-loop []
-      (let [payload (async/<! chan)]
-        (try
-          (handle payload)
-          (catch :default error
-            (let [type :handle-system-events/failed]
-              (js/console.error (str type) (clj->js payload) "\n" error)
-              (state/pub-event! [:capture-error {:error error
-                                                 :payload {:type type
-                                                           :payload payload}}])))))
+      (let [[payload d] (async/<! chan)]
+        (->
+         (try
+           (p/resolved (handle payload))
+           (catch :default error
+             (p/rejected error)))
+         (p/then (fn [result]
+                   (p/resolve! d result)))
+         (p/catch (fn [error]
+                    (let [type :handle-system-events/failed]
+                      (state/pub-event! [:capture-error {:error error
+                                                         :payload {:type type
+                                                                   :payload payload}}])
+                      (p/reject! d error))))))
       (recur))
       (recur))
     chan))
     chan))
 
 

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

@@ -41,7 +41,7 @@
     (state/set-global-config! config)
     (state/set-global-config! config)
     config))
     config))
 
 
-(def default-content (rc/inline "global-config.edn"))
+(def default-content (rc/inline "templates/global-config.edn"))
 
 
 (defn- create-global-config-file-if-not-exists
 (defn- create-global-config-file-if-not-exists
   [repo-url]
   [repo-url]

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff