瀏覽代碼

Merge pull request #1191 from logseq/feat/outliner-core

Core outliner refactor
Tienson Qin 4 年之前
父節點
當前提交
a0a423ceef
共有 100 個文件被更改,包括 3744 次插入4974 次删除
  1. 2 1
      .clj-kondo/config.edn
  2. 9 3
      .github/workflows/build-desktop-release.yml
  3. 1 1
      README.md
  4. 9 7
      deps.edn
  5. 2 0
      externs.js
  6. 6 5
      package.json
  7. 2 2
      resources/css/common.css
  8. 0 8
      resources/css/tooltip.css
  9. 0 1
      resources/electron.html
  10. 7 16
      resources/forge.config.js
  11. 二進制
      resources/icons/canary/logseq.ico
  12. 二進制
      resources/icons/canary/logseq.png
  13. 二進制
      resources/icons/canary/logseq_big_sur.icns
  14. 二進制
      resources/icons/canary/logseq_big_sur.ico
  15. 二進制
      resources/icons/canary/logseq_big_sur.png
  16. 8 2
      resources/js/preload.js
  17. 3 3
      resources/package.json
  18. 9 9
      resources/yarn.lock
  19. 2 1
      shadow-cljs.edn
  20. 58 19
      src/electron/electron/core.cljs
  21. 27 4
      src/electron/electron/handler.cljs
  22. 65 29
      src/electron/electron/search.cljs
  23. 84 46
      src/main/frontend/commands.cljs
  24. 325 262
      src/main/frontend/components/block.cljs
  25. 10 15
      src/main/frontend/components/block.css
  26. 14 0
      src/main/frontend/components/commit.cljs
  27. 12 74
      src/main/frontend/components/content.cljs
  28. 78 71
      src/main/frontend/components/diff.cljs
  29. 60 53
      src/main/frontend/components/editor.cljs
  30. 1 0
      src/main/frontend/components/editor.css
  31. 12 3
      src/main/frontend/components/export.cljs
  32. 2 6
      src/main/frontend/components/file.cljs
  33. 4 18
      src/main/frontend/components/header.cljs
  34. 9 8
      src/main/frontend/components/journal.cljs
  35. 2 2
      src/main/frontend/components/lazy_editor.cljs
  36. 16 68
      src/main/frontend/components/onboarding.cljs
  37. 221 253
      src/main/frontend/components/page.cljs
  38. 14 0
      src/main/frontend/components/page.css
  39. 0 51
      src/main/frontend/components/project.cljs
  40. 0 136
      src/main/frontend/components/publishing.cljs
  41. 0 49
      src/main/frontend/components/publishing.css
  42. 83 71
      src/main/frontend/components/reference.cljs
  43. 8 7
      src/main/frontend/components/right_sidebar.cljs
  44. 17 24
      src/main/frontend/components/search.cljs
  45. 40 0
      src/main/frontend/components/settings.cljs
  46. 65 0
      src/main/frontend/components/shortcut.cljs
  47. 14 33
      src/main/frontend/components/sidebar.cljs
  48. 13 5
      src/main/frontend/config.cljs
  49. 12 9
      src/main/frontend/context/i18n.cljs
  50. 2 1
      src/main/frontend/core.cljs
  51. 24 3
      src/main/frontend/date.cljs
  52. 35 52
      src/main/frontend/db.cljs
  53. 2 21
      src/main/frontend/db/conn.cljs
  54. 1 34
      src/main/frontend/db/debug.cljs
  55. 3 3
      src/main/frontend/db/default.cljs
  56. 269 362
      src/main/frontend/db/model.cljs
  57. 61 0
      src/main/frontend/db/outliner.cljs
  58. 18 17
      src/main/frontend/db/query_dsl.cljs
  59. 40 29
      src/main/frontend/db/query_react.cljs
  60. 70 61
      src/main/frontend/db/react.cljs
  61. 9 2
      src/main/frontend/db/utils.cljs
  62. 107 66
      src/main/frontend/db_schema.cljs
  63. 7 0
      src/main/frontend/debug.cljs
  64. 26 233
      src/main/frontend/dicts.cljs
  65. 50 58
      src/main/frontend/extensions/code.cljs
  66. 4 0
      src/main/frontend/extensions/excalidraw.cljs
  67. 11 4
      src/main/frontend/extensions/zip.cljs
  68. 1 1
      src/main/frontend/external/roam.cljc
  69. 3 7
      src/main/frontend/format.cljs
  70. 276 191
      src/main/frontend/format/block.cljs
  71. 36 20
      src/main/frontend/format/mldoc.cljs
  72. 1 41
      src/main/frontend/format/mldoc_test.cljs
  73. 13 15
      src/main/frontend/fs/nfs.cljs
  74. 12 11
      src/main/frontend/fs/node.cljs
  75. 5 15
      src/main/frontend/fs/watcher_handler.cljs
  76. 1 1
      src/main/frontend/graph.cljs
  77. 23 19
      src/main/frontend/handler.cljs
  78. 72 175
      src/main/frontend/handler/block.cljs
  79. 11 4
      src/main/frontend/handler/common.cljs
  80. 5 24
      src/main/frontend/handler/config.cljs
  81. 42 414
      src/main/frontend/handler/dnd.cljs
  82. 4 10
      src/main/frontend/handler/draw.cljs
  83. 382 628
      src/main/frontend/handler/editor.cljs
  84. 2 2
      src/main/frontend/handler/editor/keyboards.cljs
  85. 5 33
      src/main/frontend/handler/editor/lifecycle.cljs
  86. 80 0
      src/main/frontend/handler/events.cljs
  87. 0 102
      src/main/frontend/handler/expand.cljs
  88. 160 66
      src/main/frontend/handler/export.cljs
  89. 6 7
      src/main/frontend/handler/external.cljs
  90. 160 116
      src/main/frontend/handler/extract.cljs
  91. 41 52
      src/main/frontend/handler/file.cljs
  92. 1 18
      src/main/frontend/handler/git.cljs
  93. 17 12
      src/main/frontend/handler/graph.cljs
  94. 24 47
      src/main/frontend/handler/history.cljs
  95. 31 2
      src/main/frontend/handler/metadata.cljs
  96. 185 330
      src/main/frontend/handler/page.cljs
  97. 0 112
      src/main/frontend/handler/project.cljs
  98. 63 162
      src/main/frontend/handler/repo.cljs
  99. 3 3
      src/main/frontend/handler/route.cljs
  100. 14 13
      src/main/frontend/handler/search.cljs

+ 2 - 1
.clj-kondo/config.edn

@@ -6,4 +6,5 @@
            garden.def/defkeyframes clojure.core/def
            rum.core/defcc rum.core/defc
            clojure.test.check.clojure-test/defspec clojure.core/def
-           clojure.test.check.properties/for-all clojure.core/for}}
+           clojure.test.check.properties/for-all clojure.core/for
+           frontend.modules.outliner.datascript/auto-transact! clojure.core/let}}

+ 9 - 3
.github/workflows/build-desktop-release.yml

@@ -111,10 +111,10 @@ jobs:
         working-directory: ./static
 
       - name: Change Artifact Name For ZIP File
-        run: mv static/out/make/zip/linux/x64/Logseq-linux-x64-*.zip  static/out/make/zip/linux/x64/Logseq-linux.zip
+        run: mv static/out/make/zip/linux/x64/*-linux-x64-*.zip  static/out/make/zip/linux/x64/Logseq-linux.zip
 
       - name: Change Artifact Name For AppImage File
-        run: mv static/out/make/Logseq-*.AppImage  static/out/make/Logseq-linux.AppImage
+        run: mv static/out/make/*-*.AppImage  static/out/make/Logseq-linux.AppImage
 
       - name: Cache Artifact With ZIP format
         uses: actions/upload-artifact@v1
@@ -210,7 +210,7 @@ jobs:
         working-directory: ./static
 
       - name: Change DMG Name
-        run: mv static/out/make/Logseq.dmg static/out/make/logseq-darwin-x64-${{ github.event.inputs.tag-version }}.dmg
+        run: mv static/out/make/*.dmg static/out/make/logseq-darwin-x64-${{ github.event.inputs.tag-version }}.dmg
 
       - name: Cache Artifact DMG
         uses: actions/upload-artifact@v1
@@ -218,6 +218,12 @@ jobs:
           name: Logseq-x64.dmg
           path: static/out/make/logseq-darwin-x64-${{ github.event.inputs.tag-version }}.dmg
 
+      - name: ls files
+        run: du -a static/out/
+
+      - name: Change zip Name
+        run: mv static/out/make/zip/darwin/x64/*.zip static/out/make/zip/darwin/x64/logseq-darwin-x64-${{ github.event.inputs.tag-version }}.zip
+
       - name: Cache Artifact ZIP
         uses: actions/upload-artifact@v1
         with:

+ 1 - 1
README.md

@@ -15,7 +15,7 @@ Use it to organize your todo list, to write your journals, or to record your uni
 
 ## Why Logseq?
 
-[Logseq](https://logseq.com) is a platform for knowledge sharing and management. It focuses on privacy, longevity, and user control.
+[Logseq](https://logseq.com) is a platform for knowledge management and collaboration. It focuses on privacy, longevity, and user control.
 Notice: the backend code will be open-sourced as soon as we’re sure that the backend service meets the security standards.
 
 The server will never store or analyze your private notes. Your data are plain text files and we currently support both Markdown and Emacs Org mode (more to be added soon).

+ 9 - 7
deps.edn

@@ -1,4 +1,4 @@
-{:paths ["src/main"]
+{:paths ["src/main" "templates"]
  :deps
  {org.clojure/clojure         {:mvn/version "1.10.0"}
   cheshire/cheshire {:mvn/version "5.10.0"}
@@ -31,20 +31,22 @@
   hiccups/hiccups             {:mvn/version "0.3.0"}
   tongue/tongue               {:mvn/version "0.2.9"}
   org.clojure/core.async      {:mvn/version "1.3.610"}
-  thheller/shadow-cljs        {:mvn/version "2.11.14"}
+  thheller/shadow-cljs        {:mvn/version "2.12.5"}
   expound/expound             {:mvn/version "0.8.6"}
-  lambdaisland/glogi          {:mvn/version "1.0.74"}
+  com.lambdaisland/glogi      {:mvn/version "1.0.116"}
   binaryage/devtools          {:mvn/version "1.0.2"}}
 
  :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/" "src/electron/"]
-                  :extra-deps  {org.clojure/clojurescript   {:mvn/version "1.10.764"}
+                  :extra-deps  {org.clojure/clojurescript   {:mvn/version "1.10.844"}
                                 org.clojure/tools.namespace {:mvn/version "0.2.11"}
-                                cider/cider-nrepl           {:mvn/version "0.25.5"}}
+                                cider/cider-nrepl           {:mvn/version "0.25.5"}
+                                org.clojars.knubie/cljs-run-test {:mvn/version "1.0.1"}}
                   :main-opts ["-m" "shadow.cljs.devtools.cli"]}
            :test
            {:extra-paths ["src/test/"]
-            :extra-deps  {org.clojure/clojurescript {:mvn/version "1.10.764"}
-                          org.clojure/test.check {:mvn/version "RELEASE"}}
+            :extra-deps  {org.clojure/clojurescript {:mvn/version "1.10.844"}
+                          org.clojure/test.check {:mvn/version "RELEASE"}
+                          org.clojars.knubie/cljs-run-test {:mvn/version "1.0.1"}}
             :main-opts   ["-m" "shadow.cljs.devtools.cli"]}
 
            :test-clj

+ 2 - 0
externs.js

@@ -63,6 +63,8 @@ dummy.run = function() {};
 dummy.all = function() {};
 dummy.transaction = function() {};
 dummy.getPath = function() {};
+dummy.getDoc = function() {};
+dummy.setValue = function() {};
 
 
 /**

+ 6 - 5
package.json

@@ -19,7 +19,7 @@
         "postcss-import-ext-glob": "^2.0.1",
         "postcss-nested": "5.0.5",
         "purgecss": "4.0.2",
-        "shadow-cljs": "2.11.11",
+        "shadow-cljs": "^2.12.5",
         "stylelint": "^13.8.0",
         "stylelint-config-standard": "^20.0.0",
         "tailwindcss": "2.0.3"
@@ -60,6 +60,7 @@
     "dependencies": {
         "@excalidraw/excalidraw": "^0.4.2",
         "@kanru/rage-wasm": "^0.2.1",
+        "@tippyjs/react": "^4.2.5",
         "chokidar": "^3.5.1",
         "chrono-node": "^2.2.4",
         "codemirror": "^5.58.1",
@@ -73,13 +74,13 @@
         "ignore": "^5.1.8",
         "is-svg": "4.2.2",
         "jszip": "^3.5.0",
-        "mldoc": "0.6.9",
-        "mousetrap": "^1.6.5",
+        "mldoc": "0.6.16",
         "path": "^0.12.7",
-        "react": "^17.0.1",
-        "react-dom": "^17.0.1",
+        "react": "^17.0.2",
+        "react-dom": "^17.0.2",
         "react-resize-context": "^3.0.0",
         "react-textarea-autosize": "^8.0.1",
+        "react-tippy": "^1.4.0",
         "react-transition-group": "^4.3.0",
         "url": "^0.11.0",
         "yargs-parser": "^20.2.4"

+ 2 - 2
resources/css/common.css

@@ -34,7 +34,7 @@ html[data-theme='dark'] {
   --ls-table-tr-even-background-color: #03333f;
   --ls-active-primary-color: #8ec2c2;
   --ls-active-secondary-color: #d0e8e8;
-  --ls-block-properties-background-color: #02222a;
+  --ls-block-properties-background-color: #06323e;
   --ls-block-ref-link-text-color: #1a6376;
   --ls-search-background-color: linear-gradient(
     to right,
@@ -396,7 +396,7 @@ li p:first-child,
 }
 
 li p:last-child,
-.block-body p:last-child {
+.block-body p:last-child, .block-body ul:last-child, .block-body ol:last-child, .block-body dl:last-child {
   margin-bottom: 0;
 }
 

文件差異過大導致無法顯示
+ 0 - 8
resources/css/tooltip.css


+ 0 - 1
resources/electron.html

@@ -26,7 +26,6 @@
   <title>Logseq: A local-first knowledge base</title>
   <meta content="logseq" property="og:site_name">
   <meta content="A local-first knowledge base which can be synced using Git." name="description">
-  <script crossorigin="anonymous" defer onload="Sentry.init({dsn: 'https://[email protected]/5311485'});" src="https://asset.logseq.com/static/js/sentry.min.js">
   </script>
 </head>
 <body>

+ 7 - 16
resources/forge.config.js

@@ -2,33 +2,24 @@ const path = require('path')
 
 module.exports = {
   packagerConfig: {
-    icon: './icons/logseq_big_sur.icns',
-    osxSign: {
-      identity: 'Developer ID Application: Tiansheng Qin',
-      'hardened-runtime': true,
-      entitlements: 'entitlements.plist',
-      'entitlements-inherit': 'entitlements.plist',
-      'signature-flags': 'library'
-    },
-    osxNotarize: {
-      appleId: "my-fake-apple-id",
-      appleIdPassword: "my-fake-apple-id-password",
-    },
+    icon: './icons/canary/logseq_big_sur.icns',
+    name: 'Logseq Canary',
   },
   makers: [
     {
       'name': '@electron-forge/maker-squirrel',
       'config': {
-        'name': 'Logseq',
-        'setupIcon': './icons/logseq.ico'
+        title: 'Logseq Canary',
+        name: 'LogseqCanary', // ID name
+        setupIcon: './icons/canary/logseq.ico'
       }
     },
     {
       name: '@electron-forge/maker-dmg',
       config: {
         format: 'ULFO',
-        icon: './icons/logseq_big_sur.icns',
-        name: 'Logseq'
+        icon: './icons/canary/logseq_big_sur.icns',
+        name: 'Logseq Canary'
       }
     },
     {

二進制
resources/icons/canary/logseq.ico


二進制
resources/icons/canary/logseq.png


二進制
resources/icons/canary/logseq_big_sur.icns


二進制
resources/icons/canary/logseq_big_sur.ico


二進制
resources/icons/canary/logseq_big_sur.png


+ 8 - 2
resources/js/preload.js

@@ -67,8 +67,14 @@ contextBridge.exposeInMainWorld('apis', {
    *
    * @param {string} html html file with embedded state
    */
-  exportPublishAssets(html, customCSSPath) {
-    ipcRenderer.invoke('export-publish-assets', html, customCSSPath)
+  exportPublishAssets(html, customCSSPath, repoPath, assetFilenames) {
+    ipcRenderer.invoke(
+      'export-publish-assets',
+      html,
+      customCSSPath,
+      repoPath,
+      assetFilenames
+    )
   },
 
   /**

+ 3 - 3
resources/package.json

@@ -1,9 +1,9 @@
 {
-  "name": "Logseq",
+  "name": "Logseq-Canary",
   "version": "0.0.1",
   "main": "electron.js",
   "author": "Logseq",
-  "description": "A privacy-first, open-source platform for knowledge sharing and management.",
+  "description": "A privacy-first, open-source platform for knowledge management and collaboration.",
   "repository": "https://github.com/logseq/logseq",
   "scripts": {
     "electron:dev": "electron-forge start",
@@ -34,7 +34,7 @@
     "@electron-forge/maker-rpm": "^6.0.0-beta.54",
     "@electron-forge/maker-squirrel": "^6.0.0-beta.54",
     "@electron-forge/maker-zip": "^6.0.0-beta.54",
-    "electron": "11.2.0",
+    "electron": "12.0.4",
     "electron-builder": "^22.10.5",
     "electron-forge-maker-appimage": "trusktr/electron-forge-maker-appimage#patch-1"
   }

+ 9 - 9
resources/yarn.lock

@@ -392,10 +392,10 @@
   resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e"
   integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==
 
-"@types/node@^12.0.12":
-  version "12.20.7"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.7.tgz#1cb61fd0c85cb87e728c43107b5fd82b69bc9ef8"
-  integrity sha512-gWL8VUkg8VRaCAUgG9WmhefMqHmMblxe2rVpMF86nZY/+ZysU+BkAp+3cz03AixWDSSz0ks5WX59yAhv/cDwFA==
+"@types/node@^14.6.2":
+  version "14.14.41"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.41.tgz#d0b939d94c1d7bd53d04824af45f1139b8c45615"
+  integrity sha512-dueRKfaJL4RTtSa7bWeTK1M+VH+Gns73oCgzvYfHZywRCoPSd8EkXBL0mZ9unPTveBn+D9phZBaxuzpwjWkW0g==
 
 "@types/plist@^3.0.1":
   version "3.0.2"
@@ -1683,13 +1683,13 @@ electron-winstaller@^4.0.1:
     lodash.template "^4.2.2"
     temp "^0.9.0"
 
-electron@11.2.0:
-  version "11.2.0"
-  resolved "https://registry.yarnpkg.com/electron/-/electron-11.2.0.tgz#f8577ea4c9ba94068850256145be26b0b89a5dd7"
-  integrity sha512-weszOPAJPoPu6ozL7vR9enXmaDSqH+KE9iZODfbGdnFgtVfVdfyedjlvEGIUJkLMPXM1y/QWwCl2dINzr0Jq5Q==
[email protected].4:
+  version "12.0.4"
+  resolved "https://registry.yarnpkg.com/electron/-/electron-12.0.4.tgz#c2ca4710d0e4da7db6d31c4f55777b08bfcb08e5"
+  integrity sha512-A8Lq3YMZ1CaO1z5z5nsyFxIwkgwXLHUwL2pf9MVUHpq7fv3XUewCMD98EnLL3DdtiyCvw5KMkeT1WGsZh8qFug==
   dependencies:
     "@electron/get" "^1.0.1"
-    "@types/node" "^12.0.12"
+    "@types/node" "^14.6.2"
     extract-zip "^1.0.3"
 
 emoji-regex@^7.0.1:

+ 2 - 1
shadow-cljs.edn

@@ -37,7 +37,8 @@
     :http-root    "public"
     :http-port    3001
     :watch-path   "static"
-    :preloads     [devtools.preload]}}
+    :preloads     [devtools.preload
+                   shadow.remote.runtime.cljs.browser]}}
 
   :electron {:target :node-script
              :output-to "static/electron.js"

+ 58 - 19
src/electron/electron/core.cljs

@@ -7,7 +7,7 @@
             [promesa.core :as p]
             ["fs-extra" :as fs]
             ["path" :as path]
-            ["electron" :refer [BrowserWindow app protocol ipcMain dialog] :as electron]
+            ["electron" :refer [BrowserWindow app protocol ipcMain dialog Menu MenuItem] :as electron]
             ["electron-window-state" :as windowStateKeeper]
             [clojure.core.async :as async]
             [electron.state :as state]))
@@ -26,18 +26,18 @@
   []
   (let [win-state (windowStateKeeper (clj->js {:defaultWidth 980 :defaultHeight 700}))
         win-opts (cond->
-                   {:width         (.-width win-state)
-                    :height        (.-height win-state)
-                    :frame         (not mac?)
-                    :autoHideMenuBar (not mac?)
-                    :titleBarStyle (if mac? "hidden" nil)
-                    :webPreferences
-                    {:plugins                 true ; pdf
-                     :nodeIntegration         false
-                     :nodeIntegrationInWorker false
-                     :contextIsolation        true
-                     :spellcheck              true
-                     :preload                 (path/join js/__dirname "js/preload.js")}}
+                  {:width         (.-width win-state)
+                   :height        (.-height win-state)
+                   :frame         (not mac?)
+                   :autoHideMenuBar (not mac?)
+                   :titleBarStyle (if mac? "hidden" nil)
+                   :webPreferences
+                   {:plugins                 true ; pdf
+                    :nodeIntegration         false
+                    :nodeIntegrationInWorker false
+                    :contextIsolation        true
+                    :spellcheck              true
+                    :preload                 (path/join js/__dirname "js/preload.js")}}
                    linux?
                    (assoc :icon (path/join js/__dirname "icons/logseq.png")))
         url MAIN_WINDOW_ENTRY
@@ -49,10 +49,11 @@
 
 (defn setup-updater! [^js win]
   ;; manual/auto updater
-  (when-not linux?
-    (init-updater {:repo   "logseq/logseq"
-                   :logger logger
-                   :win    win})))
+  ;;(when-not linux?
+  ;;  (init-updater {:repo   "logseq/logseq"
+  ;;                 :logger logger
+  ;;                 :win    win}))
+  )
 
 (defn setup-interceptor! []
   (.registerFileProtocol
@@ -64,19 +65,29 @@
        (callback #js {:path path}))))
   #(.unregisterProtocol protocol "assets"))
 
-(defn- handle-export-publish-assets [_event html custom-css-path]
+(defn- handle-export-publish-assets [_event html custom-css-path repo-path asset-filenames]
   (let [app-path (. app getAppPath)
+        asset-filenames (js->clj asset-filenames)
         paths (js->clj (. dialog showOpenDialogSync (clj->js {:properties ["openDirectory" "createDirectory" "promptToCreate", "multiSelections"]})))]
     (when (seq paths)
       (let [root-dir (first paths)
             static-dir (path/join root-dir "static")
+            assets-from-dir (path/join repo-path "assets")
+            assets-to-dir (path/join root-dir "assets")
             index-html-path (path/join root-dir "index.html")]
         (p/let [_ (. fs ensureDir static-dir)
+                _ (. fs ensureDir assets-to-dir)
                 _ (p/all  (concat
                            [(. fs writeFile index-html-path html)
 
+
                             (. fs copy (path/join app-path "404.html") (path/join root-dir "404.html"))]
 
+                           (map
+                            (fn [filename] (. fs copy (path/join assets-from-dir filename) (path/join assets-to-dir filename)))
+
+                            asset-filenames)
+
                            (map
                             (fn [part]
                               (. fs copy (path/join app-path part) (path/join static-dir part)))
@@ -98,7 +109,7 @@
                 _ (p/all (map (fn [file]
                                 (. fs removeSync (path/join static-dir "js" (str file ".map"))))
                               ["main.js" "code-editor.js" "excalidraw.js" "age-encryption.js"]))]
-          (. dialog showMessageBox (clj->js {:message (str "Export publish assets to " root-dir " successfully")})))))))
+          (. dialog showMessageBox (clj->js {:message (str "Export public pages and publish assets to " root-dir " successfully")})))))))
 
 (defn setup-app-manager!
   [^js win]
@@ -127,6 +138,30 @@
                    (catch js/Error e
                      (js/console.error e))))))
 
+
+    (.on web-contents "context-menu"
+         (fn
+           [_event params]
+           (let [menu (Menu.)
+                 suggestions (.-dictionarySuggestions ^js params)]
+
+             (doseq [suggestion suggestions]
+               (. menu append
+                  (MenuItem. (clj->js {:label
+                                       suggestion
+                                       :click
+                                       (fn [] (. web-contents replaceMisspelling suggestion))}))))
+
+             (when-let [misspelled-word (.-misspelledWord ^js params)]
+               (. menu append
+                  (MenuItem. (clj->js {:label
+                                       "Add to dictionary"
+                                       :click
+                                       (fn [] (.. web-contents -session (addWordToSpellCheckerDictionary misspelled-word)))}))))
+
+             (. menu popup))))
+
+
     (.on web-contents  "new-window"
          (fn [e url]
            (let [url (if (string/starts-with? url "file:")
@@ -173,7 +208,11 @@
                    *quitting? (atom false)]
                (.. logger (info (str "Logseq App(" (.getVersion app) ") Starting... ")))
 
+               (when (search/version-changed?)
+                 (search/rm-search-dir!))
+
                (search/ensure-search-dir!)
+
                (search/open-dbs!)
 
                (vreset! *setup-fn

+ 27 - 4
src/electron/electron/handler.cljs

@@ -2,6 +2,7 @@
   (:require ["electron" :refer [ipcMain dialog app]]
             [cljs-bean.core :as bean]
             ["fs" :as fs]
+            ["fs-extra" :as fs-extra]
             ["path" :as path]
             ["chokidar" :as watcher]
             [promesa.core :as p]
@@ -62,8 +63,8 @@
   (or
    (some #(string/starts-with? path (str dir "/" %))
          ["." "assets" "node_modules"])
-   (some #(string/ends-with? path (str dir "/" %))
-         [".swap" ".crswap" ".tmp"])))
+   (some #(string/ends-with? path %)
+         [".swap" ".crswap" ".tmp" ".DS_Store"])))
 
 (defonce allowed-formats
   #{:org :markdown :md :edn :json :css :excalidraw})
@@ -105,8 +106,8 @@
   (async/put! state/persistent-dbs-chan true )
   true)
 
-(defmethod handle :search-blocks [window [_ repo q limit]]
-  (search/search-blocks repo q limit))
+(defmethod handle :search-blocks [window [_ repo q opts]]
+  (search/search-blocks repo q opts))
 
 (defmethod handle :rebuild-blocks-indice [window [_ repo data]]
   (search/truncate-blocks-table! repo)
@@ -127,6 +128,21 @@
 (defmethod handle :remove-db [window [_ repo]]
   (search/delete-db! repo))
 
+(defn clear-cache!
+  []
+  (let [path (.getPath ^object app "userData")]
+    (doseq [dir ["search" "IndexedDB" "Local Storage" "databases" "cache"]]
+      (let [path (path/join path dir)]
+        (try
+          (fs-extra/removeSync path)
+          (catch js/Error e
+            (js/console.error e)))))))
+
+(defmethod handle :clearCache [_window _]
+  (search/close!)
+  (clear-cache!)
+  (search/ensure-search-dir!))
+
 (defn- get-file-ext
   [file]
   (last (string/split file #"\.")))
@@ -137,6 +153,7 @@
       (send file-watcher-chan
             (bean/->js {:type type :payload payload}))))
 
+(defonce polling-interval 5000)
 (defn watch-dir!
   [win dir]
   (when (fs/existsSync dir)
@@ -144,8 +161,14 @@
                           (clj->js
                            {:ignored (partial ignored-path? dir)
                             :ignoreInitial true
+                            :ignorePermissionErrors true
+                            :interval polling-interval
+                            :binaryInterval polling-interval
                             :persistent true
+                            :disableGlobbing true
+
                             :awaitWriteFinish true}))]
+      ;; TODO: batch sender
       (.on watcher "add"
            (fn [path]
              (send-file-watcher! win "add"

+ 65 - 29
src/electron/electron/search.cljs

@@ -6,6 +6,8 @@
             [electron.utils :refer [logger] :as utils]
             ["electron" :refer [app]]))
 
+(defonce version "0.0.1")
+
 (def error (partial (.-error logger) "[Search]"))
 
 (defonce databases (atom nil))
@@ -40,15 +42,15 @@
     END;"
                   "CREATE TRIGGER IF NOT EXISTS blocks_ai AFTER INSERT ON blocks
     BEGIN
-        INSERT INTO blocks_fts (rowid, uuid, content)
-        VALUES (new.id, new.uuid, new.content);
+        INSERT INTO blocks_fts (rowid, uuid, content, page)
+        VALUES (new.id, new.uuid, new.content, new.page);
     END;
 "
                   "CREATE TRIGGER IF NOT EXISTS blocks_au AFTER UPDATE ON blocks
     BEGIN
         DELETE from blocks_fts where rowid = old.id;
-        INSERT INTO blocks_fts (rowid, uuid, content)
-        VALUES (new.id, new.uuid, new.content);
+        INSERT INTO blocks_fts (rowid, uuid, content, page)
+        VALUES (new.id, new.uuid, new.content, new.page);
     END;"
                   ]]
     (doseq [trigger triggers]
@@ -60,12 +62,13 @@
   (let [stmt (prepare db "CREATE TABLE IF NOT EXISTS blocks (
                         id INTEGER PRIMARY KEY,
                         uuid TEXT NOT NULL,
-                        content TEXT NOT NULL)")]
+                        content TEXT NOT NULL,
+                        page INTEGER)")]
     (.run ^object stmt)))
 
 (defn create-blocks-fts-table!
   [db]
-  (let [stmt (prepare db "CREATE VIRTUAL TABLE IF NOT EXISTS blocks_fts USING fts5(uuid, content)")]
+  (let [stmt (prepare db "CREATE VIRTUAL TABLE IF NOT EXISTS blocks_fts USING fts5(uuid, content, page)")]
     (.run ^object stmt)))
 
 (defn get-search-dir
@@ -73,10 +76,36 @@
   (let [path (.getPath ^object app "userData")]
     (path/join path "search")))
 
+(defonce search-version "search.version")
+
+(defn get-search-version
+  []
+  (let [path (.getPath ^object app "userData")
+        path (path/join path search-version)]
+    (when (fs/existsSync path)
+      (.toString (fs/readFileSync path)))))
+
+(defn write-search-version!
+  []
+  (let [path (.getPath ^object app "userData")
+        path (path/join path search-version)]
+    (fs/writeFileSync path version)))
+
+(defn version-changed?
+  []
+  (not= version (get-search-version)))
+
 (defn ensure-search-dir!
   []
+  (write-search-version!)
   (fs/ensureDirSync (get-search-dir)))
 
+(defn rm-search-dir!
+  []
+  (let [search-dir (get-search-dir)]
+    (when (fs/existsSync search-dir)
+      (fs/removeSync search-dir))))
+
 (defn get-db-full-path
   [db-name]
   (let [db-name (normalize-db-name db-name)
@@ -89,8 +118,7 @@
         db (sqlite3 db-full-path nil)
         _ (create-blocks-table! db)
         _ (create-blocks-fts-table! db)
-        _ (add-triggers! db)
-        ]
+        _ (add-triggers! db)]
     (swap! databases assoc db-name db)))
 
 (defn open-dbs!
@@ -105,7 +133,7 @@
   [repo blocks]
   (if-let [db (get-db repo)]
     ;; TODO: what if a CONFLICT on uuid
-    (let [insert (prepare db "INSERT INTO blocks (id, uuid, content) VALUES (@id, @uuid, @content) ON CONFLICT (id) DO UPDATE SET content = @content")
+    (let [insert (prepare db "INSERT INTO blocks (id, uuid, content, page) VALUES (@id, @uuid, @content, @page) ON CONFLICT (id) DO UPDATE SET content = @content")
           insert-many (.transaction ^object db
                                     (fn [blocks]
                                       (doseq [block blocks]
@@ -132,7 +160,7 @@
 ;;       (js->clj (.all ^object stmt q) :keywordize-keys true))))
 
 (defn search-blocks
-  [repo q limit]
+  [repo q {:keys [limit page]}]
   (when-let [database (get-db repo)]
     (when-not (string/blank? q)
       (let [match? (or
@@ -142,23 +170,30 @@
                     (string/includes? q " | ")
                     ;; (string/includes? q " not ")
                     )
-            q (if match?
-                (-> q
-                    (string/replace " and " " AND ")
-                    (string/replace " & " " AND ")
-                    (string/replace " or " " OR ")
-                    (string/replace " | " " OR ")
-                    (string/replace " not " " NOT "))
-                q)
-            limit (or limit 20)
-            [sql input] (if match?
-                          ["select rowid, uuid, content from blocks_fts where content match ? order by rank limit ?"
-                           q]
-                          (let [q (string/replace q #"\s+" "%")]
-                            ["select rowid, uuid, content from blocks_fts where content like ? limit ?"
-                             (str "%" q "%")]))
-            stmt (prepare database sql)]
-        (js->clj (.all ^object stmt input limit) :keywordize-keys true)))))
+            input  (if match?
+                         (-> q
+                             (string/replace " and " " AND ")
+                             (string/replace " & " " AND ")
+                             (string/replace " or " " OR ")
+                             (string/replace " | " " OR ")
+                             (string/replace " not " " NOT "))
+                         (str "%" (string/replace q #"\s+" "%") "%"))
+            limit  (or limit 20)
+            select "select rowid, uuid, content, page from blocks_fts where "
+            pg-sql (if page "page = ? and" "")
+            sql    (if match?
+                     (str select
+                          pg-sql
+                          " content match ? order by rank limit ?")
+                     (str select
+                          pg-sql
+                          " content like ? limit ?"))
+            stmt   (prepare database sql)]
+        (js->clj
+         (if page
+           (.all ^object stmt (int page) input limit)
+           (.all ^object stmt  input limit))
+          :keywordize-keys true)))))
 
 (defn truncate-blocks-table!
   [repo]
@@ -174,9 +209,10 @@
   [repo]
   (when-let [database (get-db repo)]
     (.close database)
-    (let [[_ db-full-path] (get-db-full-path repo)]
+    (let [[db-name db-full-path] (get-db-full-path repo)]
       (println "Delete search indice: " db-full-path)
-      (fs/unlinkSync db-full-path))))
+      (fs/unlinkSync db-full-path)
+      (swap! databases dissoc db-name))))
 
 (defn query
   [repo sql]

+ 84 - 46
src/main/frontend/commands.cljs

@@ -1,9 +1,12 @@
 (ns frontend.commands
   (:require [frontend.util :as util]
+            [frontend.util.marker :as marker]
+            [frontend.util.priority :as priority]
             [frontend.date :as date]
             [frontend.state :as state]
             [frontend.search :as search]
             [frontend.config :as config]
+            [frontend.db :as db]
             [clojure.string :as string]
             [goog.dom :as gdom]
             [goog.object :as gobj]
@@ -89,6 +92,20 @@
        ["NOW" (->marker "NOW")]])))
 
 ;; Credits to roamresearch.com
+
+(defn- ->heading
+  [heading]
+  [[:editor/clear-current-slash]
+   [:editor/set-heading heading]
+   [:editor/move-cursor-to-end]])
+
+(defn- markdown-headings
+  []
+  (let [format (state/get-preferred-format)]
+    (when (= (name format) "markdown")
+      (mapv (fn [level]
+              (let [heading (str "h" level)]
+                [heading (->heading (apply str (repeat level "#")))])) (range 1 7)))))
 (defn commands-map
   [get-page-ref-text]
   (->>
@@ -157,7 +174,8 @@
 
      ;; TODO:
      ;; ["Upload a file" nil]
-]
+     ]
+    (markdown-headings)
     ;; Allow user to modify or extend, should specify how to extend.
     (state/get-commands))
    (remove nil?)
@@ -176,9 +194,20 @@
   ([type]
    (->block type nil))
   ([type optional]
-   (let [left (util/format "#+BEGIN_%s"
-                           (string/upper-case type))
-         right (util/format "\n#+END_%s" (string/upper-case type))
+   (let [format (get state/get-edit-block :block/format :markdown)
+         org? (= format :org)
+         t (string/lower-case type)
+         markdown-src? (and (= format :markdown) (= t "src"))
+         left (cond
+                markdown-src?
+                "```"
+
+                :else
+                (util/format "#+BEGIN_%s"
+                             (string/upper-case type)))
+         right (if markdown-src?
+                 (str "\n```")
+                 (util/format "\n#+END_%s" (string/upper-case type)))
          template (str
                    left
                    (if optional (str " " optional) "")
@@ -207,7 +236,8 @@
      ["Src" (->block "src" "")]
      ["Query" (->block "query")]
      ["Latex export" (->block "export" "latex")]
-     ["Properties" (->properties)]
+     (when-not (= :markdown (state/get-preferred-format))
+       ["Properties" (->properties)])
      ["Note" (->block "note")]
      ["Tip" (->block "tip")]
      ["Important" (->block "important")]
@@ -243,34 +273,38 @@
    {:keys [last-pattern postfix-fn backward-pos forward-pos]
     :or {last-pattern slash}
     :as option}]
-  (let [input (gdom/getElement id)
-        edit-content (gobj/get input "value")
-        current-pos (:pos (util/get-caret-pos input))
-        prefix (subs edit-content 0 current-pos)
-        space? (when last-pattern
-                 (let [s (when-let [last-index (string/last-index-of prefix last-pattern)]
-                           (util/safe-subs prefix 0 last-index))]
-                   (not (and (string/ends-with? s "(")
-                             (or (string/starts-with? last-pattern "((")
-                                 (string/starts-with? last-pattern "[["))))))
-        prefix (if (string/blank? last-pattern)
-                 (if space?
-                   (util/concat-without-spaces prefix value)
-                   (str prefix value))
-                 (util/replace-last last-pattern prefix value space?))
-        postfix (subs edit-content current-pos)
-        postfix (if postfix-fn (postfix-fn postfix) postfix)
-        new-value (if space?
-                    (util/concat-without-spaces prefix postfix)
-                    (str prefix postfix))
-        new-pos (- (+ (count prefix)
-                      (or forward-pos 0))
-                   (or backward-pos 0))]
-    (state/set-block-content-and-last-pos! id new-value new-pos)
-    (util/move-cursor-to input
-                         (if (or backward-pos forward-pos)
-                           new-pos
-                           (+ new-pos 1)))))
+  (when-let [input (gdom/getElement id)]
+    (let [edit-content (gobj/get input "value")
+          current-pos (:pos (util/get-caret-pos input))
+          prefix (subs edit-content 0 current-pos)
+          space? (when (and last-pattern prefix)
+                   (let [s (when-let [last-index (string/last-index-of prefix last-pattern)]
+                             (util/safe-subs prefix 0 last-index))]
+                     (not (and s
+                               (string/ends-with? s "(")
+                               (or (string/starts-with? last-pattern "((")
+                                   (string/starts-with? last-pattern "[["))))))
+          space? (if (and space? (string/starts-with? last-pattern "#[["))
+                   false
+                   space?)
+          prefix (if (string/blank? last-pattern)
+                   (if space?
+                     (util/concat-without-spaces prefix value)
+                     (str prefix value))
+                   (util/replace-last last-pattern prefix value space?))
+          postfix (subs edit-content current-pos)
+          postfix (if postfix-fn (postfix-fn postfix) postfix)
+          new-value (if space?
+                      (util/concat-without-spaces prefix postfix)
+                      (str prefix postfix))
+          new-pos (- (+ (count prefix)
+                        (or forward-pos 0))
+                     (or backward-pos 0))]
+      (state/set-block-content-and-last-pos! id new-value new-pos)
+      (util/move-cursor-to input
+                           (if (or backward-pos forward-pos)
+                             new-pos
+                             (+ new-pos 1))))))
 
 (defn simple-insert!
   [id value
@@ -402,7 +436,7 @@
 
 (defn compute-pos-delta-when-change-marker
   [current-input edit-content new-value marker pos]
-  (let [old-marker (some->> (first (re-find format/bare-marker-pattern edit-content))
+  (let [old-marker (some->> (first (re-find marker/bare-marker-pattern edit-content))
                             (string/trim))
         old-marker (if old-marker old-marker "")
         pos-delta (- (count marker)
@@ -427,7 +461,7 @@
                     (count (re-find re-pattern prefix))))
             new-value (str (subs edit-content 0 pos)
                            (string/replace-first (subs edit-content pos)
-                                                 format/marker-pattern
+                                                 marker/marker-pattern
                                                  (str marker " ")))]
         (state/set-edit-content! input-id new-value)
         (let [new-pos (compute-pos-delta-when-change-marker
@@ -436,26 +470,30 @@
           (js/setTimeout #(util/set-caret-pos! current-input new-pos) 10))))))
 
 (defmethod handle-step :editor/set-priority [[_ priority] format]
+  (when-let [input-id (state/get-edit-input-id)]
+    (when-let [current-input (gdom/getElement input-id)]
+      (let [format (or (db/get-page-format (state/get-current-page)) (state/get-preferred-format))
+            edit-content (gobj/get current-input "value")
+            new-priority (util/format "[#%s]" priority)
+            new-value (string/trim (priority/add-or-update-priority edit-content format new-priority))]
+        (state/set-edit-content! input-id new-value)))))
+
+(defmethod handle-step :editor/set-heading [[_ heading]]
   (when-let [input-id (state/get-edit-input-id)]
     (when-let [current-input (gdom/getElement input-id)]
       (let [edit-content (gobj/get current-input "value")
             slash-pos (:pos @*slash-caret-pos)
-            priority-pattern  #"\[#[A|B|C]{1}\]"
+            heading-pattern  #"^#\+"
             prefix (subs edit-content 0 (dec slash-pos))
-            pos (count (re-find priority-pattern prefix))
-            new-priority (util/format "[#%s]" priority)
+            pos (count (re-find heading-pattern prefix))
             new-value (cond
-                        (re-find priority-pattern prefix)
+                        (re-find heading-pattern prefix)
                         (str (subs edit-content 0 pos)
                              (string/replace-first (subs edit-content pos)
-                                                   priority-pattern
-                                                   new-priority))
-                        (re-find format/marker-pattern edit-content)
-                        (string/replace-first edit-content format/marker-pattern
-                                              (fn [marker] (str marker new-priority " ")))
-
+                                                   heading-pattern
+                                                   heading))
                         :else
-                        (str new-priority " " (string/triml edit-content)))]
+                        (str heading " " (string/triml edit-content)))]
         (state/set-edit-content! input-id new-value)))))
 
 (defmethod handle-step :editor/search-page [[_]]

文件差異過大導致無法顯示
+ 325 - 262
src/main/frontend/components/block.cljs


+ 10 - 15
src/main/frontend/components/block.css

@@ -74,7 +74,7 @@
 
   .resize {
     display: inline-flex;
-    /* Fix chrome missing resize handle issue. Ref: https://github.com/logseq/logseq/pull/1692/files */
+    /* Fix chrome missing resize handle issue https://bugs.chromium.org/p/chromium/issues/detail?id=1135676&q=css%20resize%20type%3DBug&can=2.*/
     transform: translate3d(0, 0, 0);
   }
 
@@ -101,6 +101,10 @@
   }
 }
 
+.block-body ul, .block-body ol, .block-body dl {
+    margin-bottom: 2em;
+}
+
 .block-children {
   border-left: 1px solid;
   border-left-color: var(--ls-guideline-color, #ddd);
@@ -157,25 +161,16 @@
 }
 
 .blocks-properties {
+  padding: 4px 8px;
   background-color: var(--ls-block-properties-background-color, #f0f8ff);
 }
 
 .marker-switch {
-  font-size: 85%;
-  margin-right: 6px;
-  margin-left: 2px;
-  border-radius: 3px;
-  font-weight: 500;
-  display: inline-block;
-  text-align: center;
-  width: 16px;
-  height: 18px;
+  padding: 2px 4px;
   opacity: 0.5;
-  padding: 0 2px 0 2px;
-  border: 1px solid;
-  line-height: 1.3;
-  color: var(--ls-link-text-color, #045591);
-  cursor: pointer;
+  font-size: 85%;
+  margin: 0 2px 0 0px;
+  font-weight: 600;
 
   &:hover {
     color: var(--ls-link-text-hover-color);

+ 14 - 0
src/main/frontend/components/commit.cljs

@@ -1,6 +1,7 @@
 (ns frontend.components.commit
   (:require [rum.core :as rum]
             [frontend.util :as util :refer-macros [profile]]
+            [clojure.string :as string]
             [frontend.handler.repo :as repo-handler]
             [frontend.state :as state]
             [frontend.mixins :as mixins]
@@ -53,3 +54,16 @@
         {:type "button"
          :on-click close-fn}
         "Cancel"]]]]))
+
+(defn show-commit-modal! [e]
+  (when (and
+         (string/starts-with? (state/get-current-repo) "https://")
+         (not (util/input? (gobj/get e "target")))
+         (not (gobj/get e "shiftKey"))
+         (not (gobj/get e "ctrlKey"))
+         (not (gobj/get e "altKey"))
+         (not (gobj/get e "metaKey")))
+    (when-let [repo-url (state/get-current-repo)]
+      (when-not (state/get-edit-input-id)
+        (util/stop e)
+        (state/set-modal! commit-and-push!)))))

+ 12 - 74
src/main/frontend/components/content.cljs

@@ -56,11 +56,7 @@
     (ui/menu-link
      {:key "copy"
       :on-click editor-handler/copy-selection-blocks}
-     "Copy")
-    (ui/menu-link
-     {:key "make-todos"
-      :on-click editor-handler/bulk-make-todos}
-     (str "Make " (state/get-preferred-todo) "s"))]])
+     "Copy")]])
 
 ;; FIXME: Make it configurable
 (def block-background-colors
@@ -117,9 +113,9 @@
                                        [:p "Template already exists!"]
                                        :error)
                                       (do
-                                        (editor-handler/set-block-property! block-id "template" title)
+                                        (editor-handler/set-block-property! block-id :template title)
                                         (when (false? including-parent?)
-                                          (editor-handler/set-block-property! block-id "including-parent" false))
+                                          (editor-handler/set-block-property! block-id :including-parent false))
                                         (state/hide-custom-context-menu!)))))))])
       (ui/menu-link
        {:key "Make template"
@@ -133,8 +129,7 @@
   (rum/with-context [[t] i18n/*tongue-context*]
     (when-let [block (db/entity [:block/uuid block-id])]
       (let [properties (:block/properties block)
-            heading (get properties "heading")
-            heading? (= heading "true")]
+            heading? (true? (:heading properties))]
         [:div#custom-context-menu
          [:div.py-1.rounded-md.bg-base-3.shadow-xs
           [:div.flex-row.flex.justify-between.py-4.pl-2
@@ -142,58 +137,26 @@
             (for [color block-background-colors]
               [:a.m-2.shadow-sm
                {:on-click (fn [_e]
-                            (editor-handler/set-block-property! block-id "background_color" color))}
+                            (editor-handler/set-block-property! block-id "background-color" color))}
                [:div.heading-bg {:style {:background-color color}}]])]
            [:a.text-sm
             {:title (t :remove-background)
              :style {:margin-right 14
                      :margin-top 4}
              :on-click (fn [_e]
-                         (editor-handler/remove-block-property! block-id "background_color"))}
+                         (editor-handler/remove-block-property! block-id "background-color"))}
             "Clear"]]
+
           (ui/menu-link
            {:key "Convert heading"
             :on-click (fn [_e]
                         (if heading?
-                          (editor-handler/remove-block-property! block-id "heading")
-                          (editor-handler/set-block-as-a-heading! block-id true)))}
+                          (editor-handler/remove-block-property! block-id :heading)
+                          (editor-handler/set-block-property! block-id :heading true)))}
            (if heading?
              "Convert back to a block"
              "Convert to a heading"))
 
-          (let [empty-properties? (not (text/contains-properties? (:block/content block)))
-                all-hidden? (text/properties-hidden? (:block/properties block))]
-            (when (or empty-properties? all-hidden?)
-              (ui/menu-link
-               {:key "Add a property"
-                :on-click (fn [_e]
-                            (when-let [block-node (util/rec-get-block-node target)]
-                              (let [block-dom-id (gobj/get block-node "id")
-                                    edit-input-id (string/replace block-dom-id "ls-block" "edit-block")
-                                    content (:block/content block)
-                                    content (cond
-                                              empty-properties?
-                                              (text/rejoin-properties content {"" ""} {:remove-blank? false})
-                                              all-hidden?
-                                              (let [idx (string/index-of content "\n:END:")]
-                                                (str
-                                                 (subs content 0 idx)
-                                                 "\n:: "
-                                                 (subs content idx)))
-                                              :else
-                                              content)
-                                    content-without-level (text/remove-level-spaces content (:block/format block))
-                                    pos (string/index-of content-without-level ": \n:END:")]
-                                (editor-handler/edit-block! block
-                                                            pos
-                                                            (:block/format block)
-                                                            edit-input-id
-                                                            (cond-> {:custom-content content}
-                                                              all-hidden?
-                                                              (assoc :custom-properties
-                                                                     (assoc (:block/properties block) "" "")))))))}
-               "Add a property")))
-
           (ui/menu-link
            {:key "Open in sidebar"
             :on-click (fn [_e]
@@ -251,27 +214,6 @@
 ;; TODO: content could be changed
 ;; Also, keyboard bindings should only be activated after
 ;; blocks were already selected.
-
-
-(defn- cut-blocks-and-clear-selections!
-  [copy?]
-  (editor-handler/cut-selection-blocks copy?)
-  (editor-handler/clear-selection! nil))
-
-(rum/defc hidden-selection < rum/reactive
-  (mixins/keyboard-mixin (util/->system-modifier "ctrl+c")
-                         (fn [_]
-                           (editor-handler/copy-selection-blocks)
-                           (editor-handler/clear-selection! nil)))
-  (mixins/keyboard-mixin (util/->system-modifier "ctrl+x")
-                         (fn [] (cut-blocks-and-clear-selections! true)))
-  (mixins/keyboard-mixin "backspace"
-                         (fn [] (cut-blocks-and-clear-selections! false)))
-  (mixins/keyboard-mixin "delete"
-                         (fn [] (cut-blocks-and-clear-selections! false)))
-  []
-  [:div#selection.hidden])
-
 (rum/defc hiccup-content < rum/static
   (mixins/event-mixin
    (fn [state]
@@ -282,8 +224,7 @@
                           (let [blocks (remove nil? blocks)
                                 blocks (remove #(d/has-class? % "dummy") blocks)]
                             (when (seq blocks)
-                              (doseq [block blocks]
-                                (d/add-class! block "selected noselect"))
+                              (util/select-highlight! blocks)
                               ;; TODO: We delay this so the following "click" event won't clear the selections.
                               ;; Needs more thinking.
                               (js/setTimeout #(state/set-selection-blocks! blocks)
@@ -306,8 +247,7 @@
                                               :left (str client-x "px")
                                               :top (str (+ scroll-y client-y) "px")))))
 
-                          (and (state/in-selection-mode?)
-                               (seq (state/get-selection-blocks)))
+                          (state/selection?)
                           (do
                             (util/stop e)
                             (let [client-x (gobj/get e "clientX")
@@ -395,8 +335,6 @@
         selected-blocks (state/sub :selection/blocks)]
     (if hiccup
       [:div
-       (hiccup-content id option)
-       (when (and in-selection-mode? (seq selected-blocks))
-         (hidden-selection))]
+       (hiccup-content id option)]
       (let [format (format/normalize format)]
         (non-hiccup-content id content on-click on-hide config format)))))

+ 78 - 71
src/main/frontend/components/diff.cljs

@@ -6,6 +6,7 @@
             [frontend.handler.file :as file]
             [frontend.handler.notification :as notification]
             [frontend.handler.common :as common-handler]
+            [frontend.handler.file :as file-handler]
             [frontend.state :as state]
             [clojure.string :as string]
             [frontend.db :as db]
@@ -49,9 +50,16 @@
                     :style {:background-color bg-color}}
         value]))])
 
-(rum/defc file < rum/reactive
-  [repo type path contents remote-oid]
-  (let [{:keys [collapse? resolved?]} (util/react (rum/cursor diff-state path))
+(rum/defcs file < rum/reactive
+  {:will-mount (fn [state]
+                 (let [*local-content (atom "")
+                       [repo _ path & _others] (:rum/args state)]
+                   (p/let [content (file-handler/load-file repo path )]
+                     (reset! *local-content content))
+                   (assoc state ::local-content *local-content)))}
+  [state repo type path contents remote-oid]
+  (let [local-content (rum/react (get state ::local-content))
+        {:keys [collapse? resolved?]} (util/react (rum/cursor diff-state path))
         edit? (util/react *edit?)
         delete? (= type "remove")]
     [:div.cp__diff-file
@@ -70,74 +78,73 @@
      (let [content (get contents path)]
        (if (or (and delete? (nil? content))
                content)
-         (let [local-content (db/get-file path)]
-           (if (not= content local-content)
-             (let [local-content (or local-content "")
-                   content (or content "")
-                   diff (medley/indexed (diff/diff local-content content))
-                   diff? (some (fn [[_idx {:keys [added removed]}]]
-                                 (or added removed))
-                               diff)]
-               [:div.pre-line-white-space.p-2 {:class (if collapse? "hidden")
-                                               :style {:overflow "auto"}}
-                (if edit?
-                  [:div.grid.grid-cols-2.gap-1
-                   (diff-cp diff)
-                   (ui/textarea
-                    {:default-value local-content
-                     :on-change (fn [e]
-                                  (reset! *edit-content (util/evalue e)))})]
-                  (diff-cp diff))
-
-                (cond
-                  edit?
-                  [:div.mt-2
-                   (ui/button "Save"
-                              :on-click
-                              (fn []
-                                (reset! *edit? false)
-                                (let [new-content @*edit-content]
-                                  (file/alter-file repo path new-content
-                                                   {:commit? false
-                                                    :re-render-root? true})
-                                  (swap! state/state
-                                         assoc-in [:github/contents repo remote-oid path] new-content)
-                                  (mark-as-resolved path))))]
-
-                  diff?
-                  [:div.mt-2
-                   (ui/button "Use remote"
-                              :on-click
-                              (fn []
-                                ;; overwrite the file
-                                (if delete?
-                                  (file/remove-file! repo path)
-                                  (file/alter-file repo path content
-                                                   {:commit? false
-                                                    :re-render-root? true}))
-                                (mark-as-resolved path))
-                              :background "green")
-
-                   [:span.pl-2.pr-2 "or"]
-
-                   (ui/button "Keep local"
-                              :on-click
-                              (fn []
-                                ;; overwrite the file
-                                (swap! state/state
-                                       assoc-in [:github/contents repo remote-oid path] local-content)
-                                (mark-as-resolved path))
-                              :background "pink")
-
-                   [:span.pl-2.pr-2 "or"]
-
-                   (ui/button "Edit"
-                              :on-click
-                              (fn []
-                                (reset! *edit? true)))]
-
-                  :else
-                  nil)])))
+         (if (not= content local-content)
+           (let [local-content (or local-content "")
+                 content (or content "")
+                 diff (medley/indexed (diff/diff local-content content))
+                 diff? (some (fn [[_idx {:keys [added removed]}]]
+                               (or added removed))
+                             diff)]
+             [:div.pre-line-white-space.p-2 {:class (if collapse? "hidden")
+                                             :style {:overflow "auto"}}
+              (if edit?
+                [:div.grid.grid-cols-2.gap-1
+                 (diff-cp diff)
+                 (ui/textarea
+                  {:default-value local-content
+                   :on-change (fn [e]
+                                (reset! *edit-content (util/evalue e)))})]
+                (diff-cp diff))
+
+              (cond
+                edit?
+                [:div.mt-2
+                 (ui/button "Save"
+                   :on-click
+                   (fn []
+                     (reset! *edit? false)
+                     (let [new-content @*edit-content]
+                       (file/alter-file repo path new-content
+                                        {:commit? false
+                                         :re-render-root? true})
+                       (swap! state/state
+                              assoc-in [:github/contents repo remote-oid path] new-content)
+                       (mark-as-resolved path))))]
+
+                diff?
+                [:div.mt-2
+                 (ui/button "Use remote"
+                   :on-click
+                   (fn []
+                     ;; overwrite the file
+                     (if delete?
+                       (file/remove-file! repo path)
+                       (file/alter-file repo path content
+                                        {:commit? false
+                                         :re-render-root? true}))
+                     (mark-as-resolved path))
+                   :background "green")
+
+                 [:span.pl-2.pr-2 "or"]
+
+                 (ui/button "Keep local"
+                   :on-click
+                   (fn []
+                     ;; overwrite the file
+                     (swap! state/state
+                            assoc-in [:github/contents repo remote-oid path] local-content)
+                     (mark-as-resolved path))
+                   :background "pink")
+
+                 [:span.pl-2.pr-2 "or"]
+
+                 (ui/button "Edit"
+                   :on-click
+                   (fn []
+                     (reset! *edit? true)))]
+
+                :else
+                nil)]))
          [:div "loading..."]))]))
 
 ;; TODO: `n` shortcut for next diff, `p` for previous diff

+ 60 - 53
src/main/frontend/components/editor.cljs

@@ -26,33 +26,35 @@
                      *angle-bracket-caret-pos
                      *matched-block-commands
                      *show-block-commands]]
-            ["/frontend/utils" :as utils]))
+            ["/frontend/utils" :as utils]
+            [frontend.modules.shortcut.core :as shortcut]))
 
 (rum/defc commands < rum/reactive
   [id format]
-  (when (and (util/react *show-commands)
-             @*slash-caret-pos
-             (not (state/sub :editor/show-page-search?))
-             (not (state/sub :editor/show-block-search?))
-             (not (state/sub :editor/show-template-search?))
-             (not (state/sub :editor/show-input))
-             (not (state/sub :editor/show-date-picker?)))
-    (let [matched (util/react *matched-commands)]
-      (ui/auto-complete
-       (map first matched)
-       {:on-chosen (fn [chosen]
-                     (reset! commands/*current-command chosen)
-                     (let [command-steps (get (into {} matched) chosen)
-                           restore-slash? (or
-                                           (contains? #{"Today" "Yesterday" "Tomorrow"} chosen)
-                                           (and
-                                            (not (fn? command-steps))
-                                            (not (contains? (set (map first command-steps)) :editor/input))
-                                            (not (contains? #{"Date Picker" "Template" "Deadline" "Scheduled" "Upload an image"} chosen))))]
-                       (editor-handler/insert-command! id command-steps
-                                                       format
-                                                       {:restore? restore-slash?})))
-        :class     "black"}))))
+  (let [show-commands? (util/react *show-commands)]
+    (when (and show-commands?
+              @*slash-caret-pos
+              (not (state/sub :editor/show-page-search?))
+              (not (state/sub :editor/show-block-search?))
+              (not (state/sub :editor/show-template-search?))
+              (not (state/sub :editor/show-input))
+              (not (state/sub :editor/show-date-picker?)))
+     (let [matched (util/react *matched-commands)]
+       (ui/auto-complete
+        (map first matched)
+        {:on-chosen (fn [chosen]
+                      (reset! commands/*current-command chosen)
+                      (let [command-steps (get (into {} matched) chosen)
+                            restore-slash? (or
+                                            (contains? #{"Today" "Yesterday" "Tomorrow"} chosen)
+                                            (and
+                                             (not (fn? command-steps))
+                                             (not (contains? (set (map first command-steps)) :editor/input))
+                                             (not (contains? #{"Date Picker" "Template" "Deadline" "Scheduled" "Upload an image"} chosen))))]
+                        (editor-handler/insert-command! id command-steps
+                                                        format
+                                                        {:restore? restore-slash?})))
+         :class     "black"})))))
 
 (rum/defc block-commands < rum/reactive
   [id format]
@@ -75,7 +77,7 @@
           input (gdom/getElement id)]
       (when input
         (let [current-pos (:pos (util/get-caret-pos input))
-              edit-content (state/sub [:editor/content id])
+              edit-content (or (state/sub [:editor/content id]) "")
               edit-block (state/sub :editor/block)
               q (or
                  @editor-handler/*selected-text
@@ -114,8 +116,8 @@
         :on-enter    non-exist-block-handler
         :empty-div   [:div.text-gray-500.pl-4.pr-4 "Search for a block"]
         :item-render (fn [{:block/keys [content page uuid] :as item}]
-                       (let [page (or (:page/original-name page)
-                                      (:page/name page))
+                       (let [page (or (:block/original-name page)
+                                      (:block/name page))
                              repo (state/sub :git/current-repo)
                              format (db/get-page-format page)]
 
@@ -172,16 +174,16 @@
   [parent-state parent-id]
   [:div#mobile-editor-toolbar.bg-base-2.fix-ios-fixed-bottom
    [:button.bottom-action
-    {:on-click #(editor-handler/adjust-block-level! parent-state :right)}
+    {:on-click #(editor-handler/indent-outdent parent-state true)}
     svg/indent-block]
    [:button.bottom-action
-    {:on-click #(editor-handler/adjust-block-level! parent-state :left)}
+    {:on-click #(editor-handler/indent-outdent parent-state false)}
     svg/outdent-block]
    [:button.bottom-action
-    {:on-click #(editor-handler/move-up-down % true)}
+    {:on-click (editor-handler/move-up-down true)}
     svg/move-up-block]
    [:button.bottom-action
-    {:on-click #(editor-handler/move-up-down % false)}
+    {:on-click (editor-handler/move-up-down false)}
     svg/move-down-block]
    [:button.bottom-action
     {:on-click #(commands/simple-insert! parent-id "\n" {})}
@@ -337,24 +339,11 @@
         *slash-caret-pos)))])
 
 (defn- set-up-key-down!
-  [repo state input input-id format]
+  [repo state format]
   (mixins/on-key-down
    state
-   {;; enter
-    13 (editor-handler/keydown-enter-handler state input)
-    ;; up
-    38 (editor-handler/keydown-up-down-handler input true)
-    ;; down
-    40 (editor-handler/keydown-up-down-handler input false)
-    ;; left
-    37 (editor-handler/keydown-arrow-handler input :left)
-    ;; right
-    39 (editor-handler/keydown-arrow-handler input :right)
-    ;; backspace
-    8 (editor-handler/keydown-backspace-handler repo input input-id)
-    ;; tab
-    9 (editor-handler/keydown-tab-handler input input-id)}
-   {:not-matched-handler (editor-handler/keydown-not-matched-handler input input-id format)}))
+   {}
+   {:not-matched-handler (editor-handler/keydown-not-matched-handler format)}))
 
 (defn- set-up-key-up!
   [state input input-id search-timeout]
@@ -367,32 +356,50 @@
 
 (defn- setup-key-listener!
   [state]
-  (let [{:keys [id format block]} (get-state state)
+  (let [{:keys [id format block]} (get-state)
         input-id id
         input (gdom/getElement input-id)
         repo (:block/repo block)]
-    (set-up-key-down! repo state input input-id format)
+    (set-up-key-down! repo state format)
     (set-up-key-up! state input input-id search-timeout)))
 
+(defn- get-editor-style
+  [heading-level]
+  (case heading-level
+    1 {:font-size "2em" :font-weight "bold" :margin "0.67em 0"}
+    2 {:font-size "1.5em" :font-weight "bold" :margin "0.75em 0"}
+    3 {:font-size "1.17em" :font-weight "bold" :margin "0.83em 0"}
+    4 {:font-weight "bold" :margin "1.12em 0"}
+    5 {:font-size "0.83em" :font-weight "bold" :margin "1.5em 0"}
+    6 {:font-size "0.75em" :font-weight "bold" :margin "1.67em 0"}
+    nil))
+
 (rum/defcs box < rum/reactive
+  {:init (fn [state]
+           (assoc state ::heading-level (:heading-level (first (:rum/args state)))))
+   :did-mount (fn [state]
+                (state/set-editor-args! (:rum/args state))
+                state)}
   (mixins/event-mixin setup-key-listener!)
+  (shortcut/mixin :shortcut.handler/block-editing-only)
   lifecycle/lifecycle
-  [state {:keys [on-hide dummy? node format block block-parent-id]
+  [state {:keys [on-hide dummy? node format block block-parent-id heading-level]
           :or   {dummy? false}
           :as   option} id config]
-  (let [content (state/get-edit-content)]
+  (let [content (state/get-edit-content)
+        heading-level (get state ::heading-level)]
     [:div.editor-inner {:class (if block "block-editor" "non-block-editor")}
      (when config/mobile? (mobile-bar state id))
      (ui/ls-textarea
       {:id                id
-       :class             "mousetrap"
        :cacheMeasurements true
        :default-value     (or content "")
        :minRows           (if (state/enable-grammarly?) 2 1)
        :on-click          (editor-handler/editor-on-click! id)
        :on-change         (editor-handler/editor-on-change! block id search-timeout)
        :on-paste          (editor-handler/editor-on-paste! id)
-       :auto-focus        false})
+       :auto-focus        false
+       :style             (get-editor-style heading-level)})
 
      ;; TODO: how to render the transitions asynchronously?
      (transition-cp

+ 1 - 0
src/main/frontend/components/editor.css

@@ -17,6 +17,7 @@
 }
 
 .editor-wrapper {
+  width: 100%;
   margin: 0 auto;
 }
 

+ 12 - 3
src/main/frontend/components/export.cljs

@@ -21,13 +21,18 @@
         [:li.mb-4
          [:a.font-medium {:on-click #(export/export-repo-as-markdown! current-repo)}
           (t :export-markdown)]]
+        [:li.mb-4
+         [:a.font-medium {:on-click #(export/convert-repo-markdown-v2! current-repo)}
+          (t :convert-markdown)]]
         [:li.mb-4
          [:a.font-medium {:on-click #(export/export-repo-as-edn! current-repo)}
           (t :export-edn)]]]
        [:a#download-as-edn.hidden]
        [:a#download-as-html.hidden]
        [:a#download-as-zip.hidden]
-       [:a#export-as-markdown.hidden]])))
+       [:a#export-as-markdown.hidden]
+       [:a#convert-markdown-to-unordered-list-or-heading.hidden]
+       ])))
 
 
 (rum/defc export-page
@@ -40,5 +45,9 @@
          [:ul.mr-1
           [:li.mb-4
            [:a.font-medium {:on-click #(export/export-page-as-markdown! page)}
-            (t :export-markdown)]]]
-         [:a#export-page-as-markdown.hidden]]))))
+            (t :export-markdown)]]
+          [:li.mb-4
+           [:a.font-medium {:on-click #(export/convert-page-markdown-unordered-list-or-heading! page)}
+            (t :convert-markdown)]]]
+         [:a#export-page-as-markdown.hidden]
+         [:a#convert-markdown-to-unordered-list-or-heading.hidden]]))))

+ 2 - 6
src/main/frontend/components/file.cljs

@@ -1,7 +1,6 @@
 (ns frontend.components.file
   (:require [rum.core :as rum]
             [frontend.util :as util]
-            [frontend.handler.project :as project]
             [frontend.handler.export :as export-handler]
             [frontend.config :as config]
             [frontend.state :as state]
@@ -82,7 +81,7 @@
                                   :href (rfe/href :page {:name page})
                                   :on-click (fn [e]
                                               (when (gobj/get e "shiftKey")
-                                                (when-let [page (db/entity [:page/name (string/lower-case page)])]
+                                                (when-let [page (db/entity [:block/name (string/lower-case page)])]
                                                   (state/sidebar-add-block!
                                                    (state/get-current-repo)
                                                    (:db/id page)
@@ -97,9 +96,6 @@
                                 :display "inline-block"}})
           [:span.ml-1 "Please don't remove the page's title property (you can still modify it)."]])
 
-       (when (and config? (state/logged?))
-         [:a.mb-8.block {:on-click (fn [_e] (project/sync-project-settings!))}
-          (tongue :project/sync-settings)])
        (cond
          ;; image type
          (and format (contains? (config/img-formats) format))
@@ -119,7 +115,7 @@
                  mode (util/get-file-ext path)
                  mode (if (contains? #{"edn" "clj" "cljc" "cljs" "clojure"} mode) "text/x-clojure" mode)]
              (lazy-editor/editor {:file? true
-                                  :file-path path} path {:data-lang mode} content nil)))
+                                  :file-path path} path {:data-lang mode} content {})))
 
          :else
          [:div (tongue :file/format-not-supported (name format))])])))

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

@@ -16,7 +16,6 @@
             [frontend.components.search :as search]
             [frontend.components.export :as export]
             [frontend.components.right-sidebar :as sidebar]
-            [frontend.handler.project :as project-handler]
             [frontend.handler.page :as page-handler]
             [frontend.handler.web.nfs :as nfs]
             [goog.dom :as gdom]
@@ -80,7 +79,7 @@
         (svg/horizontal-dots nil)])
      (->>
       [(when-not (util/mobile?)
-         {:title (t :help/toggle-right-sidebar)
+         {:title (t :shortcut.ui/toggle-right-sidebar)
           :options {:on-click state/toggle-sidebar-open?!}})
 
        (when current-repo
@@ -110,22 +109,9 @@
           :options {:href (rfe/href :all-journals)}
           :icon svg/calendar-sm})
 
-       (when (project-handler/get-current-project current-repo projects)
-         {:title (t :my-publishing)
-          :options {:href (rfe/href :my-publishing)}})
-
-       (when-let [project (and current-repo
-                               (project-handler/get-current-project current-repo projects))]
-         (let [link (str config/website "/" project)]
-           {:title (str (t :go-to) "/" project)
-            :options {:href link
-                      :target "_blank"}
-            :icon svg/external-link}))
-
-       (when current-repo
-         {:title (t :settings)
-          :options {:on-click #(ui-handler/toggle-settings-modal!)}
-          :icon svg/settings-sm})
+       {:title (t :settings)
+        :options {:on-click #(ui-handler/toggle-settings-modal!)}
+        :icon svg/settings-sm}
 
        (when current-repo
          {:title (t :export)

+ 9 - 8
src/main/frontend/components/journal.cljs

@@ -46,9 +46,7 @@
                      false)))
                 state)}
   [blocks page document-mode?]
-  (let [start-level (or (:block/level (first blocks)) 1)
-        config {:id page
-                :start-level 2
+  (let [config {:id page
                 :editor-box editor/box
                 :document/mode? document-mode?}]
     (content/content
@@ -61,7 +59,8 @@
   (let [raw-blocks (db/get-page-blocks repo page)
         document-mode? (state/sub :document/mode?)
         blocks (->>
-                (block-handler/with-dummy-block raw-blocks format nil {:journal? true})
+                (block-handler/with-dummy-block raw-blocks format nil {:journal? true
+                                                                       :page-name page})
                 (db/with-block-refs-count repo))]
     (blocks-inner blocks page document-mode?)))
 
@@ -76,9 +75,9 @@
                     (not (config/local-db? repo))
                     (not config/publishing?)
                     today?)
-        page-entity (db/pull [:page/name (string/lower-case title)])
-        data-page-tags (when (seq (:page/tags page-entity))
-                         (let [page-names (model/get-page-names-by-ids (map :db/id (:page/tags page)))]
+        page-entity (db/pull [:block/name (string/lower-case title)])
+        data-page-tags (when (seq (:block/tags page-entity))
+                         (let [page-names (model/get-page-names-by-ids (map :db/id (:block/tags page)))]
                            (text/build-data-value page-names)))]
     [:div.flex-1.journal.page (cond->
                                {:class (if intro? "intro" "")}
@@ -106,7 +105,9 @@
 
      (page/today-queries repo today? false)
 
-     (reference/references title false)
+     (rum/with-key
+       (reference/references title false)
+       (str title "-refs"))
 
      (when intro? (onboarding/intro))]))
 

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

@@ -13,8 +13,8 @@
                             (fn []
                               (reset! loaded? true)))
                  state)}
-  [config id attr code pos_meta]
+  [config id attr code options]
   (let [loaded? (rum/react loaded?)]
     (if loaded?
-      (@lazy-editor config id attr code pos_meta)
+      (@lazy-editor config id attr code options)
       (ui/loading "CodeMirror"))))

+ 16 - 68
src/main/frontend/components/onboarding.cljs

@@ -1,10 +1,12 @@
 (ns frontend.components.onboarding
-  (:require [rum.core :as rum]
+  (:require [frontend.components.shortcut :as shortcut]
             [frontend.components.svg :as svg]
-            [frontend.extensions.latex :as latex]
-            [frontend.extensions.highlight :as highlight]
             [frontend.context.i18n :as i18n]
-            [frontend.util :as util]))
+            [frontend.extensions.highlight :as highlight]
+            [frontend.extensions.latex :as latex]
+            [frontend.handler.route :as route-handler]
+            [frontend.ui :as ui]
+            [rum.core :as rum]))
 
 (rum/defc intro
   []
@@ -216,70 +218,16 @@
          svg/discord]]]
       [:li
        (t :help/shortcuts)
-       [:table
-        [:thead
-         [:tr
-          [:th [:b (t :help/shortcuts-triggers)]]
-          [:th (t :help/shortcut)]]]
-        [:tbody
-         [:tr [:td (t :help/slash-autocomplete)] [:td "/"]]
-         [:tr [:td (t :help/block-content-autocomplete)] [:td "<"]]
-         [:tr [:td (t :help/reference-autocomplete)] [:td "[[]]"]]
-         [:tr [:td (t :help/block-reference)] [:td "(())"]]]]
-       [:table
-        [:thead
-         [:tr
-          [:th [:span [:b (t :help/key-commands)]
-                (t :help/working-with-lists)]]
-          [:th (t :help/shortcut)]]]
-        [:tbody
-         [:tr [:td (t :help/indent-block-tab)] [:td "Tab"]]
-         [:tr [:td (t :help/unindent-block)] [:td "Shift-Tab"]]
-         [:tr [:td (t :help/move-block-up)] [:td (util/->platform-shortcut "Alt-Shift-Up")]]
-         [:tr [:td (t :help/move-block-down)] [:td (util/->platform-shortcut "Alt-Shift-Down")]]
-         [:tr [:td (t :help/create-new-block)] [:td "Enter"]]
-         [:tr [:td (t :help/new-line-in-block)] [:td "Shift-Enter"]]
-         [:tr [:td (t :undo)] [:td (util/->platform-shortcut "Ctrl-z")]]
-         [:tr [:td (t :redo)] [:td (util/->platform-shortcut "Ctrl-y")]]
-         [:tr [:td (t :help/zoom-in)] [:td (util/->platform-shortcut (if util/mac? "Cmd-." "Alt-Right"))]]
-         [:tr [:td (t :help/zoom-out)] [:td (util/->platform-shortcut (if util/mac? "Cmd-," "Alt-left"))]]
-         [:tr [:td (t :help/follow-link-under-cursor)] [:td (util/->platform-shortcut "Ctrl-o")]]
-         [:tr [:td (t :help/open-link-in-sidebar)] [:td (util/->platform-shortcut "Ctrl-shift-o")]]
-         [:tr [:td (t :expand)] [:td (util/->platform-shortcut "Ctrl-Down")]]
-         [:tr [:td (t :collapse)] [:td (util/->platform-shortcut "Ctrl-Up")]]
-         [:tr [:td (t :select-block-above)] [:td "Shift-Up"]]
-         [:tr [:td (t :select-block-below)] [:td "Shift-Down"]]
-         [:tr [:td (t :select-all-blocks)] [:td (util/->platform-shortcut "Ctrl-Shift-a")]]]]
-       [:table
-        [:thead
-         [:tr
-          [:th [:b (t :general)]]
-          [:th (t :help/shortcut)]]]
-        [:tbody
-         [:tr [:td (t :help/toggle)] [:td "?"]]
-         [:tr [:td (t :help/git-commit-message)] [:td "c"]]
-         [:tr [:td (t :help/full-text-search)] [:td (util/->platform-shortcut "Ctrl-u")]]
-         [:tr [:td (t :help/page-search)] [:td (util/->platform-shortcut "Ctrl-Shift-u")]]
-         [:tr [:td (t :help/open-link-in-sidebar)] [:td "Shift-Click"]]
-         [:tr [:td (t :help/context-menu)] [:td "Right Click"]]
-         [:tr [:td (t :help/fold-unfold)] [:td "Tab"]]
-         [:tr [:td (t :help/toggle-contents)] [:td "t c"]]
-         [:tr [:td (t :help/toggle-doc-mode)] [:td "t d"]]
-         [:tr [:td (t :help/toggle-theme)] [:td "t t"]]
-         [:tr [:td (t :help/toggle-right-sidebar)] [:td "t r"]]
-         [:tr [:td (t :help/toggle-settings)] [:td "t s"]]
-         [:tr [:td (t :help/toggle-insert-new-block)] [:td "t e"]]
-         [:tr [:td (t :help/jump-to-journals)] [:td (if util/mac? "Cmd-j" "Alt-j")]]]]
-       [:table
-        [:thead
-         [:tr
-          [:th [:b (t :formatting)]]
-          [:th (t :help/shortcut)]]]
-        [:tbody
-         [:tr [:td (t :bold)] [:td (util/->platform-shortcut "Ctrl-b")]]
-         [:tr [:td (t :italics)] [:td (util/->platform-shortcut "Ctrl-i")]]
-         [:tr [:td (t :html-link)] [:td (util/->platform-shortcut "Ctrl-k")]]
-         [:tr [:td (t :highlight)] [:td (util/->platform-shortcut "Ctrl-h")]]]]]
+       [:br]
+       (ui/button
+        "Learn more"
+        :on-click
+        (fn []
+          (route-handler/redirect! {:to :shortcut})))
+       (shortcut/trigger-table)
+       (shortcut/shortcut-table :shortcut.category/basics)
+       (shortcut/shortcut-table :shortcut.category/block-editing)
+       (shortcut/shortcut-table :shortcut.category/formatting)]
 
       [:li
        (t :help/markdown-syntax)

+ 221 - 253
src/main/frontend/components/page.cljs

@@ -1,6 +1,7 @@
 (ns frontend.components.page
   (:require [rum.core :as rum]
             [frontend.util :as util :refer-macros [profile]]
+            [frontend.util.marker :as marker]
             [frontend.tools.html-export :as html-export]
             [frontend.handler.file :as file]
             [frontend.handler.page :as page-handler]
@@ -20,7 +21,6 @@
             [frontend.extensions.graph-2d :as graph-2d]
             [frontend.ui :as ui]
             [frontend.components.content :as content]
-            [frontend.components.project :as project]
             [frontend.config :as config]
             [frontend.db :as db]
             [frontend.db.model :as model]
@@ -39,6 +39,7 @@
             [frontend.context.i18n :as i18n]
             [reitit.frontend.easy :as rfe]
             [frontend.text :as text]
+            [frontend.modules.shortcut.core :as shortcut]
             [frontend.handler.block :as block-handler]))
 
 (defn- get-page-name
@@ -55,70 +56,52 @@
         (page-handler/add-page-to-recent! repo page-original-name)
         (db/get-page-blocks repo page-name)))))
 
+(defn- open-first-block!
+  [state]
+  (let [blocks (nth (:rum/args state) 1)
+        block (first blocks)]
+    (when (:block/dummy? block)
+      (editor-handler/edit-block! block :max (:block/format block) (:block/uuid block))))
+  state)
+(rum/defc page-blocks-inner <
+  {:did-mount open-first-block!
+   :did-update open-first-block!}
+  [page-name page-blocks hiccup sidebar?]
+  [:div.page-blocks-inner
+   (rum/with-key
+     (content/content page-name
+                      {:hiccup   hiccup
+                       :sidebar? sidebar?})
+     (str page-name "-hiccup"))])
+
+(declare page)
+
 (rum/defc page-blocks-cp < rum/reactive
   db-mixins/query
-  [repo page file-path page-name page-original-name encoded-page-name sidebar? journal? block? block-id format]
+  [repo page-e file-path page-name page-original-name encoded-page-name sidebar? journal? block? block-id format]
   (let [raw-page-blocks (get-blocks repo page-name page-original-name block? block-id)
-        grouped-blocks-by-file (into {} (for [[k v] (db-utils/group-by-file raw-page-blocks)]
-                                          [(:file/path (db-utils/entity (:db/id k))) v]))
-        raw-page-blocks (get grouped-blocks-by-file file-path raw-page-blocks)
         page-blocks (block-handler/with-dummy-block raw-page-blocks format
                       (if (empty? raw-page-blocks)
-                        (let [content (db/get-file repo file-path)]
-                          {:block/page {:db/id (:db/id page)}
-                           :block/file {:db/id (:db/id (:page/file page))}
-                           :block/meta
-                           (let [file-id (:db/id (:page/file page))]
-                             {:start-pos (utf8/length (utf8/encode content))
-                              :end-pos nil})}))
+                        {:block/page {:db/id (:db/id page-e)}
+                         :block/file {:db/id (:db/id (:block/file page-e))}})
                       {:journal? journal?
                        :page-name page-name})
-        start-level (or (:block/level (first page-blocks)) 1)
         hiccup-config {:id (if block? (str block-id) page-name)
-                       :start-level start-level
                        :sidebar? sidebar?
                        :block? block?
-                       :editor-box editor/box}
+                       :editor-box editor/box
+                       :page page}
         hiccup-config (common-handler/config-with-document-mode hiccup-config)
         hiccup (block/->hiccup page-blocks hiccup-config {})]
-    [:div.page-blocks-inner
-     (when (and (seq grouped-blocks-by-file)
-                (> (count grouped-blocks-by-file) 1))
-       (ui/admonition
-        :warning
-        [:div.text-sm
-         [:p.font-medium "Those pages have the same title, you might want to only keep one file."]
-         [:ol
-          (for [[file-path blocks] (into (sorted-map) grouped-blocks-by-file)]
-            [:li [:a {:key file-path
-                      :href (rfe/href :file {:path file-path})} file-path]])]]))
-
-     (rum/with-key
-       (content/content page-name
-                        {:hiccup   hiccup
-                         :sidebar? sidebar?})
-       (str encoded-page-name "-hiccup"))]))
+    (page-blocks-inner page-name page-blocks hiccup sidebar?)))
 
 (defn contents-page
-  [{:page/keys [name original-name file] :as contents}]
+  [{:block/keys [name original-name file] :as contents}]
   (when-let [repo (state/get-current-repo)]
     (let [format (db/get-page-format name)
           file-path (:file/path file)]
       (page-blocks-cp repo contents file-path name original-name name true false false nil format))))
 
-(defn presentation
-  [repo page]
-  [:a.opacity-30.hover:opacity-100.page-op
-   {:title "Presentation mode (Powered by Reveal.js)"
-    :style {:margin-top -2}
-    :on-click (fn []
-                (state/sidebar-add-block!
-                 repo
-                 (:db/id page)
-                 :page-presentation
-                 {:page page}))}
-   svg/slideshow])
-
 (rum/defc today-queries < rum/reactive
   [repo today? sidebar?]
   (when (and today? (not sidebar?))
@@ -127,9 +110,9 @@
         [:div#today-queries.mt-10
          (for [{:keys [title] :as query} queries]
            (rum/with-key
-             (block/custom-query {:start-level 2
-                                  :attr {:class "mt-10"}
-                                  :editor-box editor/box} query)
+             (block/custom-query {:attr {:class "mt-10"}
+                                  :editor-box editor/box
+                                  :page page} query)
              (str repo "-custom-query-" (:query query))))]))))
 
 (defn- delete-page!
@@ -174,6 +157,7 @@
           (t :cancel)]]]])))
 
 (rum/defcs rename-page-dialog-inner <
+  (shortcut/disable-all-shortcuts)
   (rum/local "" ::input)
   [state title page-name close-fn]
   (let [input (get state ::input)]
@@ -226,214 +210,198 @@
 
 ;; A page is just a logical block
 (rum/defcs page < rum/reactive
+  #_
   {:did-mount (fn [state]
                 (ui-handler/scroll-and-highlight! state)
                 state)
    :did-update (fn [state]
                  (ui-handler/scroll-and-highlight! state)
                  state)}
-  [state {:keys [repo] :as option}]
-  (let [current-repo (state/sub :git/current-repo)
-        repo (or repo current-repo)
-        path-page-name (or (get-page-name state)
-                           (state/get-current-page))
-        page-name (string/lower-case path-page-name)
-        marker-page? (util/marker? page-name)
-        priority-page? (contains? #{"a" "b" "c"} page-name)
-        block? (util/uuid-string? page-name)
-        block-id (and block? (uuid page-name))
-        format (let [page (if block-id
-                            (:page/name (:block/page (db/entity [:block/uuid block-id])))
-                            page-name)]
-                 (db/get-page-format page))
-        journal? (db/journal-page? page-name)
-        sidebar? (:sidebar? option)]
-    (rum/with-context [[t] i18n/*tongue-context*]
-      (cond
-        priority-page?
-        [:div.page
-         [:h1.title
-          (t :page/priority (string/upper-case page-name))]
-         [:div.ml-2
-          (reference/references page-name false true)]]
-
-        marker-page?
-        [:div.page
-         [:h1.title
-          (string/upper-case page-name)]
-         [:div.ml-2
-          (reference/references page-name true false)]]
-
-        :else
-        (let [route-page-name page-name
-              page (if block?
-                     (->> (:db/id (:block/page (db/entity repo [:block/uuid block-id])))
-                          (db/entity repo))
-                     (db/entity repo [:page/name page-name]))
-              page (if page page (do
-                                   (db/transact! repo [{:page/name page-name
-                                                        :page/original-name path-page-name}])
-                                   (db/entity repo [:page/name page-name])))
-              {:keys [title] :as properties} (:page/properties page)
-              page-name (:page/name page)
-              page-original-name (:page/original-name page)
-              title (or title page-original-name page-name)
-              file (:page/file page)
-              file-path (and (:db/id file) (:file/path (db/entity repo (:db/id file))))
-              today? (and
-                      journal?
-                      (= page-name (string/lower-case (date/journal-name))))
-              developer-mode? (state/sub [:ui/developer-mode?])
-              published? (= "true" (:published properties))
-              public? (= "true" (:public properties))]
-          [:div.flex-1.page.relative (if (seq (:page/tags page))
-                                       (let [page-names (model/get-page-names-by-ids (map :db/id (:page/tags page)))]
-                                         {:data-page-tags (text/build-data-value page-names)})
-                                       {})
-           [:div.relative
-            (when (and (not block?)
-                       (not sidebar?)
-                       (not config/publishing?))
-
-              (let [contents? (= (string/lower-case (str page-name)) "contents")
-                    links (->>
-                           [(when-not contents?
-                              {:title (t :page/add-to-contents)
-                               :options {:on-click (fn [] (page-handler/handle-add-page-to-contents! page-original-name))}})
-
-                            (when-not contents?
-                              {:title (t :page/rename)
-                               :options {:on-click #(state/set-modal! (rename-page-dialog title page-name))}})
-
-                            (when (and file-path (util/electron?))
-                              [{:title   (t :page/open-in-finder)
-                                :options {:on-click #(js/window.apis.showItemInFolder file-path)}}
-                               {:title (t :page/open-with-default-app)
-                                :options {:on-click #(js/window.apis.openPath file-path)}}])
-
-                            (when-not contents?
-                              {:title (t :page/delete)
-                               :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}})
-
-                            (when (state/get-current-page)
-                              {:title (t :export)
-                               :options {:on-click #(state/set-modal! export/export-page)}})
-
-                            (when (util/electron?)
-                              {:title  (t (if public? :page/make-private :page/make-public))
-                               :options {:on-click
-                                         (fn []
-                                           (page-handler/update-public-attribute!
-                                            page-name
-                                            (if public? false true))
-                                           (state/close-modal!))}})
-
-                            (when file
-                              {:title (t :page/re-index)
-                               :options {:on-click (fn []
-                                                     (file/re-index! file))}})
-
-                            (when developer-mode?
-                              {:title "(Dev) Show page data"
-                               :options {:on-click (fn []
-                                                     (let [page-data (with-out-str (pprint/pprint (db/pull (:db/id page))))]
-                                                       (println page-data)
-                                                       (notification/show!
-                                                        [:div
-                                                         [:pre.code page-data]
-                                                         [:br]
-                                                         (ui/button "Copy to clipboard"
-                                                                    :on-click #(.writeText js/navigator.clipboard page-data))]
-                                                        :success
-                                                        false)))}})]
-                           (flatten)
-                           (remove nil?))]
-                [:div {:style {:position "absolute"
-                               :right 0
-                               :top 20}}
-                 [:div.flex.flex-row
-                  [:a.opacity-30.hover:opacity-100.page-op.mr-2
-                   {:title "Search in current page"
-                    :on-click #(route-handler/go-to-search! :page)}
-                   svg/search]
-                  (when (not config/mobile?)
-                    (presentation repo page))
-                  (when (seq links)
-                    (ui/dropdown-with-links
-                     (fn [{:keys [toggle-fn]}]
-                       [:a.opacity-30.hover:opacity-100
-                        {:title "More options"
-                         :on-click toggle-fn}
-                        (svg/vertical-dots {:class (util/hiccup->class "opacity-30.hover:opacity-100.h-5.w-5")})])
-                     links
-                     {:modal-class (util/hiccup->class
-                                    "origin-top-right.absolute.right-0.top-10.mt-2.rounded-md.shadow-lg.whitespace-no-wrap.dropdown-overflow-auto.page-drop-options")
-                      :z-index 1}))]]))
-            (when (and (not sidebar?)
-                       (not block?))
-              [:a {:on-click (fn [e]
-                               (.preventDefault e)
-                               (when (gobj/get e "shiftKey")
-                                 (when-let [page (db/pull repo '[*] [:page/name page-name])]
-                                   (state/sidebar-add-block!
-                                    repo
-                                    (:db/id page)
-                                    :page
-                                    {:page page}))))}
-               [:h1.title {:style {:margin-left -2}}
-                (if page-original-name
-                  (if (and (string/includes? page-original-name "[[")
-                           (string/includes? page-original-name "]]"))
-                    (let [ast (mldoc/->edn page-original-name (mldoc/default-config format))]
-                      (block/markup-element-cp {} (ffirst ast)))
-                    page-original-name)
-                  (or
-                   page-name
-                   path-page-name))]])
-            [:div
-             [:div.content
-              (when (and file-path
-                         (not sidebar?)
-                         (not block?)
-                         (not (state/hide-file?))
-                         (not config/publishing?))
-                [:div.text-sm.ml-1.mb-4.flex-1.inline-flex
-                 {:key "page-file"}
-                 [:span.opacity-50 {:style {:margin-top 2}} (t :file/file)]
-                 [:a.bg-base-2.px-1.ml-1.mr-3 {:style {:border-radius 4
-                                                       :word-break    "break-word"}
-                                               :href  (rfe/href :file {:path file-path})}
-                  file-path]])]
-
-             (when (and repo (not block?))
-               (let [alias (db/get-page-alias-names repo page-name)]
-                 (when (seq alias)
-                   [:div.text-sm.ml-1.mb-4 {:key "page-file"}
-                    [:span.opacity-50 "Alias: "]
-                    (for [item alias]
-                      [:a.ml-1.mr-1 {:href (rfe/href :page {:name item})}
-                       item])])))
-
-             (when (and block? (not sidebar?))
-               (let [config {:id "block-parent"
-                             :block? true}]
-                 [:div.mb-4
-                  (block/block-parents config repo block-id format)]))
-
-             ;; blocks
-             (page-blocks-cp repo page file-path page-name page-original-name page-name sidebar? journal? block? block-id format)]]
-
-           (when-not block?
-             (today-queries repo today? sidebar?))
-
-           (tagged-pages repo page-name)
-
-           ;; referenced blocks
-           [:div {:key "page-references"}
-            (reference/references route-page-name false)]
-
-           [:div {:key "page-unlinked-references"}
-            (reference/unlinked-references route-page-name)]])))))
+  [state {:keys [repo page-name preview?] :as option}]
+  (when-let [path-page-name (or page-name
+                                (get-page-name state)
+                                (state/get-current-page))]
+    (let [current-repo (state/sub :git/current-repo)
+         repo (or repo current-repo)
+         page-name (string/lower-case path-page-name)
+         block? (util/uuid-string? page-name)
+         block-id (and block? (uuid page-name))
+         format (let [page (if block-id
+                             (:block/name (:block/page (db/entity [:block/uuid block-id])))
+                             page-name)]
+                  (db/get-page-format page))
+         journal? (db/journal-page? page-name)
+         sidebar? (:sidebar? option)]
+     (rum/with-context [[t] i18n/*tongue-context*]
+       (let [route-page-name path-page-name
+             page (if block?
+                    (->> (:db/id (:block/page (db/entity repo [:block/uuid block-id])))
+                         (db/entity repo))
+                    (db/entity repo [:block/name page-name]))
+             ;; TODO: replace page with frontend.format.block/page->map
+             page (if page page (do
+                                  (db/transact! repo [{:block/name page-name
+                                                       :block/original-name path-page-name
+                                                       :block/uuid (db/new-block-id)}])
+                                  (db/entity repo [:block/name page-name])))
+             {:keys [title] :as properties} (:block/properties page)
+             page-name (:block/name page)
+             page-original-name (:block/original-name page)
+             title (or title page-original-name page-name)
+             file (:block/file page)
+             file-path (and (:db/id file) (:file/path (db/entity repo (:db/id file))))
+             today? (and
+                     journal?
+                     (= page-name (string/lower-case (date/journal-name))))
+             developer-mode? (state/sub [:ui/developer-mode?])
+             public? (true? (:public properties))]
+         [:div.flex-1.page.relative (if (seq (:block/tags page))
+                                      (let [page-names (model/get-page-names-by-ids (map :db/id (:block/tags page)))]
+                                        {:data-page-tags (text/build-data-value page-names)})
+                                      {})
+          [:div.relative
+           (when (and (not sidebar?)
+                      (not block?))
+             [:div.flex.flex-row.space-between
+              [:div.flex-1.flex-row
+               [:a {:on-click (fn [e]
+                                (.preventDefault e)
+                                (when (gobj/get e "shiftKey")
+                                  (when-let [page (db/pull repo '[*] [:block/name page-name])]
+                                    (state/sidebar-add-block!
+                                     repo
+                                     (:db/id page)
+                                     :page
+                                     {:page page}))))}
+                [:h1.title {:style {:margin-left -2}}
+                 (if page-original-name
+                   (if (and (string/includes? page-original-name "[[")
+                            (string/includes? page-original-name "]]"))
+                     (let [ast (mldoc/->edn page-original-name (mldoc/default-config format))]
+                       (block/markup-element-cp {} (ffirst ast)))
+                     page-original-name)
+                   (or
+                    page-name
+                    path-page-name))]]]
+              (when (not config/publishing?)
+                (let [contents? (= (string/lower-case (str page-name)) "contents")
+                      links (fn [] (->>
+                                   [(when-not contents?
+                                      {:title   (t :page/add-to-contents)
+                                       :options {:on-click (fn [] (page-handler/handle-add-page-to-contents! page-original-name))}})
+
+                                    {:title "Go to presentation mode"
+                                     :options {:on-click (fn []
+                                                           (state/sidebar-add-block!
+                                                            repo
+                                                            (:db/id page)
+                                                            :page-presentation
+                                                            {:page page}))}}
+                                    (when-not contents?
+                                      {:title   (t :page/rename)
+                                       :options {:on-click #(state/set-modal! (rename-page-dialog title page-name))}})
+
+                                    (when-let [file-path (and (util/electron?) (page-handler/get-page-file-path))]
+                                      [{:title   (t :page/open-in-finder)
+                                        :options {:on-click #(js/window.apis.showItemInFolder file-path)}}
+                                       {:title   (t :page/open-with-default-app)
+                                        :options {:on-click #(js/window.apis.openPath file-path)}}])
+
+                                    (when-not contents?
+                                      {:title   (t :page/delete)
+                                       :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}})
+
+                                    (when (state/get-current-page)
+                                      {:title   (t :export)
+                                       :options {:on-click #(state/set-modal! export/export-page)}})
+
+                                    (when (util/electron?)
+                                      {:title   (t (if public? :page/make-private :page/make-public))
+                                       :options {:on-click
+                                                 (fn []
+                                                   (page-handler/update-public-attribute!
+                                                    page-name
+                                                    (if public? false true))
+                                                   (state/close-modal!))}})
+
+                                    (when developer-mode?
+                                      {:title   "(Dev) Show page data"
+                                       :options {:on-click (fn []
+                                                             (let [page-data (with-out-str (pprint/pprint (db/pull (:db/id page))))]
+                                                               (println page-data)
+                                                               (notification/show!
+                                                                [:div
+                                                                 [:pre.code page-data]
+                                                                 [:br]
+                                                                 (ui/button "Copy to clipboard"
+                                                                   :on-click #(.writeText js/navigator.clipboard page-data))]
+                                                                :success
+                                                                false)))}})]
+                                   (flatten)
+                                   (remove nil?)))]
+                  [:div.flex.flex-row
+                   [:a.opacity-30.hover:opacity-100.page-op.mr-1
+                    {:title "Search in current page"
+                     :on-click #(route-handler/go-to-search! :page)}
+                    svg/search]
+                   (ui/dropdown-with-links
+                    (fn [{:keys [toggle-fn]}]
+                      [:a.cp__vertial-menu-button
+                       {:title    "More options"
+                        :on-click toggle-fn}
+                       (svg/vertical-dots nil)])
+                    links
+                    {:modal-class (util/hiccup->class
+                                   "origin-top-right.absolute.right-0.top-10.mt-2.rounded-md.shadow-lg.whitespace-no-wrap.dropdown-overflow-auto.page-drop-options")
+                     :z-index     1})]))])
+           [:div
+            ;; [:div.content
+            ;;  (when (and file-path
+            ;;             (not sidebar?)
+            ;;             (not block?)
+            ;;             (not (state/hide-file?))
+            ;;             (not config/publishing?))
+            ;;    [:div.text-sm.ml-1.mb-4.flex-1.inline-flex
+            ;;     {:key "page-file"}
+            ;;     [:span.opacity-50 {:style {:margin-top 2}} (t :file/file)]
+            ;;     [:a.bg-base-2.px-1.ml-1.mr-3 {:style {:border-radius 4
+            ;;                                           :word-break    "break-word"}
+            ;;                                   :href  (rfe/href :file {:path file-path})}
+            ;;      file-path]])]
+
+            (when (and repo (not block?))
+              (let [alias (db/get-page-alias-names repo page-name)]
+                (when (seq alias)
+                  [:div.text-sm.ml-1.mb-4 {:key "page-file"}
+                   [:span.opacity-50 "Alias: "]
+                   (for [item alias]
+                     [:a.ml-1.mr-1 {:href (rfe/href :page {:name item})}
+                      item])])))
+
+            (when (and block? (not sidebar?))
+              (let [config {:id "block-parent"
+                            :block? true}]
+                [:div.mb-4
+                 (block/block-parents config repo block-id format)]))
+
+            ;; blocks
+            (page-blocks-cp repo page file-path page-name page-original-name page-name sidebar? journal? block? block-id format)]]
+
+          (when-not block?
+            (today-queries repo today? sidebar?))
+
+          (tagged-pages repo page-name)
+
+          ;; referenced blocks
+          [:div {:key "page-references"}
+           (rum/with-key
+             (reference/references route-page-name false)
+             (str route-page-name "-refs"))]
+
+          ;; TODO: or we can lazy load them
+          (when-not sidebar?
+            [:div {:key "page-unlinked-references"}
+             (reference/unlinked-references route-page-name)])])))))
 
 (defonce layout (atom [js/window.outerWidth js/window.outerHeight]))
 
@@ -490,14 +458,14 @@
            [:table.table-auto
             [:thead
              [:tr
-              [:th (t :page/name)]
+              [:th (t :block/name)]
               [:th (t :file/last-modified-at)]]]
             [:tbody
              (for [page pages]
                [:tr {:key page}
                 [:td [:a {:on-click (fn [e]
                                       (let [repo (state/get-current-repo)
-                                            page (db/pull repo '[*] [:page/name (string/lower-case page)])]
+                                            page (db/pull repo '[*] [:block/name (string/lower-case page)])]
                                         (when (gobj/get e "shiftKey")
                                           (state/sidebar-add-block!
                                            repo

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

@@ -28,3 +28,17 @@
     }
   }
 }
+
+.cp__vertial-menu-button {
+    opacity: 0.3;
+    display: block;
+}
+
+.cp__vertial-menu-button:hover {
+    opacity: 1;
+}
+
+.cp__vertial-menu-button svg {
+    width: 20px;
+    height: 20px;
+}

+ 0 - 51
src/main/frontend/components/project.cljs

@@ -1,51 +0,0 @@
-(ns frontend.components.project
-  (:require [rum.core :as rum]
-            [frontend.util :as util :refer-macros [profile]]
-            [frontend.handler.project :as project-handler]
-            [frontend.handler.config :as config-handler]
-            [clojure.string :as string]))
-
-(rum/defcs add-project <
-  (rum/local "" ::project)
-  [state close-fn]
-  (let [project (get state ::project)]
-    [:div
-     [:div.sm:flex.sm:items-start
-      [:div.mx-auto.flex-shrink-0.flex.items-center.justify-center.h-12.w-12.rounded-full.bg-red-100.sm:mx-0.sm:h-10.sm:w-10
-       [:svg.h-6.w-6.text-red-600
-        {:stroke "currentColor", :view-box "0 0 24 24", :fill "none"}
-        [:path
-         {:d
-          "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z",
-          :stroke-width "2",
-          :stroke-linejoin "round",
-          :stroke-linecap "round"}]]]
-      [:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
-       [:h3#modal-headline.text-lg.leading-6.font-medium
-        "Setup a public project on Logseq"]
-       [:div.mt-2
-        [:p.text-sm.leading-5.text-gray-500
-         "All published pages will be located under "
-         [:b "/project/"]
-         "."]]]]
-
-     [:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
-      {:auto-focus true
-       :on-change (fn [e]
-                    (reset! project (util/evalue e)))}]
-
-     [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
-      [:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
-       [:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
-        {:type "button"
-         :on-click (fn []
-                     (let [value @project]
-                       (when (and value (>= (count value) 2))
-                         (project-handler/add-project! value
-                                                       config-handler/set-project!))))}
-        "Submit"]]
-      [:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto
-       [:button.inline-flex.justify-center.w-full.rounded-md.border.border-gray-300.px-4.py-2.bg-white.text-base.leading-6.font-medium.text-gray-700.shadow-sm.hover:text-gray-500.focus:outline-none.focus:border-blue-300.focus:shadow-outline-blue.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
-        {:type "button"
-         :on-click close-fn}
-        "Cancel"]]]]))

+ 0 - 136
src/main/frontend/components/publishing.cljs

@@ -1,136 +0,0 @@
-(ns frontend.components.publishing
-  (:require [rum.core :as rum]
-            [frontend.context.i18n :as i18n]
-            [frontend.db :as db]
-            [frontend.state :as state]
-            [frontend.util :as util]
-            [reitit.frontend.easy :as rfe]
-            [frontend.db-mixins :as db-mixins]
-            [frontend.config :as config]
-            [lambdaisland.glogi :as log]
-            [promesa.core :as p]
-            [frontend.handler.page :as page-handler]
-            [frontend.handler.notification :as notification]
-            [frontend.ui :as ui]
-            [frontend.components.svg :as svg]
-            [frontend.handler.project :as project-handler]))
-
-
-(rum/defcs project
-  < (rum/local :display ::project-state)
-  [state current-project pages]
-  (let [editor-state (get state ::project-state)]
-    (rum/with-context [[t] i18n/*tongue-context*]
-      (if (= :display @editor-state)
-        (when current-project
-          [:div.cp__publishing-pj
-           [:span.cp__publishing-pj-name current-project]
-           [:a.cp__publishing-edit
-            {:on-click
-             (fn [_]
-               (reset! editor-state :editor))}
-            (t :publishing/edit)]])
-        [:div.flex.cp__publishing_pj_edit
-         [:input#cp__publishing-project-input
-          {:placeholder current-project
-           :auto-focus true
-           :default-value current-project}]
-         [:div.cp__publishing-pj-bt
-          (ui/button
-            (t :publishing/save)
-            :on-click (fn [e]
-                        (util/stop e)
-                        (let [editor (.getElementById js/document "cp__publishing-project-input")
-                              v (.-value editor)
-                              data {:name v}]
-                          (-> (p/let [result (project-handler/update-project current-project data)]
-                                (when (:result result)
-                                  (state/update-current-project :name v)
-                                  (notification/show! "Updated project name successfully." :success)
-                                  (reset! editor-state :display)))
-                              (p/catch
-                                (fn [error]
-                                  (notification/show! "Failed to update project name." :failed))))))
-            :background "green")]
-
-         [:div.cp__publishing-pj-bt
-          (ui/button
-            (t :publishing/delete)
-            :on-click (fn [e]
-                        (util/stop e)
-                        (let [confirm-message
-                              (util/format
-                                "This operation will delete all the published pages under the project \"%s\", are you sure?"
-                                current-project)]
-                          (when (.confirm js/window confirm-message)
-                            (p/let [result (project-handler/delete-project current-project)]
-                              (when (:result result)
-                                (reset! editor-state :display)
-                                (state/remove-current-project)
-                                (state/reset-published-pages)
-                                (doseq [{:keys [title]} pages]
-                                  (page-handler/page-add-properties! title {:published false}))
-                                (notification/show! "The project was deleted successfully." :success))))))
-            :background "red")]
-
-         [:div.cp__publishing-pj-bt
-          (ui/button
-            (t :publishing/cancel)
-            :on-click (fn [e]
-                        (util/stop e)
-                        (reset! editor-state :display))
-            :background "pink")]]))))
-
-(rum/defc my-publishing
-  < rum/reactive db-mixins/query
-    (rum/local :display ::project-state)
-  []
-  (let [current-repo (state/sub :git/current-repo)
-        projects (state/sub [:me :projects])
-        current-project (project-handler/get-current-project current-repo projects)]
-    (when current-repo
-      (p/let [_ (page-handler/get-page-list-by-project-name current-project)]
-        (let [publishing-pages (state/sub [:me :published-pages])
-              pages (get publishing-pages current-repo)]
-          (rum/with-context [[t] i18n/*tongue-context*]
-            [:div.flex-1
-             [:h1.title (t :my-publishing)]
-             [:div#cp__publishing-pj-ct
-              [:span (t :publishing/current-project)]
-              (project current-project pages)]
-             [:div#cp__publishing-pg-ct
-              [:div (t :publishing/pages)]
-              [:table.table-auto
-               [:thead
-                [:tr
-                 [:th (t :publishing/page-name)]
-                 [:th (t :publishing/delete-from-logseq)]]]
-               [:tbody
-                (for [{:keys [title permalink]} pages]
-                  [:tr {:key permalink}
-                   [:td [:div.flex {}
-                         [:span [:a {:on-click (fn [e] (util/stop e))
-                                     :href (rfe/href :page {:name title})}
-                                 title]]
-                         [:span [:a {:href (util/format "%s/%s/%s" config/website current-project title)
-                                     :target "_blank"}
-                                 svg/external-link]]]
-                    ]
-                   [:td [:span.text-gray-500.text-sm
-                         [:a {:on-click
-                              (fn [e]
-                                (util/stop e)
-                                (-> (p/let [_ (page-handler/delete-page-from-logseq current-project permalink)]
-                                      (page-handler/update-state-and-notify title))
-                                    (p/catch
-                                      (fn [error]
-                                        (let [status (.-status error)
-                                              not-found-on-server 404]
-                                          (if (= not-found-on-server status)
-                                            (page-handler/update-state-and-notify title)
-                                            (let [message (util/format "Failed to remove the page \"%s\" from Logseq"
-                                                            title)]
-                                              (notification/show! message :failed))))))))}
-                          (t :publishing/delete)]]]])]]]]))))))
-
-

+ 0 - 49
src/main/frontend/components/publishing.css

@@ -1,49 +0,0 @@
-#cp__publishing-pj-ct{
- padding: 10px;
- background-color: var(--ls-quaternary-background-color);
-}
-
-.cp__publishing-pj{
- padding: 5px;
- margin: 5px;
-}
-
-.cp__publishing-pj-name {
- font-size: 25px;
- color: var(--ls-primary-text-color);
-}
-
-.cp__publishing-edit{
-  font-size: 20px;
-  margin-left: 40px;
-  opacity: .5;
-}
-
-.cp__publishing-edit:hover{
-  font-size: 20px;
-  margin-left: 40px;
-  opacity: .6;
-}
-
-.cp__publishing_pj_edit {
-  padding: 5px;
-  margin: 5px;
-}
-
-.cp__publishing-pj-bt {
-  margin: 5px 5px;
-}
-
-#cp__publishing-project-input{
-  border-width: thin;
-  padding: 0 10px;
-  display: inline-block;
-  margin: 5px;
-  border-radius: var(--ls-border-radius-low);
-  border-color: var(--ls-border-color);
-}
-
-#cp__publishing-pg-ct {
-  margin-top: 30px;
-  padding: 10px;
-}

+ 83 - 71
src/main/frontend/components/reference.cljs

@@ -18,70 +18,78 @@
             [medley.core :as medley]))
 
 (rum/defc filter-dialog-inner < rum/reactive
-  [close-fn references page-name]
-  (let [filter-state (page-handler/get-filter page-name)]
-    [:div.filters
-     [:div.sm:flex.sm:items-start
-      [:div.mx-auto.flex-shrink-0.flex.items-center.justify-center.h-12.w-12.rounded-full.bg-gray-200.text-gray-500.sm:mx-0.sm:h-10.sm:w-10
-       (svg/filter-icon)]
-      [:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
-       [:h3#modal-headline.text-lg.leading-6.font-medium "Filter"]
-       [:span.text-xs
-        "Click to include and shift-click to exclude. Click again to remove."]]]
-     (when (seq references)
+  [filters-atom close-fn references page-name]
+  [:div.filters
+   [:div.sm:flex.sm:items-start
+    [:div.mx-auto.flex-shrink-0.flex.items-center.justify-center.h-12.w-12.rounded-full.bg-gray-200.text-gray-500.sm:mx-0.sm:h-10.sm:w-10
+     (svg/filter-icon)]
+    [:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
+     [:h3#modal-headline.text-lg.leading-6.font-medium "Filter"]
+     [:span.text-xs
+      "Click to include and shift-click to exclude. Click again to remove."]]]
+   (when (seq references)
+     (let [filters (rum/react filters-atom)]
        [:div.mt-5.sm:mt-4.sm:flex.sm.gap-1.flex-wrap
-        (for [reference references]
-          (let [filtered (get (rum/react filter-state) reference)
-                color (condp = filtered
-                        true "text-green-400"
-                        false "text-red-400"
-                        nil)]
-            [:button.border.rounded.px-1.mb-1 {:key reference :class color :style {:border-color "currentColor"}
-                                               :on-click (fn [e]
-                                                           (swap! filter-state #(if (nil? (get @filter-state reference))
-                                                                                  (assoc % reference (not (.-shiftKey e)))
-                                                                                  (dissoc % reference)))
-                                                           (page-handler/save-filter! page-name @filter-state))}
-             reference]))])]))
+       (for [reference references]
+         (let [lc-reference (string/lower-case reference)
+               filtered (get filters lc-reference)
+               color (condp = filtered
+                       true "text-green-400"
+                       false "text-red-400"
+                       nil)]
+           [:button.border.rounded.px-1.mb-1.mr-1 {:key reference :class color :style {:border-color "currentColor"}
+                                                   :on-click (fn [e]
+                                                               (swap! filters-atom #(if (nil? (get filters lc-reference))
+                                                                                      (assoc % lc-reference (not (.-shiftKey e)))
+                                                                                      (dissoc % lc-reference)))
+                                                               (page-handler/save-filter! page-name @filters-atom))}
+            reference]))]))])
 
 (defn filter-dialog
-  [references page-name]
+  [filters-atom references page-name]
   (fn [close-fn]
-    (filter-dialog-inner close-fn references page-name)))
+    (filter-dialog-inner filters-atom close-fn references page-name)))
 
-(rum/defc references < rum/reactive
-  [page-name marker? priority?]
+(rum/defcs references < rum/reactive
+  {:init (fn [state]
+           (let [page-name (first (:rum/args state))
+                 filters (when page-name
+                           (atom (page-handler/get-filters (string/lower-case page-name))))]
+             (assoc state ::filters filters)))}
+  [state page-name marker? priority?]
   (when page-name
-    (let [block? (util/uuid-string? page-name)
+    (let [filters-atom (get state ::filters)
+          block? (util/uuid-string? page-name)
           block-id (and block? (uuid page-name))
           page-name (string/lower-case page-name)
           journal? (date/valid-journal-title? (string/capitalize page-name))
           repo (state/get-current-repo)
           ref-blocks (cond
-                       priority?
-                       (db/get-blocks-by-priority repo page-name)
-
-                       marker?
-                       (db/get-marker-blocks repo page-name)
                        block-id
                        (db/get-block-referenced-blocks block-id)
                        :else
                        (db/get-page-referenced-blocks page-name))
+          ref-pages (map (comp :block/original-name first) ref-blocks)
           scheduled-or-deadlines (if (and journal?
                                           (not (true? (state/scheduled-deadlines-disabled?)))
                                           (= page-name (string/lower-case (date/journal-name))))
                                    (db/get-date-scheduled-or-deadlines (string/capitalize page-name))
                                    nil)
           references (db/get-page-linked-refs-refed-pages repo page-name)
-          filter-state (rum/react (page-handler/get-filter page-name))
-          included (filter (fn [[x v]]) filter-state)
+          references (->> (concat ref-pages references)
+                          (remove nil?)
+                          (distinct))
+          filter-state (rum/react filters-atom)
           filters (when (seq filter-state)
                     (->> (group-by second filter-state)
                          (medley/map-vals #(map first %))))
           filtered-ref-blocks (block-handler/filter-blocks repo ref-blocks filters true)
-          n-ref (count filtered-ref-blocks)]
+          n-ref (apply +
+                 (for [[_ rfs] filtered-ref-blocks]
+                   (count rfs)))]
       (when (or (> n-ref 0)
-                (seq scheduled-or-deadlines))
+                (seq scheduled-or-deadlines)
+                (seq filter-state))
         [:div.references.mt-6.flex-1.flex-row
          [:div.content
           (when (seq scheduled-or-deadlines)
@@ -90,7 +98,6 @@
              [:div.references-blocks.mb-6
               (let [ref-hiccup (block/->hiccup scheduled-or-deadlines
                                                {:id (str page-name "-agenda")
-                                                :start-level 2
                                                 :ref? true
                                                 :group-by-page? true
                                                 :editor-box editor/box}
@@ -98,46 +105,51 @@
                 (content/content page-name
                                  {:hiccup ref-hiccup}))]))
 
-          (ui/foldable
-           [:div.flex.flex-row.flex-1.justify-between
-            [:h2.font-bold.opacity-50 (let []
-                                        (str n-ref " Linked Reference"
-                                             (if (> n-ref 1) "s")))]
-            [:a.opacity-50.hover:opacity-100
-             {:title "Filter"
-              :on-click #(state/set-modal! (filter-dialog references page-name))}
-             (svg/filter-icon (cond
-                                (empty? filter-state) nil
-                                (every? true? (vals filter-state)) "text-green-400"
-                                (every? false? (vals filter-state)) "text-red-400"
-                                :else "text-yellow-400"))]]
+          (when (or (> n-ref 0)
+                    (seq filter-state))
+            (ui/foldable
+             [:div.flex.flex-row.flex-1.justify-between
+              [:h2.font-bold.opacity-50 (let []
+                                          (str n-ref " Linked Reference"
+                                               (if (> n-ref 1) "s")))]
+              [:a.opacity-50.hover:opacity-100
+               {:title "Filter"
+                :on-click #(state/set-modal! (filter-dialog filters-atom references page-name))}
+               (svg/filter-icon (cond
+                                  (empty? filter-state) nil
+                                  (every? true? (vals filter-state)) "text-green-400"
+                                  (every? false? (vals filter-state)) "text-red-400"
+                                  :else "text-yellow-400"))]]
 
-           [:div.references-blocks
-            (let [ref-hiccup (block/->hiccup filtered-ref-blocks
-                                             {:id page-name
-                                              :start-level 2
-                                              :ref? true
-                                              :breadcrumb-show? true
-                                              :group-by-page? true
-                                              :editor-box editor/box
-                                              :filters filters}
-                                             {})]
-              (content/content page-name
-                               {:hiccup ref-hiccup}))])]]))))
+             [:div.references-blocks
+              (let [ref-hiccup (block/->hiccup filtered-ref-blocks
+                                               {:id page-name
+                                                :ref? true
+                                                :breadcrumb-show? true
+                                                :group-by-page? true
+                                                :editor-box editor/box
+                                                :filters filters}
+                                               {})]
+                (content/content page-name
+                                 {:hiccup ref-hiccup}))]))]]))))
 
 (rum/defcs unlinked-references-aux
   < rum/reactive db-mixins/query
-  {:will-mount (fn [state]
-                 (let [[page-name n-ref] (:rum/args state)
-                       ref-blocks (db/get-page-unlinked-references page-name)]
-                   (reset! n-ref (count ref-blocks))
-                   (assoc state ::ref-blocks ref-blocks)))}
+  {:wrap-render
+   (fn [render-fn]
+     (fn [state]
+       (reset! (second (:rum/args state))
+               (apply +
+                      (for [[_ rfs]
+                            (db/get-page-unlinked-references
+                             (first (:rum/args state)))]
+                        (count rfs))))
+       (render-fn state)))}
   [state page-name n-ref]
-  (let [ref-blocks (::ref-blocks state)]
+  (let [ref-blocks (db/get-page-unlinked-references page-name)]
     [:div.references-blocks
      (let [ref-hiccup (block/->hiccup ref-blocks
                                       {:id (str page-name "-unlinked-")
-                                       :start-level 2
                                        :ref? true
                                        :group-by-page? true
                                        :editor-box editor/box}

+ 8 - 7
src/main/frontend/components/right_sidebar.cljs

@@ -67,6 +67,7 @@
 (defn recent-pages
   []
   (let [pages (->> (db/get-key-value :recent/pages)
+                   (remove nil?)
                    (remove #(= (string/lower-case %) "contents")))]
     [:div.recent-pages.text-sm.flex-col.flex.ml-3.mt-2
      (if (seq pages)
@@ -75,7 +76,7 @@
                    :href     (rfe/href :page {:name page})
                    :on-click (fn [e]
                                (when (gobj/get e "shiftKey")
-                                 (when-let [page (db/pull [:page/name (string/lower-case page)])]
+                                 (when-let [page (db/pull [:block/name (string/lower-case page)])]
                                    (state/sidebar-add-block!
                                     (state/get-current-repo)
                                     (:db/id page)
@@ -87,7 +88,7 @@
 (rum/defc contents < rum/reactive db-mixins/query
   []
   [:div.contents.flex-col.flex.ml-3
-   (when-let [contents (db/entity [:page/name "contents"])]
+   (when-let [contents (db/entity [:block/name "contents"])]
      (page/contents-page contents))])
 
 (defn build-sidebar-item
@@ -96,7 +97,7 @@
     :contents
     [[:a {:on-click (fn [e]
                       (util/stop e)
-                      (if-not (db/entity [:page/name "contents"])
+                      (if-not (db/entity [:block/name "contents"])
                         (page-handler/create! "contents")
                         (route-handler/redirect! {:to          :page
                                                   :path-params {:name "contents"}})))}
@@ -135,7 +136,7 @@
           (block-cp repo idx block-data)]]))
 
     :page
-    (let [page-name (:page/name block-data)]
+    (let [page-name (:block/name block-data)]
       [[:a {:href     (rfe/href :page {:name page-name})
             :on-click (fn [e]
                         (when (gobj/get e "shiftKey")
@@ -145,14 +146,13 @@
         (page-cp repo page-name)]])
 
     :page-presentation
-    (let [page-name (get-in block-data [:page :page/name])
+    (let [page-name (get-in block-data [:page :block/name])
           journal? (:journal? block-data)
           blocks (db/get-page-blocks repo page-name)
           blocks (if journal?
                    (rest blocks)
                    blocks)
           sections (block/build-slide-sections blocks {:id          "slide-reveal-js"
-                                                       :start-level 2
                                                        :slide?      true
                                                        :sidebar?    true
                                                        :page-name   page-name})]
@@ -297,7 +297,8 @@
               (t :right-side-bar/help)]]]
 
            (when sidebar-open?
-             [:div.mr-1.flex.align-items {:style {:z-index 999}}
+             [:div.flex.align-items {:style {:z-index 999
+                                             :margin-right 2}}
               (toggle)])]
 
            [:.sidebar-item-list.flex-1

+ 17 - 24
src/main/frontend/components/search.cljs

@@ -5,7 +5,6 @@
             [frontend.components.svg :as svg]
             [frontend.handler.route :as route]
             [frontend.handler.page :as page-handler]
-            [frontend.handler.file :as file-handler]
             [frontend.db :as db]
             [frontend.handler.search :as search-handler]
             [frontend.ui :as ui]
@@ -141,10 +140,7 @@
 (rum/defc search-auto-complete
   [{:keys [pages files blocks has-more?] :as result} search-q all?]
   (rum/with-context [[t] i18n/*tongue-context*]
-    (let [new-file (when-let [ext (util/get-file-ext search-q)]
-                     (when (contains? config/mldoc-support-formats (keyword (string/lower-case ext)))
-                       [{:type :new-file}]))
-          pages (when-not all? (map (fn [page] {:type :page :data page}) pages))
+    (let [pages (when-not all? (map (fn [page] {:type :page :data page}) pages))
           files (when-not all? (map (fn [file] {:type :file :data file}) files))
           blocks (map (fn [block] {:type :block :data block}) blocks)
           search-mode (state/get-search-mode)
@@ -153,13 +149,12 @@
                              (= (string/lower-case search-q)
                                 (string/lower-case (:data (first pages)))))
                         (nil? result)
-                        (not= :global search-mode)
                         all?)
                      []
                      [{:type :new-page}])
           result (if config/publishing?
                    (concat pages files blocks)
-                   (concat new-page pages new-file files blocks))]
+                   (concat new-page pages files blocks))]
       [:div.rounded-md.shadow-lg
        {:style (merge
                 {:top 48
@@ -180,24 +175,25 @@
                         (route/redirect! {:to :page
                                           :path-params {:name data}})
 
-                        :new-file
-                        (file-handler/create! search-q)
-
                         :file
                         (route/redirect! {:to :file
                                           :path-params {:path data}})
 
                         :block
                         (let [block-uuid (uuid (:block/uuid data))
-                              page (:page/name (:block/page (db/entity [:block/uuid block-uuid])))]
-                          (route/redirect! {:to :page
-                                            :path-params {:name page}
-                                            :query-params {:anchor (str "ls-block-" (:block/uuid data))}}))
+                              collapsed? (db/parents-collapsed? (state/get-current-repo) block-uuid)]
+                          (if collapsed?
+                            (route/redirect! {:to :page
+                                              :path-params {:name (str block-uuid)}})
+                            (let [page (:block/name (:block/page (db/entity [:block/uuid block-uuid])))]
+                             (route/redirect! {:to :page
+                                               :path-params {:name page}
+                                               :query-params {:anchor (str "ls-block-" (:block/uuid data))}}))))
                         nil))
          :on-shift-chosen (fn [{:keys [type data]}]
                             (case type
                               :page
-                              (let [page (db/entity [:page/name (string/lower-case data)])]
+                              (let [page (db/entity [:block/name (string/lower-case data)])]
                                 (state/sidebar-add-block!
                                  (state/get-current-repo)
                                  (:db/id page)
@@ -213,7 +209,8 @@
                                  :block
                                  block))
 
-                              nil))
+                              nil)
+                            (search-handler/clear-search!))
          :item-render (fn [{:keys [type data]}]
                         (let [search-mode (state/get-search-mode)]
                           [:div {:class "py-2"} (case type
@@ -221,10 +218,6 @@
                                                   [:div.text.font-bold (str (t :new-page) ": ")
                                                    [:span.ml-1 (str "\"" search-q "\"")]]
 
-                                                  :new-file
-                                                  [:div.text.font-bold (str (t :new-file) ": ")
-                                                   [:span.ml-1 (str "\"" search-q "\"")]]
-
                                                   :page
                                                   (search-result-item "Page" (highlight-exact-query data search-q))
 
@@ -233,8 +226,8 @@
 
                                                   :block
                                                   (let [{:block/keys [page content uuid]} data
-                                                        page (or (:page/original-name page)
-                                                                 (:page/name page))
+                                                        page (or (:block/original-name page)
+                                                                 (:block/name page))
                                                         repo (state/sub :git/current-repo)
                                                         format (db/get-page-format page)]
                                                     (search-result-item "Block" (block-search-result-item repo uuid format content search-q search-mode)))
@@ -293,12 +286,12 @@
                           (js/clearTimeout @search-timeout))
                         (let [value (util/evalue e)]
                           (if (string/blank? value)
-                            (search-handler/clear-search!)
+                            (search-handler/clear-search! false)
                             (let [search-mode (state/get-search-mode)
                                   opts (if (= :page search-mode)
                                          (let [current-page (or (state/get-current-page)
                                                                 (date/today))]
-                                           {:page-db-id (:db/id (db/entity [:page/name (string/lower-case current-page)]))})
+                                           {:page-db-id (:db/id (db/entity [:block/name (string/lower-case current-page)]))})
                                          {})]
                               (state/set-q! value)
                               (reset! search-timeout

+ 40 - 0
src/main/frontend/components/settings.cljs

@@ -8,6 +8,7 @@
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.config :as config-handler]
             [frontend.handler.page :as page-handler]
+            [frontend.handler :as handler]
             [frontend.state :as state]
             [frontend.version :refer [version]]
             [frontend.util :as util]
@@ -134,6 +135,15 @@
          :on-click close-fn}
         "Cancel"]]]]))
 
+(rum/defc outdenting-hint
+  []
+  [:div
+   [:p "See more details at " [:a {:href "https://discuss.logseq.com/t/whats-your-preferred-outdent-behavior-the-direct-one-or-the-logical-one/978"} "here"] "."]
+   [:p "default(left) vs logical(right)"]
+   [:img {:src "https://discuss.logseq.com/uploads/default/original/1X/e8ea82f63a5e01f6d21b5da827927f538f3277b9.gif"
+          :width 500
+          :height 500}]])
+
 (rum/defcs settings < rum/reactive
   []
   (let [preferred-format (state/get-preferred-format)
@@ -144,6 +154,8 @@
         current-repo (state/get-current-repo)
         enable-journals? (state/enable-journals? current-repo)
         enable-encryption? (state/enable-encryption? current-repo)
+        sentry-disabled? (state/sub :sentry/disabled?)
+        logical-outdenting? (state/logical-outdenting?)
         enable-git-auto-push? (state/enable-git-auto-push? current-repo)
         enable-block-time? (state/enable-block-time?)
         show-brackets? (state/show-brackets?)
@@ -271,6 +283,15 @@
                  "NOW/LATER"
                  "TODO/DOING")])]]]]
 
+        (toggle "preferred_outdenting"
+                (ui/tippy {:html (outdenting-hint)
+                           :interactive true
+                           :theme "customized"}
+                          (t :settings-page/preferred-outdenting))
+                logical-outdenting?
+                (fn []
+                  (config-handler/toggle-logical-outdenting!)))
+
         (toggle "enable_timetracking"
                 (t :settings-page/enable-timetracking)
                 enable-timetracking?
@@ -336,6 +357,18 @@
 
        [:hr]
 
+       [:div.panel-wrap
+
+        [:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center.sm:pt-5
+         [:label.block.text-sm.font-medium.leading-5.opacity-70
+          {:for "clear_cache"}
+          (t :settings-page/clear-cache)]
+         [:div.mt-1.sm:mt-0.sm:col-span-2
+          [:div.max-w-lg.rounded-md.sm:max-w-xs
+           (ui/button (t :settings-page/clear)
+             :on-click (fn []
+                         (handler/clear-cache!)))]]]]
+
        [:div.panel-wrap
         [:div.it.app-updater.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
          [:label.block.text-sm.font-medium.leading-5.opacity-70
@@ -344,6 +377,13 @@
           [:div.ver version]
           (if (util/electron?) (app-updater))]]
 
+        (toggle "disable_sentry"
+                (t :settings-page/disable-sentry)
+                sentry-disabled?
+                (fn []
+                  (let [value (not sentry-disabled?)]
+                    (state/set-sentry-disabled! value))))
+
         [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
          [:label.block.text-sm.font-medium.leading-5.opacity-70
           {:for "developer_mode"}

+ 65 - 0
src/main/frontend/components/shortcut.cljs

@@ -0,0 +1,65 @@
+(ns frontend.components.shortcut
+  (:require [frontend.context.i18n :as i18n]
+            [frontend.modules.shortcut.data-helper :as dh]
+            [frontend.state :as state]
+            [rum.core :as rum]))
+(def *shortcut-config (rum/cursor-in state/state [:config (state/get-current-repo) :shortcuts]))
+
+(rum/defc shortcut-table < rum/reactive
+  [name]
+  (let [_ (rum/react *shortcut-config)]
+    (rum/with-context [[t] i18n/*tongue-context*]
+      [:div
+       [:table
+        [:thead
+         [:tr
+          [:th.text-left [:b (t name)]]
+          [:th.text-right [:b (t :help/shortcut)]]]]
+        [:tbody
+         (map (fn [[k {:keys [binding]}]]
+                [:tr {:key k}
+                 [:td.text-left (t (dh/decorate-namespace k))]
+                 [:td.text-right (dh/binding-for-display k binding)]])
+              (dh/binding-by-category name))]]])))
+
+(rum/defc trigger-table []
+  (rum/with-context [[t] i18n/*tongue-context*]
+    [:table
+     [:thead
+      [:tr
+       [:th.text-left [:b (t :help/shortcuts-triggers)]]
+       [:th.text-right [:b (t :help/shortcut)]]]]
+     [:tbody
+      [:tr
+       [:td.text-left (t :help/slash-autocomplete)]
+       [:td.text-right "/"]]
+      [:tr
+       [:td.text-left (t :help/block-content-autocomplete)]
+       [:td.text-right "<"]]
+      [:tr
+       [:td.text-left (t :help/reference-autocomplete)]
+       [:td.text-right "[[]]"]]
+      [:tr
+       [:td.text-left (t :help/block-reference)]
+       [:td.text-right "(())"]]
+      [:tr
+       [:td.text-left (t :shortcut.editor/open-link-in-sidebar)]
+       [:td.text-right "shift-click"]]
+      [:tr
+       [:td.text-left (t :help/context-menu)]
+       [:td.text-right "right click"]]]]))
+
+(rum/defc shortcut
+  []
+  (rum/with-context [[t] i18n/*tongue-context*]
+    [:div
+     [:h1.title (t :help/shortcut-page-title)]
+     (trigger-table)
+     (shortcut-table :shortcut.category/basics)
+     (shortcut-table :shortcut.category/navigating)
+     (shortcut-table :shortcut.category/block-editing)
+     (shortcut-table :shortcut.category/block-command-editing)
+     (shortcut-table :shortcut.category/block-selection)
+     (shortcut-table :shortcut.category/formatting)
+     (shortcut-table :shortcut.category/toggle)
+     (shortcut-table :shortcut.category/others)]))

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

@@ -23,7 +23,6 @@
             [frontend.handler.route :as route-handler]
             [frontend.handler.export :as export]
             [frontend.config :as config]
-            [frontend.keyboards :as keyboards]
             [dommy.core :as d]
             [clojure.string :as string]
             [goog.object :as gobj]
@@ -136,11 +135,16 @@
                       :style {:margin-bottom (if global-graph-pages? 0 120)}}
           main-content])]]]))
 
+(rum/defc footer
+  []
+  (when-let [user-footer (and config/publishing? (get-in (state/get-config) [:publish-common-footer]))]
+    [:div.p-6 user-footer]))
+
 (defn get-default-home-if-valid
   []
   (when-let [default-home (state/get-default-home)]
     (when-let [page (:page default-home)]
-      (when (db/entity [:page/name (string/lower-case page)])
+      (when (db/entity [:block/name (string/lower-case page)])
         default-home))))
 
 (defonce sidebar-inited? (atom false))
@@ -229,11 +233,11 @@
                   :exit 300}}
        links
        ;; (custom-context-menu-content)
-       ))))
+))))
 
 (rum/defc new-block-mode < rum/reactive
   []
-  (when-let [alt-enter? (= "alt+enter" (state/sub [:shortcuts :editor/new-block]))]
+  (when (state/sub [:editor/new-block-toggle?])
     [:a.px-1.text-sm.font-medium.bg-base-2.mr-4.rounded-md
      {:title "Click to switch to \"Enter\" for creating new block"
       :on-click state/toggle-new-block-shortcut!}
@@ -268,38 +272,13 @@
 (rum/defcs sidebar <
   (mixins/modal :modal/show?)
   rum/reactive
-  ;; TODO: move this to keyboards
   (mixins/event-mixin
    (fn [state]
      (mixins/listen state js/window "click"
                     (fn [e]
                       ;; hide context menu
                       (state/hide-custom-context-menu!)
-
-                      (editor-handler/clear-selection! e)))
-
-     ;; TODO: move to keyboards
-     (mixins/on-key-down
-      state
-      {;; esc
-       27 (fn [_state e]
-            (editor-handler/clear-selection! e))
-
-       ;; shift+up
-       38 (fn [state e]
-            (editor-handler/on-select-block state e true))
-
-       ;; shift+down
-       40 (fn [state e]
-            (editor-handler/on-select-block state e false))
-
-       ;; ?
-       191 (fn [state e]
-             (when-not (util/input? (gobj/get e "target"))
-               (ui-handler/toggle-help!)))})))
-  {:did-mount (fn [state]
-                (keyboards/bind-shortcuts!)
-                state)}
+                      (editor-handler/clear-selection! e)))))
   [state route-match main-content]
   (let [{:keys [open? close-fn open-fn]} state
         close-fn (fn []
@@ -326,7 +305,7 @@
         :route         route-match
         :nfs-granted?  granted?
         :db-restoring? db-restoring?
-        :on-click      editor-handler/unhighlight-block!}
+        :on-click      editor-handler/unhighlight-blocks!}
 
        [:div.theme-inner
         (sidebar-mobile-sidebar
@@ -354,7 +333,9 @@
                   :indexeddb-support?  indexeddb-support?
                   :white?              white?
                   :db-restoring?       db-restoring?
-                  :main-content        main-content})]]
+                  :main-content        main-content})
+
+           (footer)]]
          (right-sidebar/sidebar)]
 
         (ui/notification)
@@ -372,4 +353,4 @@
          ;;   :on-click (fn []
          ;;               (state/set-left-sidebar-open! (not (state/get-left-sidebar-open?))))}
          ;;  (if (state/sub :ui/left-sidebar-open?) "<" ">")]
-          )]))))
+)]))))

文件差異過大導致無法顯示
+ 13 - 5
src/main/frontend/config.cljs


+ 12 - 9
src/main/frontend/context/i18n.cljs

@@ -1,14 +1,16 @@
 (ns frontend.context.i18n
   (:require [frontend.dicts :as dicts]
+            [frontend.modules.shortcut.dict :as shortcut-dict]
             [rum.core :as rum]
+            [medley.core :refer [deep-merge]]
             [frontend.state :as state]))
 
 ;; TODO
-;; - [x] Get the preffered language from state
-;; - [x] Update the preffered language
-;; - [x] Create t functiona which takes a keyword and returns text with the current preffered language
-;; - [x] Add fetch for local browser prefered language if user has set it already
-;; - [ ] Fetch prefered language from backend if user is logged in
+;; - [x] Get the preferred language from state
+;; - [x] Update the preferred language
+;; - [x] Create t functiona which takes a keyword and returns text with the current preferred language
+;; - [x] Add fetch for local browser preferred language if user has set it already
+;; - [ ] Fetch preferred language from backend if user is logged in
 
 (defn fetch-local-language []
   (.. js/window -navigator -language))
@@ -16,13 +18,14 @@
 (rum/defcontext *tongue-context*)
 
 (rum/defc tongue-provider [children]
-  (let [prefered-language (keyword (state/sub :preferred-language))
+  (let [preferred-language (keyword (state/sub :preferred-language))
         set-preferred-language state/set-preferred-language!
-        t (partial dicts/translate prefered-language)]
-    (if (nil? prefered-language)
+        all-dicts (deep-merge dicts/dicts shortcut-dict/dict)
+        t (partial (dicts/translate all-dicts) preferred-language)]
+    (if (nil? preferred-language)
       (set-preferred-language (fetch-local-language))
       :ok)
-    (rum/bind-context [*tongue-context* [t prefered-language set-preferred-language]]
+    (rum/bind-context [*tongue-context* [t preferred-language set-preferred-language]]
                       children)))
 
 (rum/defc use-tongue []

+ 2 - 1
src/main/frontend/core.cljs

@@ -8,7 +8,7 @@
             [frontend.log]
             [reitit.frontend :as rf]
             [reitit.frontend.easy :as rfe]
-            [api]))
+            [logseq.api]))
 
 (defn set-router!
   []
@@ -55,4 +55,5 @@
 (defn stop []
   ;; stop is called before any code is reloaded
   ;; this is controlled by :before-load in the config
+  (handler/stop!)
   (js/console.log "stop"))

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

@@ -20,7 +20,7 @@
   (when-let [formatter-string (state/get-date-formatter)]
     (tf/unparse (tf/formatter formatter-string) date)))
 
-(def custom-formatter (tf/formatter "yyyy-MM-dd HH:mm:ssZ"))
+(def custom-formatter (tf/formatter "yyyy-MM-dd'T'HH:mm:ssZZ"))
 
 (defn journal-title-formatters
   []
@@ -44,8 +44,24 @@
      "yyyy年MM月dd日"}
    (state/get-date-formatter)))
 
-(defn get-date-time-string [date-time]
-  (tf/unparse custom-formatter date-time))
+(defn get-date-time-string
+  ([]
+   (get-date-time-string (t/now)))
+  ([date-time]
+   (tf/unparse custom-formatter date-time)))
+
+(defn get-locale-string
+  [s]
+  (try
+    (->> (tf/parse (tf/formatters :date-time-no-ms) s)
+        (t/to-default-time-zone)
+        (tf/unparse (tf/formatter "MMM do, yyyy")))
+    (catch js/Error e
+      nil)))
+
+(defn ISO-string
+  []
+  (.toISOString (js/Date.)))
 
 (defn get-local-date-time-string
   []
@@ -170,6 +186,11 @@
     (let [journal-title (util/capitalize-all journal-title)]
       (journal-title-> journal-title #(util/parse-int (tf/unparse (tf/formatter "yyyyMMdd") %))))))
 
+(defn int->journal-title
+  [day]
+  (when day
+    (format (tf/parse (tf/formatter "yyyyMMdd") (str day)))))
+
 (defn journal-title->long
   [journal-title]
   (journal-title-> journal-title #(tc/to-long %)))

+ 35 - 52
src/main/frontend/db.cljs

@@ -21,11 +21,8 @@
   conns
   get-repo-path
   datascript-db
-  datascript-files-db
   remove-db!
-  remove-files-db!
   get-conn
-  get-files-conn
   me-tx
   remove-conn!]
 
@@ -37,29 +34,29 @@
   entity pull pull-many transact! get-key-value]
 
  [frontend.db.model
-  add-properties! block-and-children-transform blocks-count blocks-count-cache clean-export!  cloned? delete-blocks
+  block-and-children-transform blocks-count blocks-count-cache clean-export!  cloned? delete-blocks get-pre-block
   delete-file! delete-file-blocks! delete-file-pages! delete-file-tx delete-files delete-pages-by-files
   filter-only-public-pages-and-blocks get-all-block-contents get-all-tagged-pages
-  get-all-templates get-block-and-children get-block-and-children-no-cache get-block-by-uuid get-block-children
-  get-block-children-ids get-block-content get-block-file get-block-immediate-children get-block-page
-  get-block-page-end-pos get-block-parent get-block-parents get-block-referenced-blocks get-block-refs-count
-  get-blocks-by-priority get-blocks-contents get-collapsed-blocks get-custom-css
-  get-date-scheduled-or-deadlines get-db-type get-empty-pages get-file get-file-after-blocks get-file-after-blocks-meta
-  get-file-blocks get-file-contents get-file-last-modified-at get-file-no-sub get-file-page get-file-page-id
+  get-all-templates get-block-and-children get-block-by-uuid get-block-children sort-by-left
+  get-block-parent get-block-parents parents-collapsed? get-block-referenced-blocks
+  get-block-children-ids get-block-immediate-children get-block-page
+  get-blocks-contents get-custom-css
+  get-date-scheduled-or-deadlines get-db-type get-empty-pages get-file
+  get-file-blocks get-file-contents get-file-last-modified-at get-file-no-sub get-file-page get-file-page-id file-exists?
   get-file-pages get-files get-files-blocks get-files-full get-files-that-referenced-page get-journals-length
-  get-latest-journals get-marker-blocks get-matched-blocks get-page get-page-alias get-page-alias-names get-page-blocks get-page-linked-refs-refed-pages
-  get-page-blocks-count get-page-blocks-no-cache get-page-file get-page-format get-page-name get-page-properties
-  get-page-properties-content get-page-referenced-blocks get-page-referenced-pages get-page-unlinked-references
+  get-latest-journals get-matched-blocks get-page get-page-alias get-page-alias-names get-page-blocks get-page-linked-refs-refed-pages
+  get-page-blocks-count get-page-blocks-no-cache get-page-file get-page-format get-page-properties
+  get-page-referenced-blocks get-page-referenced-pages get-page-unlinked-references get-page-referenced-blocks-no-cache
   get-pages get-pages-relation get-pages-that-mentioned-page get-public-pages get-tag-pages
   journal-page? local-native-fs? mark-repo-as-cloned! page-alias-set page-blocks-transform pull-block
   set-file-last-modified-at! transact-files-db! with-block-refs-count get-modified-pages page-empty? get-alias-source-page
-  set-file-content!]
+  set-file-content! has-children?]
 
  [frontend.db.react
-  get-current-marker get-current-page get-current-priority get-handler-keys set-key-value
+  get-current-marker get-current-page get-current-priority set-key-value
   transact-react! remove-key! remove-q! remove-query-component! add-q! add-query-component! clear-query-state!
   clear-query-state-without-refs-and-embeds! get-block-blocks-cache-atom get-page-blocks-cache-atom kv q
-  query-state query-components query-entity-in-component remove-custom-query! set-new-result! sub-key-value]
+  query-state query-components query-entity-in-component remove-custom-query! set-new-result! sub-key-value refresh!]
 
  [frontend.db.query-custom
   custom-query]
@@ -70,18 +67,13 @@
 
 ;; persisting DBs between page reloads
 (defn persist! [repo]
-  (let [file-key (datascript-files-db repo)
-        non-file-key (datascript-db repo)
-        files-conn (get-files-conn repo)
-        file-db (when files-conn (d/db files-conn))
-        non-file-conn (get-conn repo false)
-        non-file-db (when non-file-conn (d/db non-file-conn))
-        file-db-str (if file-db (db->string file-db) "")
-        non-file-db-str (if non-file-db (db->string non-file-db) "")]
-    (p/let [_ (idb/set-batch! [{:key file-key :value file-db-str}
-                               {:key non-file-key :value non-file-db-str}])]
-      (state/set-last-persist-transact-id! repo true (get-max-tx-id file-db))
-      (state/set-last-persist-transact-id! repo false (get-max-tx-id non-file-db)))))
+  (let [key (datascript-db repo)
+        conn (get-conn repo false)]
+    (when conn
+      (let [db (d/db conn)
+            db-str (if db (db->string db) "")]
+        (p/let [_ (idb/set-batch! [{:key key :value db-str}])]
+          (state/set-last-persist-transact-id! repo false (get-max-tx-id db)))))))
 
 (defonce persistent-jobs (atom {}))
 
@@ -110,31 +102,27 @@
 ;; TODO: pass as a parameter
 (defonce *sync-search-indice-f (atom nil))
 (defn- repo-listen-to-tx!
-  [repo conn files-db?]
+  [repo conn]
   (d/listen! conn :persistence
              (fn [tx-report]
                (when-not (util/electron?)
                 (let [tx-id (get-tx-id tx-report)]
                   (state/set-last-transact-time! repo (util/time-ms))
-                  ;; (state/persist-transaction! repo files-db? tx-id (:tx-data tx-report))
                   (persist-if-idle! repo)))
 
                ;; rebuild search indices
-               (when-not files-db?
-                 (let [data (:tx-data tx-report)
-                       datoms (filter
-                               (fn [datom]
-                                 (contains? #{:page/name :block/content} (:a datom)))
-                               data)]
-                   (when-let [f @*sync-search-indice-f]
-                     (f datoms)))))))
+               (let [data (:tx-data tx-report)
+                     datoms (filter
+                             (fn [datom]
+                               (contains? #{:block/name :block/content} (:a datom)))
+                             data)]
+                 (when-let [f @*sync-search-indice-f]
+                   (f datoms))))))
 
 (defn- listen-and-persist!
   [repo]
-  (when-let [conn (get-files-conn repo)]
-    (repo-listen-to-tx! repo conn true))
   (when-let [conn (get-conn repo false)]
-    (repo-listen-to-tx! repo conn false)))
+    (repo-listen-to-tx! repo conn)))
 
 (defn start-db-conn!
   ([me repo]
@@ -149,17 +137,8 @@
   (let [logged? (:name me)]
     (doall
      (for [{:keys [url]} repos]
-       (let [repo url
-             db-name (datascript-files-db repo)
-             db-conn (d/create-conn db-schema/files-db-schema)]
-         (swap! conns assoc db-name db-conn)
-         (p/let [stored (idb/get-item db-name)
-                 _ (when stored
-                     (let [stored-db (string->db stored)
-                           attached-db (d/db-with stored-db
-                                                  [(me-tx stored-db me)])]
-                       (conn/reset-conn! db-conn attached-db)))
-                 db-name (datascript-db repo)
+       (let [repo url]
+         (p/let [db-name (datascript-db repo)
                  db-conn (d/create-conn db-schema/schema)
                  _ (d/transact! db-conn [{:schema/version db-schema/version}])
                  _ (swap! conns assoc db-name db-conn)
@@ -183,3 +162,7 @@
         (f))
       (recur))
     chan))
+
+(defn new-block-id
+  []
+  (d/squuid))

+ 2 - 21
src/main/frontend/db/conn.cljs

@@ -23,19 +23,10 @@
   (when repo
     (str config/idb-db-prefix (get-repo-path repo))))
 
-(defn datascript-files-db
-  [repo]
-  (when repo
-    (str "logseq-files-db/" (get-repo-path repo))))
-
 (defn remove-db!
   [repo]
   (idb/remove-item! (datascript-db repo)))
 
-(defn remove-files-db!
-  [repo]
-  (idb/remove-item! (datascript-files-db repo)))
-
 (defn get-conn
   ([]
    (get-conn (state/get-current-repo) true))
@@ -50,19 +41,12 @@
          @conn
          conn)))))
 
-(defn get-files-conn
-  ([]
-   (get-files-conn (state/get-current-repo)))
-  ([repo]
-   (get @conns (datascript-files-db repo))))
-
 (defn reset-conn! [conn db]
   (reset! conn db))
 
 (defn remove-conn!
   [repo]
-  (swap! conns dissoc (datascript-db repo))
-  (swap! conns dissoc (datascript-files-db repo)))
+  (swap! conns dissoc (datascript-db repo)))
 
 (defn me-tx
   [db {:keys [name email avatar]}]
@@ -74,11 +58,8 @@
   ([me repo]
    (start! me repo {}))
   ([me repo {:keys [db-type listen-handler]}]
-   (let [files-db-name (datascript-files-db repo)
-         files-db-conn (d/create-conn db-schema/files-db-schema)
-         db-name (datascript-db repo)
+   (let [db-name (datascript-db repo)
          db-conn (d/create-conn db-schema/schema)]
-     (swap! conns assoc files-db-name files-db-conn)
      (swap! conns assoc db-name db-conn)
      (d/transact! db-conn [(cond-> {:schema/version db-schema/version}
                              db-type

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

@@ -18,37 +18,4 @@
                :git/cloned? (cloned? repo)
                :git/status (get-key-value repo :git/status)
                :git/error (get-key-value repo :git/error)})
-            repos)))
-
-  ;; filtered blocks
-
-  (def page-and-aliases #{22})
-  (def excluded-pages #{59})
-  (def include-pages #{106})
-  (def page-linked-blocks
-    (->
-     (d/q
-      '[:find (pull ?b [:block/uuid
-                        :block/title
-                        {:block/children ...}])
-        :in $ ?pages
-        :where
-        [?b :block/ref-pages ?ref-page]
-        [(contains? ?pages ?ref-page)]]
-      (get-conn)
-      page-and-aliases)
-     flatten))
-
-  (def page-linked-blocks-include-filter
-    (if (seq include-pages)
-      (filter (fn [{:block/keys [ref-pages]}]
-                (some include-pages (map :db/id ref-pages)))
-              page-linked-blocks)
-      page-linked-blocks))
-
-  (def page-linked-blocks-exclude-filter
-    (if (seq excluded-pages)
-      (remove (fn [{:block/keys [ref-pages]}]
-                (some excluded-pages (map :db/id ref-pages)))
-              page-linked-blocks-include-filter)
-      page-linked-blocks-include-filter)))
+            repos))))

+ 3 - 3
src/main/frontend/db/default.cljs

@@ -3,7 +3,7 @@
 
 (def built-in-pages
   (mapv (fn [p]
-          {:page/name (string/lower-case p)
-           :page/original-name p
-           :page/journal? false})
+          {:block/name (string/lower-case p)
+           :block/original-name p
+           :block/journal? false})
         #{"NOW" "LATER" "DOING" "DONE" "IN-PROGRESS" "TODO" "WAIT" "WAITING" "A" "B" "C"}))

文件差異過大導致無法顯示
+ 269 - 362
src/main/frontend/db/model.cljs


+ 61 - 0
src/main/frontend/db/outliner.cljs

@@ -0,0 +1,61 @@
+(ns frontend.db.outliner
+  (:require [datascript.core :as d]
+            [frontend.db :as db]
+            [frontend.db.react :as react]
+            [frontend.util :as util]
+            [frontend.debug :as debug]
+            [frontend.db.utils :as db-utils]))
+
+(defn get-by-id
+  [conn id]
+  (try
+    (d/pull @conn '[*] id)
+    (catch js/Error e nil)))
+
+(defn get-by-parent-&-left
+  [conn parent-id left-id]
+  (let [r (d/q '[:find (pull ?a [*])
+                 :in $ ?p ?l
+                 :where
+                 [?a :block/left ?l]
+                 [?a :block/parent ?p]]
+            @conn parent-id left-id)]
+    (ffirst r)))
+
+;; key [:block/children parent-id]
+
+(def get-by-parent-id
+  '[:find (pull ?a [*])
+    :in $ ?id
+    :where
+    [?a :block/parent ?id]])
+
+(defn save-block
+  [conn block-m]
+  (let [tx (-> (dissoc block-m :block/children :block/dummy? :block/level :block/meta)
+             (util/remove-nils))
+        block-id (:block/uuid block-m)]
+    (d/transact! conn [tx])
+    (db-utils/pull [:block/uuid block-id])))
+
+(defn del-block
+  [conn id-or-look-ref]
+  (d/transact! conn [[:db.fn/retractEntity id-or-look-ref]]))
+
+(defn del-blocks
+  [ids-or-look-refs]
+  (mapv (fn [id-or-look-ref]
+         [:db.fn/retractEntity id-or-look-ref])
+    ids-or-look-refs))
+
+(defn get-journals
+  [conn]
+  (let [r (d/q '[:find (pull ?a [*])
+                 :where
+                 [?a :block/journal? true]]
+               @conn)]
+    (flatten r)))
+
+(defn remove-non-existed-refs!
+  [refs]
+  (filter db/entity refs))

+ 18 - 17
src/main/frontend/db/query_dsl.cljs

@@ -27,7 +27,7 @@
 ;;            (between last-modified-at -1d today)
 ;; [[page-ref]]
 ;; property (block)
-;; todo (block)
+;; task (block)
 ;; priority (block)
 ;; page
 ;; page-property (page)
@@ -138,7 +138,7 @@
          page-ref? (text/page-ref? e)]
      (when (or (and page-ref?
                     (not (contains? #{'page-property 'page-tags} (:current-filter env))))
-               (contains? #{'between 'property 'todo 'priority 'sort-by 'page} fe))
+               (contains? #{'between 'property 'todo 'task 'priority 'sort-by 'page} fe))
        (reset! blocks? true))
      (cond
        (nil? e)
@@ -147,7 +147,7 @@
        page-ref?
        (let [page-name (-> (text/page-ref-un-brackets! e)
                            (string/lower-case))]
-         [['?b :block/path-ref-pages [:page/name page-name]]])
+         [['?b :block/path-refs [:block/name page-name]]])
 
        (contains? #{'and 'or 'not} fe)
        (let [clauses (->> (map (fn [form]
@@ -193,8 +193,8 @@
              end (->journal-day-int (nth e 2))
              [start end] (sort [start end])]
          [['?b :block/page '?p]
-          ['?p :page/journal? true]
-          ['?p :page/journal-day '?d]
+          ['?p :block/journal? true]
+          ['?p :block/journal-day '?d]
           [(list '>= '?d start)]
           [(list '<= '?d end)]])
 
@@ -224,7 +224,7 @@
                    '?v
                      (uniq-symbol counter "?v"))]
          [['?b :block/properties '?prop]
-          [(list 'get '?prop (name (nth e 1))) sym]
+          [(list 'get '?prop (keyword (nth e 1))) sym]
           (list
            'or
            [(list '= sym v)]
@@ -233,9 +233,9 @@
        (and (= 'property fe)
             (= 2 (count e)))
        [['?b :block/properties '?prop]
-        [(list 'get '?prop (name (nth e 1)))]]
+        [(list 'get '?prop (keyword (nth e 1)))]]
 
-       (= 'todo fe)
+       (or (= 'todo fe) (= 'task fe))
        (let [markers (if (coll? (first (rest e)))
                        (first (rest e))
                        (rest e))]
@@ -274,7 +274,7 @@
        (= 'page fe)
        (let [page-name (string/lower-case (first (rest e)))
              page-name (text/page-ref-un-brackets! page-name)]
-         [['?b :block/page [:page/name page-name]]])
+         [['?b :block/page [:block/name page-name]]])
 
        (= 'page-property fe)
        (let [[k v] (rest e)]
@@ -282,13 +282,14 @@
            (let [v (some->> (name (nth e 2))
                             (text/page-ref-un-brackets!))
                  sym '?v]
-             [['?p :page/properties '?prop]
+             [['?p :block/name]
+              ['?p :block/properties '?prop]
               [(list 'get '?prop (keyword (nth e 1))) sym]
               (list
                'or
                [(list '= sym v)]
                [(list 'contains? sym v)])])
-           [['?p :page/properties '?prop]
+           [['?p :block/properties '?prop]
             [(list 'get '?prop (keyword (nth e 1)))]]))
 
        (= 'page-tags fe)
@@ -301,12 +302,12 @@
              (let [tags (set (map (comp text/page-ref-un-brackets! string/lower-case name) tags))]
                (let [sym-1 (uniq-symbol counter "?t")
                      sym-2 (uniq-symbol counter "?tag")]
-                 [['?p :page/tags sym-1]
-                  [sym-1 :page/name sym-2]
+                 [['?p :block/tags sym-1]
+                  [sym-1 :block/name sym-2]
                   [(list 'contains? tags sym-2)]])))))
 
        (= 'all-page-tags fe)
-       [['?e :page/tags '?p]]
+       [['?e :block/tags '?p]]
 
        :else
        nil))))
@@ -335,13 +336,13 @@
     (if not?
       (cond
         (and b? p?)
-        (concat [['?b :block/uuid] ['?p :page/name] ['?b :block/page '?p]] q)
+        (concat [['?b :block/uuid] ['?p :block/name] ['?b :block/page '?p]] q)
 
         b?
         (concat [['?b :block/uuid]] q)
 
         p?
-        (concat [['?p :page/name]] q)
+        (concat [['?p :block/name]] q)
 
         :else
         q)
@@ -437,7 +438,7 @@
 
   (query "(and [[some page]] (property foo bar))")
 
-  (query "(and [[some page]] (todo now later))")
+  (query "(and [[some page]] (task now later))")
 
   (query "(and [[some page]] (priority A))")
 

+ 40 - 29
src/main/frontend/db/query_react.cljs

@@ -47,31 +47,31 @@
   [query-result remove-blocks q]
   (try
     (let [repo (state/get-current-repo)
-         result (db-utils/seq-flatten query-result)
-         block? (:block/uuid (first result))]
-     (let [result (if block?
-                    (let [result (if (seq remove-blocks)
-                                   (let [remove-blocks (set remove-blocks)]
-                                     (remove (fn [h]
-                                               (contains? remove-blocks (:block/uuid h)))
-                                             result))
-                                   result)]
-                      (some->> result
-                               (db-utils/with-repo repo)
-                               (model/with-block-refs-count repo)
-                               (model/sort-blocks)))
-                    result)]
-       (if-let [result-transform (:result-transform q)]
-         (if-let [f (sci/eval-string (pr-str result-transform))]
-           (try
-             (sci/call-fn f result)
-             (catch js/Error e
-               (log/error :sci/call-error e)
-               result))
-           result)
-         (if block?
-           (db-utils/group-by-page result)
-           result))))
+          result (db-utils/seq-flatten query-result)
+          block? (:block/uuid (first result))]
+      (let [result (if block?
+                     (let [result (if (seq remove-blocks)
+                                    (let [remove-blocks (set remove-blocks)]
+                                      (remove (fn [h]
+                                                (contains? remove-blocks (:block/uuid h)))
+                                              result))
+                                    result)]
+                       (some->> result
+                                (db-utils/with-repo repo)
+                                (model/with-block-refs-count repo)
+                                (model/with-pages)))
+                     result)]
+        (if-let [result-transform (:result-transform q)]
+          (if-let [f (sci/eval-string (pr-str result-transform))]
+            (try
+              (sci/call-fn f result)
+              (catch js/Error e
+                (log/error :sci/call-error e)
+                result))
+            result)
+          (if block?
+            (db-utils/group-by-page result)
+            result))))
     (catch js/Error e
       (log/error :query/failed e))))
 
@@ -80,14 +80,25 @@
   (let [page-ref? #(and (string? %) (text/page-ref? %))]
     (walk/postwalk
      (fn [f]
-       (if (and (list? f)
-                (= (first f) '=)
-                (= 3 (count f))
-                (some page-ref? (rest f)))
+       (cond
+         ;; backward compatible
+         ;; 1. replace :page/ => :block/
+         (and (keyword? f) (= "page" (namespace f)))
+         (keyword "block" (name f))
+
+         (and (keyword? f) (contains? #{:block/ref-pages :block/ref-blocks} f))
+         :block/refs
+
+         (and (list? f)
+              (= (first f) '=)
+              (= 3 (count f))
+              (some page-ref? (rest f)))
          (let [[x y] (rest f)
                [page-ref sym] (if (page-ref? x) [x y] [y x])
                page-ref (string/lower-case page-ref)]
            (list 'contains? sym (text/page-ref-un-brackets! page-ref)))
+
+         :else
          f)) query)))
 
 (defn react-query

+ 70 - 61
src/main/frontend/db/react.cljs

@@ -8,6 +8,7 @@
             [frontend.state :as state]
             [frontend.date :as date]
             [frontend.util :as util :refer-macros [profile] :refer [react]]
+            [frontend.util.marker :as marker]
             [clojure.string :as string]
             [frontend.config :as config]
             [datascript.core :as d]
@@ -54,6 +55,17 @@
                    (into {}))]
     (reset! query-state state)))
 
+(defn get-current-repo-refs-keys
+  []
+  (when-let [current-repo (state/get-current-repo)]
+    (->>
+     (map (fn [[repo k id]]
+            (when (and (= repo current-repo)
+                       (contains? #{:block/refed-blocks :block/unlinked-refs} k))
+              [k id]))
+       (keys @query-state))
+     (remove nil?))))
+
 ;; TODO: Add components which subscribed to a specific query
 (defn add-q!
   [k query inputs result-atom transform-fn query-fn inputs-fn]
@@ -119,16 +131,12 @@
          (add-q! k nil nil result-atom identity identity identity))))))
 
 (defn q
-  [repo k {:keys [use-cache? files-db? transform-fn query-fn inputs-fn]
+  [repo k {:keys [use-cache? transform-fn query-fn inputs-fn]
            :or {use-cache? true
-                files-db? false
                 transform-fn identity}} query & inputs]
   (let [kv? (and (vector? k) (= :kv (first k)))
         k (vec (cons repo k))]
-    (when-let [conn (if files-db?
-                      (when-let [files-conn (conn/get-files-conn repo)]
-                        (deref files-conn))
-                      (conn/get-conn repo))]
+    (when-let [conn (conn/get-conn repo)]
       (let [result-atom (:result (get @query-state k))]
         (when-let [component *query-component*]
           (add-query-component! k component))
@@ -175,7 +183,7 @@
                (date/journal-name))]
     (when page
       (let [page-name (string/lower-case page)]
-        (db-utils/entity [:page/name page-name])))))
+        (db-utils/entity [:block/name page-name])))))
 
 (defn get-current-priority
   []
@@ -192,10 +200,10 @@
         route-name (get-in match [:data :name])]
     (when (= route-name :page)
       (when-let [page-name (get-in match [:path-params :name])]
-        (and (util/marker? page-name)
+        (and (marker/marker? page-name)
              (string/upper-case page-name))))))
 
-(defn get-handler-keys
+(defn get-related-keys
   [{:keys [key data]}]
   (cond
     (coll? key)
@@ -210,7 +218,7 @@
               current-marker (get-current-marker)
               current-page-id (:db/id (get-current-page))
               {:block/keys [page]} (first blocks)
-              handler-keys (->>
+              related-keys (->>
                             (util/concat-without-nil
                              (mapcat
                               (fn [block]
@@ -233,29 +241,26 @@
 
                              (when current-page-id
                                [[:page/ref-pages current-page-id]
-                                [:page/refed-blocks current-page-id]
+                                ;; [:block/refed-blocks current-page-id]
                                 [:page/mentioned-pages current-page-id]])
 
-                             ;; refed-pages
                              (apply concat
-                                    (for [{:block/keys [ref-pages]} blocks]
-                                      (map (fn [page]
-                                             (when-let [page (db-utils/entity [:page/name (:page/name page)])]
-                                               [:page/refed-blocks (:db/id page)]))
-                                           ref-pages)))
-
-                             ;; refed-blocks
-                             (apply concat
-                                    (for [{:block/keys [ref-blocks]} blocks]
-                                      (map (fn [ref-block]
-                                             [:block/refed-blocks (last ref-block)])
-                                           ref-blocks))))
+                               (for [{:block/keys [refs]} blocks]
+                                 (mapcat (fn [ref]
+                                           (when-let [block (if (and (map? ref) (:block/name ref))
+                                                              (db-utils/entity [:block/name (:block/name ref)])
+                                                              (db-utils/entity ref))]
+                                             [[:page/blocks (:db/id (:block/page block))]
+                                              ;; [:block/refed-blocks (:db/id block)]
+                                              ]))
+                                         refs))))
                             (distinct))
               refed-pages (map
                            (fn [[k page-id]]
-                             (if (= k :page/refed-blocks)
+                             (if (= k :block/refed-blocks)
                                [:page/ref-pages page-id]))
-                           handler-keys)
+                            related-keys)
+              all-refed-blocks (get-current-repo-refs-keys)
               custom-queries (some->>
                               (filter (fn [v]
                                         (and (= (first v) (state/get-current-repo))
@@ -272,53 +277,57 @@
                                    (vec (drop 1 v)))))]
           (->>
            (util/concat-without-nil
-            handler-keys
+            related-keys
             refed-pages
+            all-refed-blocks
             custom-queries
             block-blocks)
            distinct)))
       [[key]])))
 
+(defn refresh!
+  [repo-url {:keys [key data] :as handler-opts}]
+  (let [related-keys (get-related-keys handler-opts)
+        db (conn/get-conn repo-url)]
+    (doseq [related-key related-keys]
+      (let [related-key (vec (cons repo-url related-key))]
+        (when-let [cache (get @query-state related-key)]
+          (let [{:keys [query inputs transform-fn query-fn inputs-fn]} cache]
+            (when (or query query-fn)
+              (let [new-result (profile
+                                "takes"
+                                (->
+                                 (cond
+                                   query-fn
+                                   (profile
+                                    "Query:"
+                                    (doall (query-fn db)))
+
+                                   inputs-fn
+                                   (let [inputs (inputs-fn)]
+                                     (apply d/q query db inputs))
+
+                                   (keyword? query)
+                                   (db-utils/get-key-value repo-url query)
+
+                                   (seq inputs)
+                                   (apply d/q query db inputs)
+
+                                   :else
+                                   (d/q query db))
+                                 transform-fn))]
+                (set-new-result! related-key new-result)))))))))
+
 (defn transact-react!
-  [repo-url tx-data {:keys [key data files-db?] :as handler-opts
-                     :or {files-db? false}}]
+  [repo-url tx-data {:keys [key data] :as handler-opts}]
   (when-not config/publishing?
     (let [repo-url (or repo-url (state/get-current-repo))
           tx-data (->> (util/remove-nils tx-data)
                        (remove nil?))
-          get-conn (fn [] (if files-db?
-                            (conn/get-files-conn repo-url)
-                            (conn/get-conn repo-url false)))]
+          get-conn (fn [] (conn/get-conn repo-url false))]
       (when (and (seq tx-data) (get-conn))
-        (let [tx-result (d/transact! (get-conn) (vec tx-data))
-              db (:db-after tx-result)
-              handler-keys (get-handler-keys handler-opts)]
-          (doseq [handler-key handler-keys]
-            (let [handler-key (vec (cons repo-url handler-key))]
-              (when-let [cache (get @query-state handler-key)]
-                (let [{:keys [query inputs transform-fn query-fn inputs-fn]} cache]
-                  (when (or query query-fn)
-                    (let [new-result (->
-                                      (cond
-                                        query-fn
-                                        (profile
-                                         "Query:"
-                                         (doall (query-fn db)))
-
-                                        inputs-fn
-                                        (let [inputs (inputs-fn)]
-                                          (apply d/q query db inputs))
-
-                                        (keyword? query)
-                                        (db-utils/get-key-value repo-url query)
-
-                                        (seq inputs)
-                                        (apply d/q query db inputs)
-
-                                        :else
-                                        (d/q query db))
-                                      transform-fn)]
-                      (set-new-result! handler-key new-result))))))))))))
+        (d/transact! (get-conn) (vec tx-data))
+        (refresh! repo-url handler-opts)))))
 
 (defn set-key-value
   [repo-url key value]

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

@@ -37,8 +37,10 @@
 
 (defn group-by-page
   [blocks]
-  (some->> blocks
-           (group-by :block/page)))
+  (if (:block/page (first blocks))
+    (some->> blocks
+             (group-by :block/page))
+    blocks))
 
 (defn group-by-file
   [blocks]
@@ -114,3 +116,8 @@
    (when-let [db (conn/get-conn repo-url)]
      (some-> (d/entity db key)
              key))))
+
+(defn q
+  [query & inputs]
+  (when-let [repo (state/get-current-repo)]
+    (apply d/q query (conn/get-conn repo) inputs)))

+ 107 - 66
src/main/frontend/db_schema.cljs

@@ -1,100 +1,141 @@
 (ns frontend.db-schema)
 
-(defonce version "0.0.1")
+(defonce version "0.0.2")
 
-(def files-db-schema
-  {:file/path {:db/unique :db.unique/identity}
-   :file/content {}
-   :file/size {}
-   :file/handle {}})
-
-;; A page can corresponds to multiple files (same title),
-;; a month journal file can have multiple pages,
-;; also, each block can be treated as a page too.
+;; A page is a special block, a page can corresponds to multiple files with the same ":block/name".
 (def schema
   {:schema/version  {}
    :db/type         {}
    :db/ident        {:db/unique :db.unique/identity}
+
    :db/encrypted?    {}
    :db/encryption-keys {}
+
    ;; user
-   :me/name  {}
+   :me/name {}
    :me/email {}
    :me/avatar {}
 
    ;; Git
-   :repo/url        {:db/unique :db.unique/identity}
-   :repo/cloned?    {}
-   :git/status {}
-   :git/last-pulled-at {}
-   ;; last error, better we should record all the errors
-   :git/error {}
+   :repo/url {:db/unique :db.unique/identity}
 
-   ;; file
-   :file/path       {:db/unique :db.unique/identity}
-   :file/created-at {}
-   :file/last-modified-at {}
+   :recent/pages {}
 
-   ;; toggle to comment this line to force to clone
-   :release/re-clone? {}
+   :block/type {}
+   :block/uuid {:db/unique :db.unique/identity}
+   :block/parent {:db/valueType :db.type/ref}
+   :block/left {:db/valueType :db.type/ref}
 
-   :recent/pages    {}
-
-   :page/name       {:db/unique      :db.unique/identity}
-   :page/original-name {:db/unique      :db.unique/identity}
-   :page/file       {:db/valueType   :db.type/ref}
-   :page/properties {}
-   :page/alias      {:db/valueType   :db.type/ref
-                     :db/cardinality :db.cardinality/many}
-   :page/tags       {:db/valueType   :db.type/ref
-                     :db/cardinality :db.cardinality/many}
-   :page/journal?   {}
-   :page/journal-day {}
-
-   ;; block
-   :block/uuid   {:db/unique      :db.unique/identity}
-   :block/file   {:db/valueType   :db.type/ref}
+   ;; :markdown, :org
    :block/format {}
+
+   ;; mldoc parsed ast
    :block/title {}
+
    ;; belongs to which page
-   :block/page   {:db/valueType   :db.type/ref
-                  :db/index       true}
-   ;; referenced pages
-   :block/ref-pages {:db/valueType   :db.type/ref
-                     :db/cardinality :db.cardinality/many}
+   :block/page {:db/valueType :db.type/ref
+                :db/index true}
+   ;; reference blocks
+   :block/refs {:db/valueType :db.type/ref
+                :db/cardinality :db.cardinality/many}
    ;; referenced pages inherited from the parents
-   :block/path-ref-pages {:db/valueType   :db.type/ref
-                          :db/cardinality :db.cardinality/many}
+   :block/path-refs {:db/valueType   :db.type/ref
+                     :db/cardinality :db.cardinality/many}
 
-   ;; Referenced pages
-   ;; Notice: it's only for org mode, :tag1:tag2:
-   ;; Markdown tags will be only stored in :block/ref-pages
-   :block/tags {:db/valueType   :db.type/ref
+   ;; for pages
+   :block/tags {:db/valueType :db.type/ref
                 :db/cardinality :db.cardinality/many}
 
-   ;; referenced blocks
-   :block/ref-blocks {:db/valueType   :db.type/ref
-                      :db/cardinality :db.cardinality/many}
-   :block/embed-blocks {:db/valueType   :db.type/ref
-                        :db/cardinality :db.cardinality/many}
-   :block/embed-pages {:db/valueType   :db.type/ref
-                       :db/cardinality :db.cardinality/many}
+   ;; for pages
+   :block/alias {:db/valueType :db.type/ref
+                 :db/cardinality :db.cardinality/many}
+
+   ;; full-text for current block
    :block/content {}
-   :block/anchor {}
+
+   ;; todo keywords, e.g. "TODO", "DOING", "DONE"
    :block/marker {}
+
+   ;; "A", "B", "C"
    :block/priority {}
+
+   ;; TODO: remove
+   ;; 1, 2, 3, etc.
    :block/level {}
-   ;; :start-pos :end-pos
+   ;; TODO: remove
    :block/meta {}
+
+   ;; block key value properties
    :block/properties {}
+
+   ;; parsed ast
    :block/body {}
+
+   ;; first block that's not a heading or unordered list
    :block/pre-block? {}
-   :block/collapsed? {}
-   :block/children {:db/valueType   :db.type/ref
-                    :db/cardinality :db.cardinality/many
-                    :db/unique :db.unique/identity}
+
+   ;; scheduled day
    :block/scheduled {}
-   :block/scheduled-ast {}
+
+   ;; deadline day
    :block/deadline {}
-   :block/deadline-ast {}
-   :block/repeated? {}})
+
+   ;; whether blocks is a repeated block (usually a task)
+   :block/repeated? {}
+
+   :block/created-at {}
+   :block/updated-at {}
+
+   ;; page additional attributes
+   ;; page's name, lowercase
+   :block/name {:db/unique :db.unique/identity}
+   ;; page's original name
+   :block/original-name {:db/unique :db.unique/identity}
+   ;; whether page's is a journal
+   :block/journal? {}
+   :block/journal-day {}
+
+   ;; block's file
+   :block/file {:db/valueType :db.type/ref}
+
+   ;; file
+   :file/path {:db/unique :db.unique/identity}
+   ;; only store the content of logseq's files
+   :file/content {}
+   :file/handle {}
+   ;; :file/created-at {}
+   ;; :file/last-modified-at {}
+   ;; :file/size {}
+   ;; :file/handle {}
+
+   ;; git
+   :repo/cloned? {}
+   :git/status {}
+   :git/last-pulled-at {}
+   ;; last error, better we should record all the errors
+   :git/error {}
+
+   })
+
+(def retract-attributes
+  #{
+    :block/refs
+    :block/path-refs
+    :block/tags
+    :block/alias
+    :block/marker
+    :block/priority
+    :block/scheduled
+    :block/deadline
+    :block/repeated?
+    :block/pre-block?
+    :block/level
+    :block/heading-level
+    :block/type
+    :block/title
+    :block/body
+    :block/properties
+    :block/created-at
+    :block/updated-at
+    }
+  )

+ 7 - 0
src/main/frontend/debug.cljs

@@ -0,0 +1,7 @@
+(ns frontend.debug
+  (:require [cljs.pprint :as pprint]))
+
+(defn pprint
+  [& xs]
+  (doseq [x xs]
+    (pprint/pprint x)))

+ 26 - 233
src/main/frontend/dicts.cljs

@@ -1,76 +1,15 @@
 (ns frontend.dicts
-  (:require [tongue.core :as tongue]
-            [frontend.config :as config]))
-
-;; TODO
-;; - [ ] Localizing Number Formats
-;; - [ ] Localizing Dates
-
+  (:require [frontend.config :as config]
+            [shadow.resource :as rc]
+            [tongue.core :as tongue]))
 
 (def dicts
-  {:en {:tutorial/text "---
-title: $today
----
-
-## Hi, welcome to Logseq!
-:PROPERTIES:
-:heading: true
-:END:
-## Logseq is a _privacy-first_, _open-source_ platform for _knowledge_ sharing and management.
-## This is a 3 minute tutorial on how to use Logseq. Let's get started!
-## (Feel free to edit anything, no change will be saved at this moment. If you do want to persist your work, click the **top-right** corner of the screen to connect Logseq to either Github or local directory.)
-## Here are some tips might be useful.
-#+BEGIN_TIP
-Click to edit any block.
-Type `Enter` to create a new block.
-Type `Shift+Enter` to create a new line.
-Type `/` to show all the commands.
-#+END_TIP
-## 1. Let's create a page called [[How to take dummy notes?]]. You can click it to go to that page, or you can `Shift+Click` to open it in the right sidebar! Now you should see both _Linked References_ and _Unlinked References_.
-## 2. Let's reference some blocks on [[How to take dummy notes?]], you can `Shift+Click` any block reference to open it in the right sidebar. Try making
-some changes on the right sidebar, those referenced blocks will be changed too!
-### ((5f713e91-8a3c-4b04-a33a-c39482428e2d)) : This is a block reference.
-### ((5f713ea8-8cba-403d-ac00-9964b1ec7190)) : This is another block reference.
-## 3. Do you support tags?
-### Of course, this is a #dummy tag.
-## 4. Do you support tasks like todo/doing/done and priorities?
-### Yes, type `/` and pick your favorite todo keyword or priority (A/B/C).
-### NOW [#A] A dummy tutorial on \"How to take dummy notes?\"
-### LATER [#A] Check out this awesome video by [:a {:href \"https://twitter.com/EdTravelling\" :target \"_blank\"} \"@EdTravelling\"], which shows how to use logseq to open your local directory.
-
-[:div.video-wrapper.mb-4
-        [:iframe
-         {:allowFullScreen \"allowfullscreen\"
-          :allow
-          \"accelerometer; autoplay; encrypted-media; gyroscope\"
-        :frameBorder \"0\"
-        :src \"https://www.youtube.com/embed/Afmqowr0qEQ\"
-        :height \"367\"
-        :width \"653\"}]]
-### DONE Create a page
-### CANCELED [#C] Write a page with more than 1000 blocks
-## That's it! You can create more bullets or open a local directory to import some notes now!
-## You can also download our desktop app at https://github.com/logseq/logseq/releases
-"
-        :tutorial/dummy-notes "---
-title: How to take dummy notes?
----
-
-## Hello, I'm a block!
-:PROPERTIES:
-:id: 5f713e91-8a3c-4b04-a33a-c39482428e2d
-:END:
-### I'm a child block!
-### I'm another child block!
-## Hey, I'm another block!
-:PROPERTIES:
-:id: 5f713ea8-8cba-403d-ac00-9964b1ec7190
-:END:
-"
+  {:en {:tutorial/text (rc/inline "tutorial-en.md")
+        :tutorial/dummy-notes (rc/inline "dummy-notes-en.md")
         :on-boarding/title "Hi, welcome to Logseq!"
         :on-boarding/sharing "sharing"
         :on-boarding/is-a " is a "
-        :on-boarding/vision "A privacy-first, open-source platform for knowledge sharing and management."
+        :on-boarding/vision "A privacy-first, open-source platform for knowledge management and collaboration."
         :on-boarding/local-first "local-first"
         :on-boarding/non-linear "non-linear"
         :on-boarding/outliner "outliner"
@@ -134,42 +73,15 @@ title: How to take dummy notes?
         :help/block-reference "Block Reference"
         :help/key-commands "Key Commands"
         :help/working-with-lists " (working with lists)"
-        :help/indent-block-tab "Indent Block Tab"
-        :help/unindent-block "Unindent Block"
-        :help/move-block-up "Move Block Up"
-        :help/move-block-down "Move Block Down"
-        :help/create-new-block "Create New Block"
-        :help/new-line-in-block "New Line in Block"
         :help/select-nfs-browser "Please use another browser (like latest chrome) which support NFS features to open local directory."
         :undo "Undo"
         :redo "Redo"
-        :help/zoom-in "Zoom In/Forward"
-        :help/zoom-out "Zoom out/Back"
-        :help/follow-link-under-cursor "Follow link under cursor"
-        :help/open-link-in-sidebar "Open link in Sidebar"
-        :expand "Expand"
-        :collapse "Collapse"
-        :select-block-above "Select Block Above"
-        :select-block-below "Select Block Below"
-        :select-all-blocks "Select All Blocks"
         :general "General"
         :more "More"
         :search/result-for "Search result for "
         :search/items "items"
-        :help/toggle "Toggle help"
-        :help/git-commit-message "Git commit message"
-        :help/full-text-search "Full Text Search"
-        :help/page-search "Search in the current page"
         :help/context-menu "Context Menu"
         :help/fold-unfold "Fold/Unfold blocks (when not in edit mode)"
-        :help/toggle-doc-mode "Toggle document mode"
-        :help/toggle-contents "Toggle Contents"
-        :help/toggle-theme "Toggle between dark/light theme"
-        :help/toggle-right-sidebar "Toggle right sidebar"
-        :help/toggle-settings "Toggle settings"
-        :help/toggle-insert-new-block "Toggle Enter/Alt+Enter for inserting new block"
-        :help/jump-to-journals "Jump to Journals"
-        :formatting "Formatting"
         :help/markdown-syntax "Markdown syntax"
         :help/org-mode-syntax "Org mode syntax"
         :bold "Bold"
@@ -222,12 +134,11 @@ title: How to take dummy notes?
         :project/location "All published pages will be located under"
         :project/sync-settings "Sync project settings"
         :page/presentation-mode "Presentation mode (Powered by Reveal.js)"
-        :page/edit-properties-placeholder "Click here to edit this page's properties"
+        :page/edit-properties-placeholder "Properties"
         :page/delete-success "Page {1} was deleted successfully!"
         :page/delete-confirmation "Are you sure you want to delete this page and its file?"
         :page/rename-to "Rename \"{1}\" to:"
         :page/priority "Priority \"{1}\""
-        :page/re-index "Re-index this page"
         :page/copy-to-json "Copy the whole page as JSON"
         :page/rename "Rename page"
         :page/open-in-finder "Open in directory"
@@ -244,7 +155,7 @@ title: How to take dummy notes?
         :page/show-journals "Show Journals"
         :page/show-name "Show page name"
         :page/hide-name "Hide page name"
-        :page/name "Page name"
+        :block/name "Page name"
         :page/last-modified "Last modified at"
         :page/new-title "What's your new page title?"
         :page/earlier "Earlier"
@@ -291,6 +202,8 @@ title: How to take dummy notes?
         :content/click-to-edit "Click to edit"
         :settings-page/edit-config-edn "Edit config.edn (for current repo)"
         :settings-page/show-brackets "Show brackets"
+        :settings-page/disable-sentry "Disable Sentry.io (for error tracking)"
+        :settings-page/preferred-outdenting "Enable logical outdenting"
         :settings-page/custom-date-format "Preferred journal format"
         :settings-page/preferred-file-format "Preferred file format"
         :settings-page/preferred-workflow "Preferred workflow"
@@ -300,6 +213,8 @@ title: How to take dummy notes?
         :settings-page/home-default-page "Set the default home page"
         :settings-page/enable-block-time "Enable block timestamps"
         :settings-page/dont-use-other-peoples-proxy-servers "Don't use other people's proxy servers. It's very dangerous, which could make your token and notes stolen. Logseq will not be responsible for this loss if you use other people's proxy servers. You can deploy it yourself, check "
+        :settings-page/clear-cache "Clear cache"
+        :settings-page/clear "Clear"
         :settings-page/custom-cors-proxy-server "Custom CORS proxy server"
         :settings-page/developer-mode "Developer mode"
         :settings-page/enable-developer-mode "Enable developer mode"
@@ -331,6 +246,7 @@ title: How to take dummy notes?
         :export-markdown "Export as Markdown"
         :export-public-pages "Export public pages"
         :export-edn "Export as EDN"
+        :convert-markdown "Convert Markdown headings to unordered lists (# -> -)"
         :all-graphs "All graphs"
         :all-pages "All pages"
         :all-files "All files"
@@ -362,7 +278,9 @@ title: How to take dummy notes?
         :open-a-directory "Open a local directory"
         :user/delete-account "Delete account"
         :user/delete-your-account "Delete your account"
-        :user/delete-account-notice "All your published pages on logseq.com will be deleted."}
+        :user/delete-account-notice "All your published pages on logseq.com will be deleted."
+
+        :help/shortcut-page-title "Learn & Customize Shortcuts"}
 
    :de {:help/about "Über Logseq"
         :help/bug "Fehlerbericht"
@@ -382,36 +300,12 @@ title: How to take dummy notes?
         :help/block-reference "Blockverweis"
         :help/key-commands "Tastenbefehle"
         :help/working-with-lists " (mit Listen arbeiten)"
-        :help/indent-block-tab "Block einrücken"
-        :help/unindent-block "Block ausrücken"
-        :help/move-block-up "Block nach oben verschieben"
-        :help/move-block-down "Block nach unten verschieben"
-        :help/create-new-block "Neuen Block erstellen"
-        :help/new-line-in-block "Neue Zeile innerhalb des Blocks erstellen"
         :help/select-nfs-browser "Bitte einen anderen Browser verwenden (z. B. den neuesten Chrome), der NFS-Funktionen unterstützt, um lokale Verzeichnisse zu öffnen."
         :undo "Rückgängig machen"
         :redo "Wiederholen"
-        :help/zoom-in "Heranzoomen"
-        :help/zoom-out "Herauszoomen"
-        :help/follow-link-under-cursor "Link unter dem Cursor folgen"
-        :help/open-link-in-sidebar "Link in Seitenleiste öffnen"
-        :expand "Erweitern"
-        :collapse "Zusammenklappen"
-        :select-block-above "Block oberhalb auswählen"
-        :select-block-below "Block unterhalb auswählen"
-        :select-all-blocks "Alle Blöcke auswählen"
         :general "Allgemein"
-        :help/toggle "Hilfe aktivieren"
-        :help/git-commit-message "Git Commit-Nachricht"
-        :help/full-text-search "Volltextsuche"
         :help/context-menu "Kontextmenü"
         :help/fold-unfold "Blöcke ein-/ausklappen (wenn nicht im Bearbeitungsmodus)"
-        :help/toggle-doc-mode "Dokumentenmodus umschalten"
-        :help/toggle-theme "Umschalten zwischen dunklem/hellem Thema"
-        :help/toggle-right-sidebar "Rechte Seitenleiste umschalten"
-        :help/toggle-insert-new-block "Umschalten von Enter/Alt+Enter zum Einfügen eines neuen Blocks"
-        :help/jump-to-journals "Zu Journalen springen"
-        :formatting "Formatierung"
         :help/markdown-syntax "Markdown-Syntax"
         :help/org-mode-syntax "Org-Mode-Syntax"
         :bold "Fett"
@@ -468,7 +362,6 @@ title: How to take dummy notes?
         :page/delete-confirmation "Diese Seite und die zugehörige Datei löschen?"
         :page/rename-to "\"{1}\" umbenennen nach:"
         :page/priority "Priorität \"{1}\""
-        :page/re-index "Diese Seite neu indizieren"
         :page/copy-to-json "Gesamte Seite als JSON kopieren"
         :page/rename "Seite umbenennen"
         :page/open-in-finder "Im Verzeichnis öffnen"
@@ -485,7 +378,7 @@ title: How to take dummy notes?
         :page/show-journals "Journal anzeigen"
         :page/show-name "Seitennamen anzeigen"
         :page/hide-name "Seitennamen verbergen"
-        :page/name "Seitenname"
+        :block/name "Seitenname"
         :page/last-modified "Zuletzt geändert am"
         :page/new-title "Wie lautet der neue Seitenname?"
         :publishing/pages "Seiten"
@@ -609,36 +502,12 @@ title: How to take dummy notes?
         :help/block-reference "Référence à un Bloc"
         :help/key-commands "Key Commands"
         :help/working-with-lists " (working with lists)"
-        :help/indent-block-tab "Indenter un Bloc vers la droite"
-        :help/unindent-block "Indenter un Bloc vers la gauche"
-        :help/move-block-up "Déplacer un bloc au dessus"
-        :help/move-block-down "Déplacer un bloc en dessous"
-        :help/create-new-block "Créer un nouveau bloc"
-        :help/new-line-in-block "Aller à la ligne dans un bloc"
         :help/select-nfs-browser "Please use another browser (like latest chrome) which support NFS features to open local directory."
         :undo "Annuler"
         :redo "Redo"
-        :help/zoom-in "Zoomer"
-        :help/zoom-out "Dézoomer"
-        :help/follow-link-under-cursor "Suivre le lien sous le curseur"
-        :help/open-link-in-sidebar "Ouvrir le lien dans la barre latérale"
-        :expand "Etendre"
-        :collapse "Réduire"
-        :select-block-above "Sélectionner le bloc au dessus"
-        :select-block-below "Sélectionner le bloc en dessous"
-        :select-all-blocks "Sélectionner tous les blocs"
         :general "Général"
-        :help/toggle "Afficher l'aide"
-        :help/git-commit-message "Message de commit Git"
-        :help/full-text-search "Recherche globale dans le texte"
         :help/context-menu "Menu contextuel"
         :help/fold-unfold "Plier/Déplier les blocs (hors mode édition)"
-        :help/toggle-doc-mode "Intervertir le mode document"
-        :help/toggle-theme "Intervertir le thème foncé/clair"
-        :help/toggle-right-sidebar "Afficher/cacher la barre latérale"
-        :help/toggle-insert-new-block "Activer Entreée ou Alt+Enter pour insérer un bloc"
-        :help/jump-to-journals "Aller au Journal"
-        :formatting "Formats"
         :help/markdown-syntax "Syntaxe Markdown"
         :help/org-mode-syntax "Syntaxe Org mode"
         :bold "Gras"
@@ -694,7 +563,6 @@ title: How to take dummy notes?
         :page/delete-confirmation "Etes-vous sûr de vouloir supprimer la page ?"
         :page/rename-to "Renommer \"{1}\" en:"
         :page/priority "Priorité \"{1}\""
-        :page/re-index "Indexer à nouveau cette page"
         :page/copy-to-json "Copier la page au format JSON"
         :page/rename "Renommer la page"
         :page/make-public "Rendre la page publique"
@@ -708,7 +576,7 @@ title: How to take dummy notes?
         :page/show-journals "Afficher le Journal"
         :page/show-name "Afficher le nom de la page"
         :page/hide-name "Cacher le nom de la page"
-        :page/name "Nom de la page"
+        :block/name "Nom de la page"
         :page/last-modified "Dernières modifications à"
         :page/new-title "Quel est le nouveau titre de la page ?"
         :publishing/pages "Pages"
@@ -858,51 +726,25 @@ title: How to take dummy notes?
            :help/shortcuts "快捷键"
            :help/shortcuts-triggers "触发"
            :help/shortcut "快捷键"
+           :help/shortcut-page-title "完整快捷键"
            :help/slash-autocomplete "Slash 自动提示"
            :help/block-content-autocomplete "块内容 (Src, Quote, Query 等) 自动完成"
            :help/reference-autocomplete "页面引用自动补全"
            :help/block-reference "块引用"
            :help/key-commands "关键命令"
            :help/working-with-lists " (与列表相关)"
-           :help/indent-block-tab "缩进块标签"
-           :help/unindent-block "取消缩进块"
-           :help/move-block-up "向上移动块"
-           :help/move-block-down "向下移动块"
-           :help/create-new-block "创建块"
-           :help/new-line-in-block "块中新建行"
            :help/select-nfs-browser "请选择支持nfs的浏览来使用logseq本地文件夹功能, 如最新的Chrome浏览器."
            :text/image "图片"
            :asset/confirm-delete "确定要删除{1}吗?"
            :asset/physical-delete "同时删除本地文件(目前不可撤销)"
            :undo "撤销"
            :redo "重做"
-           :help/zoom-in "聚焦"
-           :help/zoom-out "退出聚焦"
-           :help/follow-link-under-cursor "跟随光标下的链接"
-           :help/open-link-in-sidebar "在侧边栏打开"
-           :expand "展开"
-           :collapse "折叠"
-           :select-block-above "选择上方的块"
-           :select-block-below "选择下方的块"
-           :select-all-blocks "选择所有块"
            :general "常规​​​​​"
            :more "更多"
            :search/result-for "更多搜索结果 "
            :search/items "条目"
-           :help/toggle "显示/关闭帮助"
-           :help/git-commit-message "提交消息"
-           :help/full-text-search "全文搜索"
-           :help/page-search "在当前页面搜索"
            :help/context-menu "右键菜单"
            :help/fold-unfold "折叠/展开方块(不在编辑模式中)"
-           :help/toggle-doc-mode "切换文档模式"
-           :help/toggle-contents "打开/关闭目录"
-           :help/toggle-theme "“在暗色/亮色主题之间切换”"
-           :help/toggle-right-sidebar "启用/关闭右侧栏"
-           :help/toggle-settings "显示/关闭设置"
-           :help/toggle-insert-new-block "切换 Enter/Alt+Enter 以插入新块"
-           :help/jump-to-journals "跳转到日记"
-           :formatting "格式化"
            :help/markdown-syntax "Markdown 语法"
            :help/org-mode-syntax "Org Mode 语法"
            :bold "粗体"
@@ -960,7 +802,6 @@ title: How to take dummy notes?
            :page/delete-confirmation "您确定要删除此页面和文件吗?"
            :page/rename-to "重命名 \"{1}\" 至:"
            :page/priority "优先级 \"{1}\""
-           :page/re-index "对此页面重新建立索引"
            :page/copy-to-json "将整页以 JSON 格式复制"
            :page/rename "重命名本页"
            :page/open-in-finder "打开文件对应目录"
@@ -977,7 +818,7 @@ title: How to take dummy notes?
            :page/show-journals "显示日志"
            :page/show-name "显示页面名"
            :page/hide-name "隐藏页面名"
-           :page/name "页面名称"
+           :block/name "页面名称"
            :page/last-modified "最后更改于"
            :page/new-title "请输入新页面的名字:"
            :page/earlier "之前"
@@ -1046,6 +887,7 @@ title: How to take dummy notes?
            :re-index "重新建立索引"
            :export-json "以 JSON 格式导出"
            :export-markdown "以 Markdown 格式导出"
+           :convert-markdown "转换 Markdown 格式(Unordered list 或 Heading)"
            :unlink "解除绑定"
            :search (if config/publishing?
                      "搜索"
@@ -1089,6 +931,7 @@ title: How to take dummy notes?
            :user/delete-your-account "删除你的帐号"
            :user/delete-account-notice "你在 logseq.com 发布的页面(假如有的话)也会被删除。"}
 
+
    :zh-Hant {:on-boarding/title "你好,歡迎使用 Logseq!"
              :on-boarding/sharing "分享"
              :on-boarding/is-a " 是一個 "
@@ -1153,36 +996,12 @@ title: How to take dummy notes?
              :help/block-reference "塊引用"
              :help/key-commands "關鍵命令"
              :help/working-with-lists " (與列表相關)"
-             :help/indent-block-tab "縮進塊標簽"
-             :help/unindent-block "取消縮進塊"
-             :help/move-block-up "向上移動塊"
-             :help/move-block-down "向下移動塊"
-             :help/create-new-block "創建塊"
-             :help/new-line-in-block "塊中新建行"
              :help/select-nfs-browser "Please use another browser (like latest chrome) which support NFS features to open local directory."
              :undo "撤銷"
              :redo "重做"
-             :help/zoom-in "聚焦"
-             :help/zoom-out "推出聚焦"
-             :help/follow-link-under-cursor "跟隨光標下的鏈接"
-             :help/open-link-in-sidebar "在側邊欄打開"
-             :expand "展開"
-             :collapse "折疊"
-             :select-block-above "選擇上方的塊"
-             :select-block-below "選擇下方的塊"
-             :select-all-blocks "選擇所有塊"
              :general "常規​​​​​"
-             :help/toggle "顯示/關閉幫助"
-             :help/git-commit-message "提交消息"
-             :help/full-text-search "全文搜索"
              :help/context-menu "右鍵菜單"
              :help/fold-unfold "折疊/展開方塊(不在編輯模式中)"
-             :help/toggle-doc-mode "切換文檔模式"
-             :help/toggle-theme "“在暗色/亮色主題之間切換”"
-             :help/toggle-right-sidebar "啟用/關閉右側欄"
-             :help/toggle-insert-new-block "切換 Enter/Alt+Enter 以插入新塊"
-             :help/jump-to-journals "跳轉到日記"
-             :formatting "格式化"
              :help/markdown-syntax "Markdown 語法"
              :help/org-mode-syntax "Org Mode 語法"
              :bold "粗體"
@@ -1237,7 +1056,6 @@ title: How to take dummy notes?
              :page/delete-confirmation "您確定要刪除此頁面嗎?"
              :page/rename-to "重命名 \"{1}\" 至:"
              :page/priority "優先級 \"{1}\""
-             :page/re-index "對此頁面重新建立索引"
              :page/copy-to-json "將整頁以 JSON 格式復制"
              :page/rename "重命名本頁"
              :page/make-public "導出 HTML 時發布本頁面"
@@ -1251,7 +1069,7 @@ title: How to take dummy notes?
              :page/show-journals "顯示日志"
              :page/show-name "顯示頁面名"
              :page/hide-name "隱藏頁面名"
-             :page/name "頁面名稱:"
+             :block/name "頁面名稱:"
              :page/last-modified "最後更改於"
              :page/new-title "請輸入新頁面的名字:"
              :page/load-more-journals "載入更多"
@@ -1312,6 +1130,7 @@ title: How to take dummy notes?
              :re-index "重新建立索引"
              :export-json "以 JSON 格式導出"
              :export-markdown "以 Markdown 格式導出"
+             :convert-markdown "轉換 Markdown 格式(Unordered list 或 Heading)"
              :unlink "解除綁定"
              :search (if config/publishing?
                        "搜索"
@@ -1406,36 +1225,12 @@ title: How to take dummy notes?
         :help/block-reference "Blok verwysing"
         :help/key-commands "Sleutel instruksies"
         :help/working-with-lists " (werk met lyste)"
-        :help/indent-block-tab "Ingekeepte blok oortjie"
-        :help/unindent-block "Oningekeepte blok"
-        :help/move-block-up "Skuif Blok Boontoe"
-        :help/move-block-down "Skuif Blok Ondertoe"
-        :help/create-new-block "Skep 'n nuwe blok"
-        :help/new-line-in-block "Nuwe lyn in blok"
         :help/select-nfs-browser "Please use another browser (like latest chrome) which support NFS features to open local directory."
         :undo "Ontdoen"
         :redo "Herdoen"
-        :help/zoom-in "Zoem in"
-        :help/zoom-out "Zoem uit"
-        :help/follow-link-under-cursor "Volg die skakel onder die wyser"
-        :help/open-link-in-sidebar "Maak skakel in kantlys oop"
-        :expand "Brei uit"
-        :collapse "Vou in"
-        :select-block-above "Kies blok bo"
-        :select-block-below "Kies blok onder"
-        :select-all-blocks "Kies alle blokke"
         :general "Algemeen"
-        :help/toggle "Wissel help"
-        :help/git-commit-message "Jou git stoor boodskap"
-        :help/full-text-search "Volteks soek"
         :help/context-menu "Konteks kaart"
         :help/fold-unfold "Vou/ontvou blokke (wanneer nie in wysigings modus)"
-        :help/toggle-doc-mode "Wissel dokument modus"
-        :help/toggle-theme "Wissel tussen donker/lig temas"
-        :help/toggle-right-sidebar "Wissel regter sybalk"
-        :help/toggle-insert-new-block "Wissel Enter/Alt+enter vir die byvoeging van nuwe blokke"
-        :help/jump-to-journals "Spring na joernale"
-        :formatting "Formatering"
         :help/markdown-syntax "Markdown sintaksis"
         :help/org-mode-syntax "Org mode sintaksis"
         :bold "Vetdruk"
@@ -1490,7 +1285,6 @@ title: How to take dummy notes?
         :page/delete-confirmation "Is jy seker jy wil die bladsy uitvee?"
         :page/rename-to "Hernoem \"{1}\" na:"
         :page/priority "Prioriteit \"{1}\""
-        :page/re-index "Re-index this page"
         :page/copy-to-json "Kopieer die hele bladsy as JSON"
         :page/rename "Hernoem die bladsy"
         :page/delete "Delete page (will delete the file too)"
@@ -1502,7 +1296,7 @@ title: How to take dummy notes?
         :page/show-journals "Wys joernale"
         :page/show-name "Wys blad naam"
         :page/hide-name "Steek bladnaam weg"
-        :page/name "Page name"
+        :block/name "Page name"
         :page/last-modified "Laaste verander op"
         :page/new-title "Wat is die nuwe blad se titel?"
         :publishing/pages "Pages"
@@ -1569,7 +1363,6 @@ title: How to take dummy notes?
         :settings "Verstellings"
         :import "Invoer"
         :join-community "Sluit by die gemeenskap aan"
-        :discord-title "Ons discord groep!"
         :sign-out "Teken af"
         :help-shortcut-title "Kliek op die kortpad en ander wenke"
         :loading "Laai tans"
@@ -1594,5 +1387,5 @@ title: How to take dummy notes?
                 {:label "繁體中文" :value :zh-Hant}
                 {:label "Afrikaans" :value :af}])
 
-(def translate
+(defn translate [dicts]
   (tongue/build-translate dicts))

+ 50 - 58
src/main/frontend/extensions/code.cljs

@@ -3,7 +3,6 @@
             [frontend.util :as util]
             [goog.dom :as gdom]
             [goog.object :as gobj]
-            [medley.core :as medley]
             [frontend.db :as db]
             [frontend.state :as state]
             [frontend.handler.editor :as editor-handler]
@@ -52,43 +51,37 @@
   [editor textarea config state]
   (.save editor)
   (let [value (gobj/get textarea "value")
-        default-value (gobj/get textarea "defaultValue")
-        pos-meta (:pos-meta state)]
+        default-value (gobj/get textarea "defaultValue")]
     (when (not= value default-value)
       (cond
-       (:block/uuid config)
-       (let [block (db/pull [:block/uuid (:block/uuid config)])
-             format (:block/format block)
-             content (:block/content block)
-             {:keys [start_pos end_pos]} @pos-meta
-             prev-content (utf8/substring (utf8/encode content)
-                                          0 start_pos)
-             value (str (if (not= "\n" (last prev-content))
-                          "\n")
-                        (string/trimr value)
-                        "\n")
-             content' (utf8/insert! content start_pos end_pos value)]
-         (state/set-editor-op! :code-editor)
-         (editor-handler/save-block-if-changed! block content')
-         (let [new-pos-meta {:start_pos start_pos
-                             :end_pos (+ start_pos
-                                         (utf8/length (utf8/encode value)))}
-               old-pos-meta @pos-meta]
-           (reset! pos-meta new-pos-meta)))
+        (:block/uuid config)
+        (let [block (db/pull [:block/uuid (:block/uuid config)])
+              format (:block/format block)
+              content (:block/content block)
+              full-content (:full_content (last (:rum/args state)))]
+          (when (and full-content (string/includes? content full-content))
+            (let [lines (string/split-lines full-content)
+                  fl (first lines)
+                  ll (last lines)]
+              (when (and fl ll)
+                (let [value' (str fl "\n" value "\n" ll)
+                      ;; FIXME: What if there're multiple code blocks with the same value?
+                      content' (string/replace-first content full-content value')]
+                  (editor-handler/save-block-if-changed! block content'))))))
 
-       (:file-path config)
-       (let [path (:file-path config)
-             content (db/get-file-no-sub path)
-             value (some-> (gdom/getElement path)
-                           (gobj/get "value"))]
-         (when (and
-                (not (string/blank? value))
-                (not= (string/trim value) (string/trim content)))
-           (file-handler/alter-file (state/get-current-repo) path (string/trim value)
-                                    {:re-render-root? true})))
+        (:file-path config)
+        (let [path (:file-path config)
+              content (db/get-file-no-sub path)
+              value (some-> (gdom/getElement path)
+                            (gobj/get "value"))]
+          (when (and
+                 (not (string/blank? value))
+                 (not= (string/trim value) (string/trim content)))
+            (file-handler/alter-file (state/get-current-repo) path (string/trim value)
+                                     {:re-render-root? true})))
 
-       :else
-       nil))))
+        :else
+        nil))))
 
 (defn- text->cm-mode
   [text]
@@ -116,7 +109,10 @@
   (let [editor-atom (:editor-atom state)
         esc-pressed? (atom nil)]
     (if @editor-atom
-      @editor-atom
+      (let [editor @editor-atom
+            doc (.getDoc editor)
+            code (nth (:rum/args state) 3)]
+        (.setValue doc code))
       (let [[config id attr code] (:rum/args state)
             original-mode (get attr :data-lang)
             mode (or original-mode "javascript")
@@ -135,16 +131,12 @@
                                           :lineNumbers true
                                           :styleActiveLine true
                                           :extraKeys #js {"Esc" (fn [cm]
-                                                                  (let [save! #(save-file-or-block-when-blur-or-esc! cm textarea config state)]
-                                                                    (if-let [block-id (:block/uuid config)]
-                                                                      (let [block (db/pull [:block/uuid block-id])
-                                                                            value (.getValue cm)
-                                                                            textarea-value (gobj/get textarea "value")
-                                                                            changed? (not= value textarea-value)]
-                                                                        (if changed?
-                                                                          (save!)
-                                                                          (editor-handler/edit-block! block :max (:block/format block) block-id)))
-                                                                      (save!)))
+                                                                  (save-file-or-block-when-blur-or-esc! cm textarea config state)
+                                                                  (when-let [block-id (:block/uuid config)]
+                                                                    (let [block (db/pull [:block/uuid block-id])
+                                                                          value (.getValue cm)
+                                                                          textarea-value (gobj/get textarea "value")]
+                                                                      (editor-handler/edit-block! block :max (:block/format block) block-id)))
                                                                   ;; TODO: return "handled" or false doesn't always prevent event bubbles
                                                                   (reset! esc-pressed? true)
                                                                   (js/setTimeout #(reset! esc-pressed? false) 10))}})))]
@@ -152,35 +144,35 @@
           (let [element (.getWrapperElement editor)]
             (.on editor "blur" (fn [_cm e]
                                  (util/stop e)
+                                 (state/set-block-component-editing-mode! false)
                                  (when-not @esc-pressed?
                                    (save-file-or-block-when-blur-or-esc! editor textarea config state))))
-            (.addEventListener element "click"
+            (.addEventListener element "mousedown"
                                (fn [e]
-                                 (util/stop e)))
+                                 (state/clear-selection!)
+                                 (util/stop e)
+                                 (state/set-block-component-editing-mode! true)))
             (.save editor)
             (.refresh editor)))
         editor))))
 
 (defn- load-and-render!
   [state]
-  (let [editor-atom (:editor-atom state)]
-    (let [editor (render! state)]
-      (reset! editor-atom editor))))
+  (let [editor-atom (:editor-atom state)
+        editor (render! state)]
+    (reset! editor-atom editor)))
 
 (rum/defcs editor < rum/reactive
   {:init (fn [state]
-           (assoc state
-                  :pos-meta (atom (last (:rum/args state)))
-                  :editor-atom (atom nil)))
+           (assoc state :editor-atom (atom nil)))
    :did-mount (fn [state]
                 (load-and-render! state)
                 state)
-   :did-remount (fn [old_state state]
-                  (load-and-render! state)
-                  state)}
-  [state config id attr code pos-meta]
+   :did-update (fn [state]
+                 (load-and-render! state)
+                 state)}
+  [state config id attr code options]
   [:div.extensions__code
-   {:on-mouse-down (fn [e] (util/stop e))}
    [:div.extensions__code-lang
     (let [mode (string/lower-case (get attr :data-lang "javascript"))]
       (if (= mode "text/x-clojure")

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

@@ -80,6 +80,10 @@
         [:a.mr-2 {:on-click #(swap! *view-mode? not)}
          (util/format "View Mode (%s)" (if @*view-mode? "ON" "OFF"))]]
        [:div.draw-wrap
+        {:on-mouse-down (fn [e]
+                          (util/stop e)
+                          (state/set-block-component-editing-mode! true))
+         :on-blur #(state/set-block-component-editing-mode! false)}
         (excalidraw
          (merge
           {:on-change (fn [elements state]

+ 11 - 4
src/main/frontend/extensions/zip.cljs

@@ -2,6 +2,8 @@
   (:require ["jszip" :as JSZip]
             ["/frontend/utils" :as utils]
             [promesa.core :as p]
+            [clojure.string :as string]
+            [frontend.config :as config]
             [medley.core :as medley]))
 
 (defn make-file [content file-name args]
@@ -11,10 +13,15 @@
     (aset args "lastModified" last-modified)
     (js/File. blob-content file-name args)))
 
-(defn make-zip [repo file-name->content]
+(defn make-zip [zip-filename file-name->content repo]
   (let [zip (JSZip.)
-        folder (.folder zip repo)]
+        zip-foldername (subs zip-filename (inc (string/last-index-of zip-filename "/")))
+        src-filepath (string/replace repo config/local-db-prefix "")
+        folder (.folder zip zip-foldername)]
     (doseq [[file-name content] file-name->content]
-      (.file folder file-name content))
+      (.file folder (-> file-name
+                        (string/replace src-filepath "")
+                        (string/replace #"^/+" ""))
+             content))
     (p/let [zip-blob (.generateAsync zip #js {:type "blob"})]
-      (make-file zip-blob (str repo ".zip") {:type "application/zip"}))))
+      (make-file zip-blob (str zip-filename ".zip") {:type "application/zip"}))))

+ 1 - 1
src/main/frontend/external/roam.cljc

@@ -85,7 +85,7 @@
         level-pattern (apply str (repeat level "#"))
         properties (when (contains? @all-refed-uids uid)
                      (str
-                      (util/format ":PROPERTIES:\n:ID:%s\n:END:"
+                      (util/format "id:: %s"
                                    (str (get @uid->uuid uid)))
                       "\n"))]
     (if string

+ 3 - 7
src/main/frontend/format.cljs

@@ -36,7 +36,9 @@
   ([format]
    (mldoc/default-config format))
   ([format heading-to-list?]
-   (mldoc/default-config format heading-to-list?)))
+   (mldoc/default-config format heading-to-list?))
+  ([format heading-to-list? exporting-keep-properties?]
+   (mldoc/default-config format heading-to-list? exporting-keep-properties?)))
 
 (defn to-html
   ([content format]
@@ -62,9 +64,3 @@
   [format]
   (when-let [record (get-format-record format)]
     (protocol/loaded? record)))
-
-(def marker-pattern
-  #"^(NOW|LATER|TODO|DOING|DONE|WAITING|WAIT|CANCELED|CANCELLED|STARTED|IN-PROGRESS)?\s?")
-
-(def bare-marker-pattern
-  #"^(NOW|LATER|TODO|DOING|DONE|WAITING|WAIT|CANCELED|CANCELLED|STARTED|IN-PROGRESS){1}\s+")

+ 276 - 191
src/main/frontend/format/block.cljs

@@ -9,6 +9,7 @@
             [datascript.core :as d]
             [frontend.date :as date]
             [frontend.text :as text]
+            [frontend.util.property :as property]
             [medley.core :as medley]
             [frontend.state :as state]
             [frontend.db :as db]))
@@ -41,7 +42,8 @@
                      (when (and (not (util/starts-with? page "http:"))
                                 (not (util/starts-with? page "https:"))
                                 (not (util/starts-with? page "file:"))
-                                (or (= ext :excalidraw) (not (contains? (config/supported-formats) ext))))
+                                (or (= ext :excalidraw)
+                                    (not (contains? (config/supported-formats) ext))))
                        page)))
 
                   (and
@@ -108,10 +110,12 @@
 
                         (and (vector? block)
                              (= "Link" (first block))
-                             (map? (second block))
-                             (= "id" (:protocol (second (:url (second block))))))
-
-                        (:link (second (:url (second block))))
+                             (map? (second block)))
+                        (if (= "id" (:protocol (second (:url (second block)))))
+                          (:link (second (:url (second block))))
+                          (let [id (second (:url (second block)))]
+                            (when (text/block-ref? id)
+                             (text/block-ref-un-brackets! id))))
 
                         :else
                         nil)]
@@ -138,18 +142,12 @@
    (vector? block)
    (= "Hiccup" (first block))))
 
-(defn- timestamp-block?
+(defn timestamp-block?
   [block]
   (and
    (vector? block)
    (= "Timestamp" (first block))))
 
-(defn properties-block?
-  [block]
-  (and
-   (vector? block)
-   (= "Property_Drawer" (first block))))
-
 (defn definition-list-block?
   [block]
   (and
@@ -157,14 +155,8 @@
    (= "List" (first block))
    (:name (first (second block)))))
 
-(defn- ->schema-properties
-  [properties]
-  (-> properties
-      (update "created_at" util/safe-parse-int)
-      (update "last_modified_at" util/safe-parse-int)))
-
 (defonce non-parsing-properties
-  (atom #{"background_color"}))
+  (atom #{"background-color" "background_color"}))
 
 (defn extract-properties
   [[_ properties] _start-pos _end-pos]
@@ -183,34 +175,43 @@
                    (remove string/blank?))
         properties (->> properties
                         (medley/map-kv (fn [k v]
-                                         (let [v (string/trim v)
-                                               k (string/replace k " " "_")]
-                                           (cond
-                                             (and (= "\"" (first v) (last v))) ; wrapped in ""
-                                             [(string/lower-case k) (string/trim (subs v 1 (dec (count v))))]
-
-                                             (contains? @non-parsing-properties (string/lower-case k))
-                                             [(string/lower-case k) v]
-
-                                             :else
-                                             (let [k' (and k (string/trim (string/lower-case k)))
-                                                   v' v
-                                                   ;; built-in collections
-                                                   comma? (contains? #{"tags" "alias"} k)
-                                                   v' (if (and k' v'
-                                                               (contains? config/markers k')
+                                         (if (coll? v)
+                                           [(keyword k) v]
+                                           (let [k (name k)
+                                                v (string/trim v)
+                                                k (string/replace k " " "-")
+                                                 k (string/lower-case k)
+                                                v (cond
+                                                    (= v "true")
+                                                    true
+                                                    (= v "false")
+                                                    false
+
+                                                    (re-find #"^\d+$" v)
+                                                    (util/safe-parse-int v)
+
+                                                    (and (= "\"" (first v) (last v))) ; wrapped in ""
+                                                    (string/trim (subs v 1 (dec (count v))))
+
+                                                    (contains? @non-parsing-properties (string/lower-case k))
+                                                    v
+
+                                                    :else
+                                                    (let [v' v]
+                                                      (if (and k v'
+                                                               (contains? config/markers k)
                                                                (util/safe-parse-int v'))
                                                         (util/safe-parse-int v')
-                                                        (text/split-page-refs-without-brackets v' comma?))]
-                                               [k' v'])))))
-                        (->schema-properties))]
+                                                        (text/split-page-refs-without-brackets v' true))))]
+                                            [(keyword k) v])))))]
     {:properties properties
      :page-refs page-refs}))
 
 (defn- paragraph-timestamp-block?
   [block]
   (and (paragraph-block? block)
-       (timestamp-block? (first (second block)))))
+       (or (timestamp-block? (first (second block)))
+           (timestamp-block? (second (second block))))))
 
 (defn extract-timestamps
   [block]
@@ -232,38 +233,56 @@
                             (cond->
                              (case k
                                :scheduled
-                               {:scheduled day
-                                :scheduled-ast v}
+                               {:scheduled day}
                                :deadline
-                               {:deadline day
-                                :deadline-ast v})
+                               {:deadline day})
                               repetition
                               (assoc :repeated? true))))))]
     (apply merge m)))
 
-(defn block-tags->pages
-  [{:keys [tags] :as block}]
-  (if (seq tags)
-    (assoc block :tags (map (fn [tag]
-                              [:page/name (string/lower-case tag)]) tags))
-    block))
+(defn convert-page-if-journal
+  "Convert journal file name to user' custom date format"
+  [original-page-name]
+  (let [page-name (string/lower-case original-page-name)
+        day (date/journal-title->int page-name)]
+    (if day
+      (let [original-page-name (date/int->journal-title day)]
+        [original-page-name (string/lower-case original-page-name) day])
+      [original-page-name page-name day])))
+
+(defn page-name->map
+  [original-page-name with-id?]
+  (when original-page-name
+    (let [[original-page-name page-name journal-day] (convert-page-if-journal original-page-name)
+          m (merge
+             {:block/name page-name
+              :block/original-name original-page-name}
+             (when with-id?
+               (if-let [block (db/entity [:block/name page-name])]
+                 {}
+                 {:block/uuid (db/new-block-id)})))]
+      (if journal-day
+        (merge m
+               {:block/journal? true
+                :block/journal-day journal-day})
+        (assoc m :block/journal? false)))))
 
 (defn with-page-refs
-  [{:keys [title body tags ref-pages] :as block}]
-  (let [ref-pages (->> (concat tags ref-pages)
-                       (remove string/blank?)
-                       (distinct))
-        ref-pages (atom ref-pages)]
+  [{:keys [title body tags refs marker priority] :as block} with-id?]
+  (let [refs (->> (concat tags refs [marker priority])
+                  (remove string/blank?)
+                  (distinct))
+        refs (atom refs)]
     (walk/postwalk
      (fn [form]
        (when-let [page (get-page-reference form)]
-         (swap! ref-pages conj page))
+         (swap! refs conj page))
        (when-let [tag (get-tag form)]
          (when (util/tag-valid? tag)
-           (swap! ref-pages conj tag)))
+           (swap! refs conj tag)))
        form)
      (concat title body))
-    (let [ref-pages (remove string/blank? @ref-pages)
+    (let [refs (remove string/blank? @refs)
           children-pages (->> (mapcat (fn [p]
                                         (when (and (string/includes? p "/")
                                                    (not (string/starts-with? p "../"))
@@ -271,10 +290,12 @@
                                                    (not (string/starts-with? p "http")))
                                           ;; Don't create the last page for now
                                           (butlast (string/split p #"/"))))
-                                      ref-pages)
+                                      refs)
                               (remove string/blank?))
-          ref-pages (distinct (concat ref-pages children-pages))]
-      (assoc block :ref-pages ref-pages))))
+          refs (->> (distinct (concat refs children-pages))
+                    (remove nil?))
+          refs (map (fn [ref] (page-name->map ref with-id?)) refs)]
+      (assoc block :refs refs))))
 
 (defn with-block-refs
   [{:keys [title body] :as block}]
@@ -285,27 +306,14 @@
          (swap! ref-blocks conj block))
        form)
      (concat title body))
-    (let [ref-blocks (remove string/blank? @ref-blocks)]
-      (assoc block :ref-blocks (map
-                                (fn [id]
-                                  [:block/uuid (medley/uuid id)])
-                                ref-blocks)))))
-
-(defn update-src-pos-meta!
-  [{:keys [body] :as block}]
-  (let [body (walk/postwalk
-              (fn [form]
-                (if (and (vector? form)
-                         (= (first form) "Src")
-                         (map? (:pos_meta (second form))))
-                  (let [{:keys [start_pos end_pos]} (:pos_meta (second form))
-                        new_start_pos (- start_pos (get-in block [:meta :start-pos]))]
-                    ["Src" (assoc (second form)
-                                  :pos_meta {:start_pos new_start_pos
-                                             :end_pos (+ new_start_pos (- end_pos start_pos))})])
-                  form))
-              body)]
-    (assoc block :body body)))
+    (let [ref-blocks (->> @ref-blocks
+                          (filter util/uuid-string?))
+          ref-blocks (map
+                       (fn [id]
+                         [:block/uuid (medley/uuid id)])
+                       ref-blocks)
+          refs (distinct (concat (:refs block) ref-blocks))]
+      (assoc block :refs refs))))
 
 (defn block-keywordize
   [block]
@@ -338,32 +346,83 @@
             [path-refs parents]
             (cond
               (zero? level-diff)            ; sibling
-              (let [path-refs (mapcat :block/ref-pages (drop-last parents))
+              (let [path-refs (mapcat :block/refs (drop-last parents))
                     parents (conj (vec (butlast parents)) block)]
                 [path-refs parents])
 
               (> level-diff 0)              ; child
-              (let [path-refs (mapcat :block/ref-pages parents)]
+              (let [path-refs (mapcat :block/refs parents)]
                 [path-refs (conj parents block)])
 
               (< level-diff 0)              ; new parent
               (let [parents (vec (take-while (fn [p] (< (:block/level p) cur-level)) parents))
-                    path-refs (mapcat :block/ref-pages parents)]
+                    path-refs (mapcat :block/refs parents)]
                 [path-refs (conj parents block)]))
             path-ref-pages (->> path-refs
-                                (concat (:block/ref-pages block))
+                                (concat (:block/refs block))
+                                (map (fn [ref]
+                                       (cond
+                                         (map? ref)
+                                         (:block/name ref)
+
+                                         :else
+                                         ref)))
                                 (remove string/blank?)
-                                (map string/lower-case)
-                                (distinct)
-                                (map (fn [p]
-                                       {:page/name p})))]
+                                (map (fn [ref]
+                                       (if (string? ref)
+                                         {:block/name (string/lower-case ref)}
+                                         ref)))
+                                (remove vector?)
+                                (distinct))]
         (recur (rest blocks)
-               (conj acc (assoc block :block/path-ref-pages path-ref-pages))
+               (conj acc (assoc block :block/path-refs path-ref-pages))
                parents)))))
 
+(defn block-tags->pages
+  [{:keys [tags] :as block}]
+  (if (seq tags)
+    (assoc block :tags (map (fn [tag]
+                              [:block/name (string/lower-case tag)]) tags))
+    block))
+
+(defn- remove-indentation-spaces
+  [s level]
+  (let [level (inc level)
+        lines (string/split-lines s)
+        [f & r] lines
+        body (map (fn [line]
+                    (if (string/blank? (util/safe-subs line 0 level))
+                      (util/safe-subs line level)
+                      line))
+                  r)
+        content (cons f body)]
+    (string/join "\n" content)))
+
+(defn- get-block-content
+  [utf8-content block format]
+  (let [meta (:meta block)
+        content (if-let [end-pos (:end-pos meta)]
+                  (utf8/substring utf8-content
+                                  (:start-pos meta)
+                                  end-pos)
+                  (utf8/substring utf8-content
+                                  (:start-pos meta)))]
+    (let [content (when content
+                    (let [content (text/remove-level-spaces content format)]
+                      (if (or (:pre-block? block)
+                              (= (:format block) :org))
+                        content
+                        (remove-indentation-spaces content (:level block)))))]
+      (if (= format :org)
+        content
+        (property/->new-properties content)))))
+
 (defn extract-blocks
-  [blocks last-pos encoded-content]
-  (let [pre-block-body (atom nil)
+  [blocks content with-id? format]
+  (let [encoded-content (utf8/encode content)
+        last-pos (utf8/length encoded-content)
+        pre-block-body (atom nil)
+        pre-block-properties (atom nil)
         blocks
         (loop [headings []
                block-body []
@@ -375,35 +434,40 @@
                children []]
           (if (seq blocks)
             (let [[block {:keys [start_pos end_pos]}] (first blocks)
-                  level (:level (second block))]
+                  unordered? (:unordered (second block))
+                  markdown-heading? (and (false? unordered?) (= :markdown format))]
               (cond
                 (paragraph-timestamp-block? block)
                 (let [timestamps (extract-timestamps block)
                       timestamps' (merge timestamps timestamps)
-                      other-body (->> (remove timestamp-block? (second block))
+                      other-body (->> (second block)
                                       (drop-while #(= ["Break_Line"] %)))]
                   (recur headings (conj block-body ["Paragraph" other-body]) (rest blocks) timestamps' properties last-pos last-level children))
 
-                (properties-block? block)
+                (property/properties-ast? block)
                 (let [properties (extract-properties block start_pos end_pos)]
                   (recur headings block-body (rest blocks) timestamps properties last-pos last-level children))
 
                 (heading-block? block)
-                (let [id (or (when-let [custom-id (or (get-in properties [:properties "custom_id"])
-                                                      (get-in properties [:properties "id"]))]
+                (let [id (or (when-let [custom-id (or (get-in properties [:properties :custom-id])
+                                                      (get-in properties [:properties :custom_id])
+                                                      (get-in properties [:properties :id]))]
                                (let [custom-id (string/trim custom-id)]
                                  (when (util/uuid-string? custom-id)
                                    (uuid custom-id))))
-                             (d/squuid))
-                      ref-pages-in-properties (:page-refs properties)
+                             (db/new-block-id))
+                      ref-pages-in-properties (->> (:page-refs properties)
+                                                   (remove string/blank?))
                       block (second block)
+                      block (if markdown-heading?
+                              (assoc block
+                                     :type :heading
+                                     :level 1
+                                     :heading-level (:level block))
+                              block)
                       level (:level block)
                       [children current-block-children]
                       (cond
-                        (>= level last-level)
-                        [(conj children [id level])
-                         #{}]
-
                         (< level last-level)
                         (let [current-block-children (set (->> (filter #(< level (second %)) children)
                                                                (map first)
@@ -411,23 +475,31 @@
                                                                       [:block/uuid id]))))
                               others (vec (remove #(< level (second %)) children))]
                           [(conj others [id level])
-                           current-block-children]))
+                           current-block-children])
+
+                        (>= level last-level)
+                        [(conj children [id level])
+                         #{}])
+
                       block (-> (assoc block
                                        :uuid id
                                        :body (vec (reverse block-body))
                                        :properties (:properties properties)
-                                       :ref-pages ref-pages-in-properties
-                                       :children (or current-block-children []))
+                                       :refs ref-pages-in-properties
+                                       :children (or current-block-children [])
+                                       :format format)
                                 (assoc-in [:meta :start-pos] start_pos)
-                                (assoc-in [:meta :end-pos] last-pos))
+                                (assoc-in [:meta :end-pos] last-pos)
+                                ((fn [block]
+                                   (assoc block
+                                          :content (get-block-content encoded-content block format)))))
                       block (if (seq timestamps)
                               (merge block (timestamps->scheduled-and-deadline timestamps))
                               block)
                       block (-> block
-                                with-page-refs
+                                (with-page-refs with-id?)
                                 with-block-refs
-                                block-tags->pages
-                                update-src-pos-meta!)
+                                block-tags->pages)
                       last-pos' (get-in block [:meta :start-pos])]
                   (recur (conj headings block) [] (rest blocks) {} {} last-pos' (:level block) children))
 
@@ -436,108 +508,121 @@
                   (recur headings block-body' (rest blocks) timestamps properties last-pos last-level children))))
             (do
               (when (seq block-body)
-                (reset! pre-block-body block-body))
+                (reset! pre-block-body (reverse block-body)))
+              (when (seq properties)
+                (let [properties (:properties properties)]
+                  (reset! pre-block-properties properties)))
               (-> (reverse headings)
                   safe-blocks))))]
     (let [first-block (first blocks)
           first-block-start-pos (get-in first-block [:block/meta :start-pos])
-          blocks (if (and
-                      (not (string/blank? encoded-content))
-                      (or (empty? blocks)
-                          (> first-block-start-pos 1)))
+          blocks (if (or (seq @pre-block-body)
+                         (seq @pre-block-properties))
                    (cons
                     (merge
-                     (let [content (utf8/substring encoded-content 0 first-block-start-pos)
-                           uuid (d/squuid)]
+                     (let [content (utf8/substring encoded-content 0 first-block-start-pos)]
                        (->
-                        {:uuid uuid
+                        {:uuid (db/new-block-id)
                          :content content
-                         :anchor (str uuid)
-                         :level 2
+                         :level 1
                          :meta {:start-pos 0
                                 :end-pos (or first-block-start-pos
                                              (utf8/length encoded-content))}
                          :body @pre-block-body
-                         ;; (take-while (fn [block] (not (heading-block? block))) blocks)
-                         :pre-block? true}
+                         :properties @pre-block-properties
+                         :pre-block? true
+                         :unordered true}
                         (block-keywordize)))
                      (select-keys first-block [:block/file :block/format :block/page]))
                     blocks)
                    blocks)]
       (with-path-refs blocks))))
 
-(defn- page-with-journal
-  [original-page-name]
-  (when original-page-name
-    (let [page-name (string/lower-case original-page-name)]
-      (if-let [d (date/journal-title->int page-name)]
-        {:page/name page-name
-         :page/original-name original-page-name
-         :page/journal? true
-         :page/journal-day d}
-        {:page/name page-name
-         :page/original-name original-page-name}))))
+(defn with-parent-and-left
+  [page-id blocks]
+  (loop [blocks (map (fn [block] (assoc block :block/level-spaces (:block/level block))) blocks)
+         parents [{:page/id page-id     ; db id or lookup ref [:block/name "xxx"]
+                   :block/level 0
+                   :block/level-spaces 0}]
+         sibling nil
+         result []]
+    (if (empty? blocks)
+      (map #(dissoc % :block/level-spaces) result)
+      (let [[block & others] blocks
+            level-spaces (:block/level-spaces block)
+            {:block/keys [uuid level parent unordered] :as last-parent} (last parents)
+            parent-spaces (:block/level-spaces last-parent)
+            [blocks parents sibling result]
+            (cond
+              (= level-spaces parent-spaces)        ; sibling
+              (let [block (assoc block
+                                 :block/parent parent
+                                 :block/left [:block/uuid uuid]
+                                 :block/level level
+                                 )
+                    parents' (conj (vec (butlast parents)) block)
+                    result' (conj result block)]
+                [others parents' block result'])
+
+              (> level-spaces parent-spaces)         ; child
+              (let [parent (if uuid [:block/uuid uuid] (:page/id last-parent))
+                    block (cond->
+                            (assoc block
+                                  :block/parent parent
+                                  :block/left parent)
+                            ;; fix block levels with wrong order
+                            ;; For example:
+                            ;;   - a
+                            ;; - b
+                            ;; What if the input indentation is two spaces instead of 4 spaces
+                            (>= (- level-spaces parent-spaces) 1)
+                            (assoc :block/level (inc level)))
+                    parents' (conj parents block)
+                    result' (conj result block)]
+                [others parents' block result'])
+
+              ;; - a
+              ;;    - b
+              ;;  - c
+              (and (>= (count parents) 2)
+                   (< level-spaces parent-spaces)
+                   (> level-spaces (:block/level-spaces (nth parents (- (count parents) 2)))))
+              (let [block (assoc block
+                                 :block/parent parent
+                                 :block/left [:block/uuid uuid]
+                                 :block/level level
+                                 :block/level-spaces parent-spaces)
+                    parents' (conj (vec (butlast parents)) block)
+                    result' (conj result block)]
+                [others parents' block result'])
+
+              (< level-spaces parent-spaces)         ; outdent
+              (let [parents' (vec (filter (fn [p] (<= (:block/level-spaces p) level-spaces)) parents))
+                    blocks (cons (assoc (first blocks) :block/level (dec level))
+                                 (rest blocks))]
+                [blocks parents' (last parents') result]))]
+        (recur blocks parents sibling result)))))
 
 (defn parse-block
-  ([block format]
-   (parse-block block format nil))
-  ([{:block/keys [uuid content meta file page pre-block?] :as block} format ast]
+  ([block]
+   (parse-block block nil))
+  ([{:block/keys [uuid content meta file page parent left format] :as block} {:keys [with-id?]
+                                                                              :or {with-id? true}}]
    (when-not (string/blank? content)
-     (let [ast (or ast (format/to-edn content format nil))
-           start-pos (:start-pos meta)
-           encoded-content (utf8/encode content)
-           content-length (utf8/length encoded-content)
-           blocks (extract-blocks ast content-length encoded-content)
-           ref-pages-atom (atom [])
-           parent-ref-pages (->> (db/get-block-parent (state/get-current-repo) uuid)
-                                 :block/path-ref-pages
-                                 (map :db/id))
-           blocks (doall
-                   (map-indexed
-                    (fn [idx {:block/keys [ref-pages ref-blocks meta] :as block}]
-                      (let [path-ref-pages (->> ref-pages
-                                                (remove string/blank?)
-                                                (map string/lower-case)
-                                                (map (fn [p] [:page/name p]))
-                                                (concat parent-ref-pages))
-                            block (merge
-                                   block
-                                   {:block/meta meta
-                                    :block/marker (get block :block/marker "nil")
-                                    :block/properties (get block :block/properties {})
-                                    :block/file file
-                                    :block/format format
-                                    :block/page page
-                                    :block/content (utf8/substring encoded-content
-                                                                   (:start-pos meta)
-                                                                   (:end-pos meta))
-                                    :block/path-ref-pages path-ref-pages}
-                                   ;; Preserve the original block id
-                                   (when (zero? idx)
-                                     {:block/uuid uuid})
-                                   (when (seq ref-pages)
-                                     {:block/ref-pages
-                                      (mapv
-                                       (fn [page]
-                                         (let [page (page-with-journal page)]
-                                           (swap! ref-pages-atom conj page)
-                                           page))
-                                       ref-pages)}))]
-                        (-> block
-                            (assoc-in [:block/meta :start-pos] (+ (:start-pos meta) start-pos))
-                            (assoc-in [:block/meta :end-pos] (+ (:end-pos meta) start-pos)))))
-                    blocks))
-           pages (vec (distinct @ref-pages-atom))]
-       {:blocks blocks
-        :pages pages
-        :start-pos start-pos
-        :end-pos (+ start-pos content-length)}))))
-
-(defn with-levels
-  [text format {:block/keys [level pre-block?]}]
-  (let [pattern (config/get-block-pattern format)
-        prefix (if pre-block? "" (str (apply str (repeat level pattern)) " "))]
-    (str prefix (string/triml text))))
+     (let [block (dissoc block :block/pre-block?)
+           ast (format/to-edn content format nil)
+           new-block (first (extract-blocks ast content with-id? format))
+           parent-refs (->> (db/get-block-parent (state/get-current-repo) uuid)
+                            :block/path-refs
+                            (map :db/id))
+           {:block/keys [refs]} new-block
+           ref-pages (filter :block/name refs)
+           path-ref-pages (concat ref-pages parent-refs)
+           block (merge
+                  block
+                  new-block
+                  {:block/path-refs path-ref-pages})]
+       (if uuid (assoc block :block/uuid uuid) block)))))
 
 (defn macro-subs
   [macro-content arguments]

+ 36 - 20
src/main/frontend/format/mldoc.cljs

@@ -1,6 +1,7 @@
 (ns frontend.format.mldoc
   (:require [frontend.format.protocol :as protocol]
             [frontend.util :as util]
+            [frontend.utf8 :as utf8]
             [clojure.string :as string]
             [cljs-bean.core :as bean]
             [cljs.core.match :refer-macros [match]]
@@ -15,6 +16,7 @@
 (defonce parseHtml (gobj/get Mldoc "parseHtml"))
 (defonce anchorLink (gobj/get Mldoc "anchorLink"))
 (defonce parseAndExportMarkdown (gobj/get Mldoc "parseAndExportMarkdown"))
+(defonce astExportMarkdown (gobj/get Mldoc "astExportMarkdown"))
 
 (defn default-config
   ([format]
@@ -27,13 +29,22 @@
         :heading_number false
         :keep_line_break true
         :format format
-        :heading_to_list export-heading-to-list?})))))
+        :heading_to_list export-heading-to-list?}))))
+  ([format export-heading-to-list? exporting-keep-properties?]
+   (let [format (string/capitalize (name (or format :markdown)))]
+     (js/JSON.stringify
+      (bean/->js
+       {:toc false
+        :heading_number false
+        :keep_line_break true
+        :format format
+        :heading_to_list export-heading-to-list?
+        :exporting_keep_properties exporting-keep-properties?})))))
 
 (def default-references
   (js/JSON.stringify
    (clj->js {:embed_blocks []
-             :embed_pages []
-             :refer_blocks []})))
+             :embed_pages []})))
 
 (defn parse-json
   [content config]
@@ -49,6 +60,12 @@
                           (or config default-config)
                           (or references default-references)))
 
+(defn ast-export-markdown
+  [ast config references]
+  (astExportMarkdown ast
+                     (or config default-config)
+                     (or references default-references)))
+
 ;; Org-roam
 (defn get-tags-from-definition
   [ast]
@@ -106,10 +123,9 @@
           properties (->> (map first directive-ast)
                           (map (fn [[_ k v]]
                                  (let [k (keyword (string/lower-case k))
-                                       comma? (contains? #{:tags :alias :roam_tags} k)
                                        v (if (contains? #{:title :description :roam_tags} k)
                                            v
-                                           (text/split-page-refs-without-brackets v comma?))]
+                                           (text/split-page-refs-without-brackets v true))]
                                    [k v])))
                           (reverse)
                           (into {}))
@@ -161,6 +177,19 @@
         original-ast))
     ast))
 
+(defn update-src-full-content
+  [ast content]
+  (let [content (utf8/encode content)]
+    (map (fn [[block pos-meta]]
+          (if (and (vector? block)
+                   (= "Src" (first block)))
+            (let [{:keys [start_pos end_pos]} pos-meta
+                  block ["Src" (assoc (second block)
+                                      :full_content
+                                      (utf8/substring content start_pos end_pos))]]
+              [block pos-meta])
+            [block pos-meta])) ast)))
+
 (defn ->edn
   [content config]
   (try
@@ -169,6 +198,7 @@
       (-> content
           (parse-json config)
           (util/json->clj)
+          (update-src-full-content content)
           (collect-page-properties)))
     (catch js/Error e
       (log/error :edn/convert-failed e)
@@ -196,22 +226,8 @@
   (lazyLoad [this ok-handler]
     true)
   (exportMarkdown [this content config references]
-    (parse-export-markdown content config references))
-  )
+    (parse-export-markdown content config references)))
 
 (defn plain->text
   [plains]
   (string/join (map last plains)))
-
-(defn parse-properties
-  [content format]
-  (let [ast (->> (->edn content
-                        (default-config format))
-                 (map first))
-        properties (collect-page-properties ast)
-        properties (let [properties (and (seq ast)
-                                         (= "Properties" (ffirst ast))
-                                         (last (first ast)))]
-                     (if (and properties (seq properties))
-                       properties))]
-    (into {} properties)))

+ 1 - 41
src/main/frontend/format/mldoc_test.cljs

@@ -1,44 +1,4 @@
 (ns frontend.format.mldoc-test
-  (:require [frontend.format.mldoc :refer [parse-properties]]
+  (:require [frontend.format.mldoc]
             [clojure.string :as string]
             [cljs.test :refer [deftest are is testing]]))
-
-(deftest test-parse-org-properties
-  []
-  (testing "just title"
-    (let [content "#+TITLE:   some title   "
-          props (parse-properties content "org")]
-      (are [x y] (= x y)
-        ;; TODO: should we trim in parse-properties?
-        "some title" (string/trim (:title props)))))
-
-  (testing "filetags"
-    (let [content "
-#+FILETAGS:   :tag1:tag_2:@tag:
-#+ROAM_TAGS:  roamtag
-body"
-          props (parse-properties content "org")]
-      (are [x y] (= x y)
-        (list "@tag" "tag1" "tag_2") (sort (:filetags props))
-        ["roamtag"] (:roam_tags props)
-        (list "@tag" "roamtag" "tag1" "tag_2") (sort (:tags props)))))
-
-  (testing "roam tags"
-    (let [content "
-#+FILETAGS: filetag
-#+ROAM_TAGS: roam1 roam2
-body
-"
-          props (parse-properties content "org")]
-      (are [x y] (= x y)
-        ["roam1" "roam2"] (:roam_tags props)
-        (list "filetag" "roam1" "roam2") (sort (:tags props)))))
-
-  (testing "quoted roam tags"
-    (let [content "
-#+ROAM_TAGS: \"why would\"  you use \"spaces\" xxx
-body
-"
-          props (parse-properties content "org")]
-      ;; TODO maybe need to sort or something
-      (is (= ["why would" "spaces" "you" "use" "xxx"] (:roam_tags props))))))

+ 13 - 15
src/main/frontend/fs/nfs.cljs

@@ -105,8 +105,7 @@
         (and local-file (.text local-file)))))
 
   (write-file! [this repo dir path content opts]
-    (let [{:keys [old-content]} opts
-          last-modified-at (db/get-file-last-modified-at repo path)
+    (let [last-modified-at (db/get-file-last-modified-at repo path)
           parts (string/split path "/")
           basename (last parts)
           sub-dir (->> (butlast parts)
@@ -135,23 +134,22 @@
                              (config/get-file-format))
                   pending-writes (state/get-write-chan-length)
                   draw? (and path (string/ends-with? path ".excalidraw"))]
-            (if (and local-content (or old-content
-                                       ;; temporally fix
-                                       draw?) new?
-                     (or
-                      draw?
-                      ;; Writing not finished
-                      (> pending-writes 0)
-                      ;; not changed by other editors
-                      not-changed?
-                      new-created?))
-              (do
+            (p/let [_ (verify-permission repo file-handle true)
+                    _ (utils/writeFile file-handle content)
+                    file (.getFile file-handle)]
+              (if (and local-content new?
+                       (or
+                        draw?
+                        ;; Writing not finished
+                        (> pending-writes 0)
+                        ;; not changed by other editors
+                        not-changed?
+                        new-created?))
                 (p/let [_ (verify-permission repo file-handle true)
                         _ (utils/writeFile file-handle content)
                         file (.getFile file-handle)]
                   (when file
-                    (nfs-saved-handler repo path file))))
-              (do
+                    (nfs-saved-handler repo path file)))
                 (js/alert (str "The file has been modified on your local disk! File path: " path
                                ", please save your changes and click the refresh button to reload it.")))))
            ;; create file handle

+ 12 - 11
src/main/frontend/fs/node.cljs

@@ -24,10 +24,15 @@
            (str "/" (string/replace path #"^/" ""))))))
 
 (defn- write-file-impl!
-  [repo dir path content {:keys [ok-handler error-handler] :as opts} stat]
+  [this repo dir path content {:keys [ok-handler error-handler] :as opts} stat]
   (p/let [disk-mtime (when stat (gobj/get stat "mtime"))
-          db-mtime (db/get-file-last-modified-at repo path)]
-    (if (not= disk-mtime db-mtime)
+          db-mtime (db/get-file-last-modified-at repo path)
+          old-content nil
+          old-content (-> (protocol/read-file this dir path nil)
+                          (p/catch (fn [error]
+                                     "")))]
+    (if (and (not= disk-mtime db-mtime)
+             (not= (string/trim old-content) (string/trim content)))
       (js/alert (str "The file has been modified on your local disk! File path: " path
                      ", please save your changes and click the refresh button to reload it."))
       (->
@@ -57,14 +62,10 @@
       (ipc/ipc "readFile" path)))
   (write-file! [this repo dir path content {:keys [ok-handler error-handler] :as opts}]
     (let [path (concat-path dir path)]
-      (->
-       (p/let [stat (protocol/stat this dir path)]
-         ;; update
-         (write-file-impl! repo dir path content opts stat))
-       (p/catch
-        (fn [_error]
-             ;; create
-          (write-file-impl! repo dir path content opts nil))))))
+      (p/let [stat (p/catch
+                       (protocol/stat this dir path)
+                       (fn [_e] nil))]
+        (write-file-impl! this repo dir path content opts stat))))
   (rename! [this repo old-path new-path]
     (ipc/ipc "rename" old-path new-path))
   (stat [this dir path]

+ 5 - 15
src/main/frontend/fs/watcher_handler.cljs

@@ -20,28 +20,18 @@
       (when (and content (not (encrypt/content-encrypted? content)))
         (cond
           (= "add" type)
-          (let [db-content (db/get-file path)]
-            (when (and (not= content db-content)
-                       ;; Avoid file overwrites
-                       ;; 1. create a new page which writes a new file
-                       ;; 2. add some new content
-                       ;; 3. file watcher notified it with the old content
-                       ;; 4. old content will overwrites the new content in step 2
-                       (not (and db-content
-                                 (string/starts-with? db-content content))))
-              (let [_ (file-handler/alter-file repo path content {:re-render-root? true
-                                                                  :from-disk? true})]
-                (db/set-file-last-modified-at! repo path mtime))))
+          (when-not (db/file-exists? repo path)
+            (let [_ (file-handler/alter-file repo path content {:re-render-root? true
+                                                                :from-disk? true})]
+              (db/set-file-last-modified-at! repo path mtime)))
 
           (and (= "change" type)
-               (nil? (db/get-file path)))
+               (not (db/file-exists? repo path)))
           (js/console.warn "Can't get file in the db: " path)
 
           (and (= "change" type)
-               (not= content (db/get-file path))
                (when-let [last-modified-at (db/get-file-last-modified-at repo path)]
                  (> mtime last-modified-at)))
-
           (let [_ (file-handler/alter-file repo path content {:re-render-root? true
                                                               :from-disk? true})]
             (db/set-file-last-modified-at! repo path mtime))

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

@@ -143,7 +143,7 @@
                      (let [page-name (string/lower-case (gobj/get node "id"))]
                        (if (gobj/get event "shiftKey")
                          (let [repo (state/get-current-repo)
-                               page (db/entity repo [:page/name page-name])]
+                               page (db/entity repo [:block/name page-name])]
                            (state/sidebar-add-block!
                             repo
                             (:db/id page)

+ 23 - 19
src/main/frontend/handler.cljs

@@ -18,6 +18,8 @@
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.web.nfs :as nfs]
+            [frontend.modules.shortcut.core :as shortcut]
+            [frontend.handler.events :as events]
             [frontend.fs.watcher-handler :as fs-watcher-handler]
             [frontend.ui :as ui]
             [goog.object :as gobj]
@@ -25,6 +27,7 @@
             [lambdaisland.glogi :as log]
             [frontend.handler.common :as common-handler]
             [electron.listener :as el]
+            [electron.ipc :as ipc]
             [frontend.version :as version]))
 
 (defn- watch-for-date!
@@ -44,11 +47,6 @@
   []
   (storage/set :db-schema db-schema/schema))
 
-(defn schema-changed?
-  []
-  (when-let [schema (storage/get :db-schema)]
-    (not= schema db-schema/schema)))
-
 (defn- get-me-and-repos
   []
   (let [me (and js/window.user (bean/->clj js/window.user))
@@ -60,14 +58,6 @@
      :logged? logged?
      :repos repos}))
 
-(declare restore-and-setup!)
-
-(defn clear-stores-and-refresh!
-  []
-  (p/let [_ (idb/clear-local-storage-and-idb!)]
-    (let [{:keys [me logged? repos]} (get-me-and-repos)]
-      (js/window.location.reload))))
-
 (defn restore-and-setup!
   [me repos logged?]
   (let [interval (atom nil)
@@ -97,7 +87,7 @@
                                false)
                               (store-schema!))
 
-                            (nfs/ask-permission-if-local?)
+                            (state/pub-event! [:modal/nfs-ask-permission])
 
                             (page-handler/init-commands!)
                             (when (seq (:repos me))
@@ -148,10 +138,11 @@
 
 (defn init-sentry
   []
-  (let [cfg
-        {:dsn "https://[email protected]/5311485"
-         :release (util/format "logseq@%s" version/version)}]
-    (.init js/window.Sentry (clj->js cfg))))
+  (when-not (state/sentry-disabled?)
+    (let [cfg
+          {:dsn "https://[email protected]/5311485"
+           :release (util/format "logseq@%s" version/version)}]
+      (.init js/window.Sentry (clj->js cfg)))))
 
 (defn on-load-events
   []
@@ -159,6 +150,13 @@
             (when-not config/dev? (init-sentry)))]
     (set! js/window.onload f)))
 
+(defn clear-cache!
+  []
+  (p/let [_ (idb/clear-local-storage-and-idb!)
+          _ (when (util/electron?)
+              (ipc/ipc "clearCache"))]
+    (js/window.location.reload)))
+
 (defn start!
   [render]
   (let [{:keys [me logged? repos]} (get-me-and-repos)]
@@ -173,12 +171,18 @@
        (notification/show! "Sorry, it seems that your browser doesn't support IndexedDB, we recommend to use latest Chrome(Chromium) or Firefox(Non-private mode)." :error false)
        (state/set-indexedb-support! false)))
 
+    (events/run!)
+
     (p/let [repos (get-repos)]
       (state/set-repos! repos)
       (restore-and-setup! me repos logged?))
+
     (reset! db/*sync-search-indice-f search/sync-search-indice!)
     (db/run-batch-txs!)
     (file-handler/run-writes-chan!)
-    (editor-handler/periodically-save!)
+    (shortcut/install-shortcuts!)
     (when (util/electron?)
       (el/listen!))))
+
+(defn stop! []
+  (prn "stop!"))

+ 72 - 175
src/main/frontend/handler/block.cljs

@@ -1,5 +1,6 @@
 (ns frontend.handler.block
   (:require [frontend.util :as util]
+            [frontend.util.property :as property]
             [clojure.walk :as walk]
             [frontend.db :as db]
             [frontend.state :as state]
@@ -8,91 +9,12 @@
             [frontend.config :as config]
             [datascript.core :as d]
             [clojure.set :as set]
-            [medley.core :as medley]))
-
-(defn blocks->vec-tree [col]
-  (let [col (map (fn [h] (cond->
-                          h
-                           (not (:block/dummy? h))
-                           (dissoc h :block/meta))) col)
-        parent? (fn [item children]
-                  (and (seq children)
-                       (every? #(< (:block/level item) (:block/level %)) children)))
-        get-all-refs (fn [block]
-                       (let [refs (if-let [refs (seq (:block/refs-with-children block))]
-                                    refs
-                                    (concat
-                                     (:block/ref-pages block)
-                                     (:block/tags block)))]
-                         (distinct refs)))]
-    (loop [col (reverse col)
-           children (list)]
-      (if (empty? col)
-        children
-        (let [[item & others] col
-              cur-level (:block/level item)
-              bottom-level (:block/level (first children))
-              pre-block? (:block/pre-block? item)
-              item (assoc item :block/refs-with-children (->> (get-all-refs item)
-                                                              (remove nil?)))]
-          (cond
-            (empty? children)
-            (recur others (list item))
-
-            (<= bottom-level cur-level)
-            (recur others (conj children item))
-
-            pre-block?
-            (recur others (cons item children))
-
-            (> bottom-level cur-level)      ; parent
-            (let [[children other-children] (split-with (fn [h]
-                                                          (> (:block/level h) cur-level))
-                                                        children)
-                  refs-with-children (->> (mapcat get-all-refs (cons item children))
-                                          (remove nil?)
-                                          distinct)
-                  children (cons
-                            (assoc item
-                                   :block/children children
-                                   :block/refs-with-children refs-with-children)
-                            other-children)]
-              (recur others children))))))))
-
-;; recursively with children content for tree
-(defn get-block-content-rec
-  ([block]
-   (get-block-content-rec block (fn [block] (:block/content block))))
-  ([block transform-fn]
-   (let [contents (atom [])
-         _ (walk/prewalk
-            (fn [form]
-              (when (map? form)
-                (when-let [content (:block/content form)]
-                  (swap! contents conj (transform-fn form))))
-              form)
-            block)]
-     (apply util/join-newline @contents))))
-
-;; with children content
-(defn get-block-full-content
-  ([repo block-id]
-   (get-block-full-content repo block-id (fn [block] (:block/content block))))
-  ([repo block-id transform-fn]
-   (let [blocks (db/get-block-and-children-no-cache repo block-id)]
-     (->> blocks
-          (map transform-fn)
-          (apply util/join-newline)))))
-
-(defn get-block-end-pos-rec
-  [repo block]
-  (let [children (:block/children block)]
-    (if (seq children)
-      (get-block-end-pos-rec repo (last children))
-      (if-let [end-pos (get-in block [:block/meta :end-pos])]
-        end-pos
-        (when-let [block (db/entity repo [:block/uuid (:block/uuid block)])]
-          (get-in block [:block/meta :end-pos]))))))
+            [medley.core :as medley]
+            [frontend.format.block :as block]
+            [frontend.debug :as debug]
+            [clojure.string :as string]
+            [frontend.text :as text]
+            [frontend.handler.common :as common-handler]))
 
 (defn get-block-ids
   [block]
@@ -106,107 +28,50 @@
            block)]
     @ids))
 
-(defn collapse-block!
-  [block]
-  (let [repo (:block/repo block)]
-    (db/transact! repo
-                  [{:block/uuid (:block/uuid block)
-                    :block/collapsed? true}])))
-
-(defn collapse-blocks!
-  [block-ids]
-  (let [repo (state/get-current-repo)]
-    (db/transact! repo
-                  (map
-                   (fn [id]
-                     {:block/uuid id
-                      :block/collapsed? true})
-                   block-ids))))
-
-(defn expand-block!
-  [block]
-  (let [repo (:block/repo block)]
-    (db/transact! repo
-                  [{:block/uuid (:block/uuid block)
-                    :block/collapsed? false}])))
-
-(defn expand-blocks!
-  [block-ids]
-  (let [repo (state/get-current-repo)]
-    (db/transact! repo
-                  (map
-                   (fn [id]
-                     {:block/uuid id
-                      :block/collapsed? false})
-                   block-ids))))
-
-(defn pre-block-with-only-title?
-  [repo block-id]
-  (when-let [block (db/entity repo [:block/uuid block-id])]
-    (let [properties (:page/properties (:block/page block))
-          property-names (keys properties)]
-      (and (every? #(contains? #{:title :filters} %) property-names)
-           (let [ast (mldoc/->edn (:block/content block) (mldoc/default-config (:block/format block)))]
-             (and
-              (= "Properties" (ffirst (first ast)))
-              (or
-               (empty? (rest ast))
-               (every? (fn [[[typ break-lines]] _]
-                         (and (= typ "Paragraph")
-                              (every? #(= % ["Break_Line"]) break-lines))) (rest ast)))))))))
-
+;; TODO: should we remove this dummy block and use the page's root block instead?
 (defn with-dummy-block
   ([blocks format]
    (with-dummy-block blocks format {} {}))
   ([blocks format default-option {:keys [journal? page-name]
                                   :or {journal? false}}]
    (let [format (or format (state/get-preferred-format) :markdown)
-         blocks (if (and journal?
-                         (seq blocks)
-                         (when-let [title (second (first (:block/title (first blocks))))]
-                           (date/valid-journal-title? title)))
-                  (rest blocks)
-                  blocks)
          blocks (vec blocks)]
-     (cond
-       (and (seq blocks)
-            (or (and (> (count blocks) 1)
-                     (:block/pre-block? (first blocks)))
-                (and (>= (count blocks) 1)
-                     (not (:block/pre-block? (first blocks))))))
+     (if (seq blocks)
        blocks
-
-       :else
-       (let [last-block (last blocks)
-             end-pos (get-in last-block [:block/meta :end-pos] 0)
-             dummy (merge last-block
-                          (let [uuid (d/squuid)]
-                            {:block/uuid uuid
-                             :block/title ""
-                             :block/content (config/default-empty-block format)
-                             :block/format format
-                             :block/level 2
-                             :block/priority nil
-                             :block/anchor (str uuid)
-                             :block/meta {:start-pos end-pos
-                                          :end-pos end-pos}
-                             :block/body nil
-                             :block/dummy? true
-                             :block/marker nil
-                             :block/pre-block? false})
-                          default-option)]
-         (conj blocks dummy))))))
+       (let [page-block (when page-name (db/pull [:block/name (string/lower-case page-name)]))
+             create-title-property? (util/include-windows-reserved-chars? page-name)
+             content (if create-title-property?
+                       (let [title (or (:block/original-name page-block)
+                                       (:block/name page-block))
+                             properties (common-handler/get-page-default-properties title)]
+                         (property/build-properties-str format properties))
+                       "")
+             page-id {:db/id (:db/id page-block)}
+             dummy (merge {:block/uuid (db/new-block-id)
+                           :block/left page-id
+                           :block/parent page-id
+                           :block/page page-id
+                           :block/title ""
+                           :block/content content
+                           :block/format format
+                           :block/dummy? true}
+                          default-option)
+             dummy (if (:db/id (:block/file dummy))
+                     dummy
+                     (dissoc dummy :block/file))]
+         [dummy])))))
 
 (defn filter-blocks
   [repo ref-blocks filters group-by-page?]
   (let [ref-pages (->> (if group-by-page?
                          (mapcat last ref-blocks)
                          ref-blocks)
-                       (mapcat (fn [b] (concat (:block/ref-pages b) (:block/children-refs b))))
+                       (mapcat (fn [b] (concat (:block/refs b) (:block/children-refs b))))
+                       (concat (when group-by-page? (map first ref-blocks)))
                        (distinct)
                        (map :db/id)
-                       (db/pull-many repo '[:db/id :page/name]))
-        ref-pages (zipmap (map :page/name ref-pages) (map :db/id ref-pages))
+                       (db/pull-many repo '[:db/id :block/name]))
+        ref-pages (zipmap (map :block/name ref-pages) (map :db/id ref-pages))
         exclude-ids (->> (map (fn [page] (get ref-pages page)) (get filters false))
                          (remove nil?)
                          (set))
@@ -219,19 +84,51 @@
                        (cond->> ref-blocks
                          (seq exclude-ids)
                          (remove (fn [block]
-                                   (let [ids (set (concat (map :db/id (:block/ref-pages block))
-                                                          (map :db/id (:block/children-refs block))))]
+                                   (let [ids (set (concat (map :db/id (:block/refs block))
+                                                          (map :db/id (:block/children-refs block))
+                                                          [(:db/id (:block/page block))]))]
                                      (seq (set/intersection exclude-ids ids)))))
 
                          (seq include-ids)
                          (remove (fn [block]
-                                   (let [ids (set (concat (map :db/id (:block/ref-pages block))
+                                   (let [page-block-id (:db/id (:block/page block))
+                                         ids (set (concat (map :db/id (:block/refs block))
                                                           (map :db/id (:block/children-refs block))))]
-                                     (empty? (set/intersection include-ids ids)))))
-                         ))]
+                                     (if (and (contains? include-ids page-block-id)
+                                              (= 1 (count include-ids)))
+                                       (not= page-block-id (first include-ids))
+                                       (empty? (set/intersection include-ids (set (conj ids page-block-id))))))))))]
         (if group-by-page?
           (->> (map (fn [[p ref-blocks]]
                       [p (filter-f ref-blocks)]) ref-blocks)
                (remove #(empty? (second %))))
           (->> (filter-f ref-blocks)
                (remove nil?)))))))
+
+;; TODO: reduced version
+(defn walk-block
+  [block check? transform]
+  (let [result (atom nil)]
+    (walk/postwalk
+     (fn [x]
+       (if (check? x)
+         (reset! result (transform x))
+         x))
+     (:block/body block))
+    @result))
+
+(defn get-timestamp
+  [block typ]
+  (walk-block block
+              (fn [x]
+                (and (block/timestamp-block? x)
+                     (= typ (first (second x)))))
+              #(second (second %))))
+
+(defn get-scheduled-ast
+  [block]
+  (get-timestamp block "Scheduled"))
+
+(defn get-deadline-ast
+  [block]
+  (get-timestamp block "Deadline"))

+ 11 - 4
src/main/frontend/handler/common.cljs

@@ -4,7 +4,7 @@
             [cljs-bean.core :as bean]
             [promesa.core :as p]
             [frontend.util :as util]
-            [frontend.text :as text]
+            [frontend.util.property :as property]
             [frontend.git :as git]
             [frontend.db :as db]
             [frontend.encrypt :as e]
@@ -15,7 +15,8 @@
             [cljs-time.format :as tf]
             [frontend.config :as config]
             ["ignore" :as Ignore]
-            ["/frontend/utils" :as utils]))
+            ["/frontend/utils" :as utils]
+            [frontend.date :as date]))
 
 (defn get-ref
   [repo-url]
@@ -55,8 +56,8 @@
                  (js/console.dir error)))))))
 
 (defn copy-to-clipboard-without-id-property!
-  [content]
-  (util/copy-to-clipboard! (text/remove-id-property content)))
+  [format content]
+  (util/copy-to-clipboard! (property/remove-id-property format content)))
 
 (defn config-with-document-mode
   [config]
@@ -160,3 +161,9 @@
                    (do (log/error :token/failed-get-token token-m)
                        (reject)))))
              nil))))))))
+
+(defn get-page-default-properties
+  [page-name]
+  {:title page-name
+   ;; :date (date/get-date-time-string)
+   })

+ 5 - 24
src/main/frontend/handler/config.cljs

@@ -1,37 +1,18 @@
 (ns frontend.handler.config
   (:require [frontend.state :as state]
             [frontend.handler.file :as file-handler]
-            [borkdude.rewrite-edn :as rewrite]
             [frontend.config :as config]
-            [frontend.db :as db]
             [clojure.string :as string]))
 
 (defn set-config!
   [k v]
-  (when-let [repo (state/get-current-repo)]
-    (let [path (config/get-config-path)]
-      (when-let [config (db/get-file-no-sub path)]
-        (let [config (try
-                       (rewrite/parse-string config)
-                       (catch js/Error e
-                         (println "Parsing config file failed: ")
-                         (js/console.dir e)
-                         {}))
-              ks (if (vector? k) k [k])
-              new-config (rewrite/assoc-in config ks v)]
-          (state/set-config! repo new-config)
-          (let [new-content (str new-config)]
-            (file-handler/set-file-content! repo path new-content)))))))
+  (let [path (config/get-config-path)]
+    (file-handler/edn-file-set-key-value path k v state/set-config!)))
 
 (defn toggle-ui-show-brackets! []
   (let [show-brackets? (state/show-brackets?)]
     (set-config! :ui/show-brackets? (not show-brackets?))))
 
-(defn set-project!
-  [project]
-  (when-not (string/blank? project)
-    (set-config! [:project :name] project)))
-
-(defn set-preferred-workflow!
-  [workflow]
-  (set-config! :preferred-workflow (name workflow)))
+(defn toggle-logical-outdenting! []
+  (let [logical-outdenting? (state/logical-outdenting?)]
+    (set-config! :editor/logical-outdenting? (not logical-outdenting?))))

+ 42 - 414
src/main/frontend/handler/dnd.cljs

@@ -1,357 +1,25 @@
 (ns frontend.handler.dnd
-  (:require [frontend.handler.notification :as notification]
-            [frontend.handler.repo :as repo-handler]
-            [frontend.handler.block :as block-handler]
-            [frontend.config :as config]
-            [frontend.state :as state]
-            [frontend.util :as util :refer-macros [profile]]
+  (:require [frontend.state :as state]
             [frontend.db :as db]
-            [clojure.walk :as walk]
-            [clojure.string :as string]
-            [frontend.utf8 :as utf8]
-            [cljs-time.coerce :as tc]
-            [cljs-time.core :as t]))
-
-(defn- remove-block-child!
-  [target-block parent-block]
-  (let [child-ids (set (block-handler/get-block-ids target-block))]
-    (block-handler/get-block-content-rec
-     parent-block
-     (fn [{:block/keys [uuid level content]}]
-       (if (contains? child-ids uuid)
-         ""
-         content)))))
-
-(defn- recompute-block-level
-  [to-block nested?]
-  (+ (:block/level to-block)
-     (if nested? 1 0)))
-
-(defn- recompute-block-content-and-changes
-  [target-block to-block nested? same-repo? same-file?]
-  (let [new-level (recompute-block-level to-block nested?)
-        target-level (:block/level target-block)
-        format (:block/format target-block)
-        pattern (config/get-block-pattern format)
-        block-changes (atom [])
-        all-content (block-handler/get-block-content-rec
-                     target-block
-                     (fn [{:block/keys [uuid level content]
-                           :as block}]
-                       (let [new-level (+ new-level (- level target-level))
-                             new-content (string/replace-first content
-                                                               (apply str (repeat level pattern))
-                                                               (apply str (repeat new-level pattern)))
-                             block (cond->
-                                    {:block/uuid uuid
-                                     :block/level new-level
-                                     :block/content new-content
-                                     :block/page (:block/page to-block)}
-
-                                     (not same-repo?)
-                                     (merge (dissoc block [:block/level :block/content]))
-
-                                     (not same-file?)
-                                     (merge {:block/page (:block/page to-block)
-                                             :block/file (:block/file to-block)}))]
-                         (swap! block-changes conj block)
-                         new-content)))]
-    [all-content @block-changes]))
-
-(defn- move-parent-to-child?
-  [target-block to-block]
-  (let [to-block-id (:block/uuid to-block)
-        result (atom false)
-        _ (walk/postwalk
-           (fn [form]
-             (when (map? form)
-               (when-let [id (:block/uuid form)]
-                 (when (= id to-block-id)
-                   (reset! result true))))
-             form)
-           target-block)]
-    @result))
-
-(defn- compute-target-child?
-  [target-block to-block]
-  (let [target-block-id (:block/uuid target-block)
-        result (atom false)
-        _ (walk/postwalk
-           (fn [form]
-             (when (map? form)
-               (when-let [id (:block/uuid form)]
-                 (when (= id target-block-id)
-                   (reset! result true))))
-             form)
-           to-block)]
-    @result))
-
-(defn rebuild-dnd-blocks
-  [repo file target-child? start-pos target-blocks offset-block-uuid {:keys [delete? same-file?]
-                                                                      :or {delete? false
-                                                                           same-file? true}}]
-  (when (seq target-blocks)
-    (let [file-id (:db/id file)
-          target-block-ids (set (map :block/uuid target-blocks))
-          after-blocks (->> (db/get-file-after-blocks repo file-id start-pos)
-                            (remove (fn [h] (contains? target-block-ids (:block/uuid h)))))
-
-          after-blocks (cond
-                         delete?
-                         after-blocks
-
-                         (and offset-block-uuid
-                              (not (contains? (set (map :block/uuid after-blocks)) offset-block-uuid)))
-                         (concat target-blocks after-blocks)
-
-                         offset-block-uuid
-                         (let [[before after] (split-with (fn [h] (not= (:block/uuid h)
-                                                                        offset-block-uuid)) after-blocks)]
-                           (concat (conj (vec before) (first after))
-                                   target-blocks
-                                   (rest after)))
-                         :else
-                         (concat target-blocks after-blocks))
-          after-blocks (remove nil? after-blocks)
-          ;; _ (prn {:start-pos start-pos
-          ;;         :target-blocks target-blocks
-          ;;         :after-blocks (map (fn [block]
-          ;;                                (:block/content block))
-          ;;                           after-blocks)})
-          last-start-pos (atom start-pos)
-          result (mapv
-                  (fn [{:block/keys [uuid meta content level page] :as block}]
-                    (let [content (str (util/trim-safe content) "\n")
-                          target-block? (contains? target-block-ids uuid)
-                          content-length (if target-block?
-                                           (utf8/length (utf8/encode content))
-                                           (- (:end-pos meta) (:start-pos meta)))
-                          new-end-pos (+ @last-start-pos content-length)
-                          new-meta {:start-pos @last-start-pos
-                                    :end-pos new-end-pos}]
-                      (reset! last-start-pos new-end-pos)
-                      (let [data {:block/uuid uuid
-                                  :block/meta new-meta}]
-                        (cond
-                          (and target-block? (not same-file?))
-                          (merge
-                           (dissoc block :block/idx :block/dummy?)
-                           data)
-
-                          target-block?
-                          (merge
-                           data
-                           {:block/level level
-                            :block/content content
-                            :block/page page})
-
-                          :else
-                          data))))
-                  after-blocks)]
-      result)))
-
-(defn- get-start-pos
-  [block]
-  (get-in block [:block/meta :start-pos]))
-
-(defn- get-end-pos
-  [block]
-  (get-in block [:block/meta :end-pos]))
-
-(defn- compute-direction
-  [target-block top-block nested? top? target-child?]
-  (cond
-    (= top-block target-block)
-    :down
-
-    (and target-child? nested?)
-    :up
-
-    (and target-child? (not top?))
-    :down
-
-    :else
-    :up))
-
-(defn- compute-after-blocks-in-same-file
-  [repo target-block to-block direction top? nested? target-child? target-file original-top-block-start-pos block-changes]
-  (cond
-    top?
-    (rebuild-dnd-blocks repo target-file target-child?
-                        original-top-block-start-pos
-                        block-changes
-                        nil
-                        {})
-
-    (= direction :up)
-    (let [offset-block-id (if nested?
-                            (:block/uuid to-block)
-                            (last (block-handler/get-block-ids to-block)))
-          offset-end-pos (get-end-pos
-                          (db/entity repo [:block/uuid offset-block-id]))]
-      (rebuild-dnd-blocks repo target-file target-child?
-                          offset-end-pos
-                          block-changes
-                          nil
-                          {}))
-
-    (= direction :down)
-    (let [offset-block-id (if nested?
-                            (:block/uuid to-block)
-                            (last (block-handler/get-block-ids to-block)))
-          target-start-pos (get-start-pos target-block)]
-      (rebuild-dnd-blocks repo target-file target-child?
-                          target-start-pos
-                          block-changes
-                          offset-block-id
-                          {}))))
-
-;; TODO: still could be different pages, e.g. move a block from one journal to another journal
-(defn- move-block-in-same-file
-  [repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes]
-  (if (move-parent-to-child? target-block to-block)
-    nil
-    (let [old-file-content (db/get-file (:file/path (db/entity (:db/id (:block/file target-block)))))
-          old-file-content (utf8/encode old-file-content)
-          subs (fn [start-pos end-pos] (utf8/substring old-file-content start-pos end-pos))
-          bottom-content (block-handler/get-block-content-rec bottom-block)
-          top-content (remove-block-child! bottom-block top-block)
-          top-area (subs 0 (get-start-pos top-block))
-          bottom-area (subs
-                       (cond
-                         (and nested? (= direction :down))
-                         (get-end-pos bottom-block)
-                         target-child?
-                         (block-handler/get-block-end-pos-rec repo top-block)
-                         :else
-                         (block-handler/get-block-end-pos-rec repo bottom-block))
-                       nil)
-          between-area (if (= direction :down)
-                         (subs (block-handler/get-block-end-pos-rec repo target-block) (get-start-pos to-block))
-                         (subs (block-handler/get-block-end-pos-rec repo to-block) (get-start-pos target-block)))
-          up-content (when (= direction :up)
-                       (cond
-                         nested?
-                         (util/join-newline (:block/content top-block)
-                                            target-content
-                                            (if target-child?
-                                              (remove-block-child! target-block (:block/children to-block))
-                                              (block-handler/get-block-content-rec (:block/children top-block))))
-                         (and top? target-child?)
-                         (util/join-newline target-content (remove-block-child! target-block to-block))
-
-                         top?
-                         (util/join-newline target-content top-content)
-
-                         :else
-                         (let [top-content (if target-child?
-                                             (remove-block-child! target-block to-block)
-                                             top-content)]
-                           (util/join-newline top-content target-content))))
-          down-content (when (= direction :down)
-                         (cond
-                           nested?
-                           (util/join-newline (:block/content bottom-block)
-                                              target-content)
-                           target-child?
-                           (util/join-newline top-content target-content)
-
-                           :else
-                           (util/join-newline bottom-content target-content)))
-          ;; _ (prn {:direction direction
-          ;;         :nested? nested?
-          ;;         :top? top?
-          ;;         :target-child? target-child?
-          ;;         :top-area top-area
-          ;;         :up-content up-content
-          ;;         :between-area between-area
-          ;;         :down-content down-content
-          ;;         :bottom-area bottom-area
-          ;;         })
-          new-file-content (string/trim
-                            (util/join-newline
-                             top-area
-                             up-content
-                             between-area
-                             down-content
-                             bottom-area))
-          after-blocks (->> (compute-after-blocks-in-same-file repo target-block to-block direction top? nested? target-child? target-file original-top-block-start-pos block-changes)
-                            (remove nil?))
-          path (:file/path (db/entity repo (:db/id (:block/file to-block))))]
-      (profile
-       "Move block in the same file: "
-       (repo-handler/transact-react-and-alter-file!
-        repo
-        (concat
-         after-blocks)
-        {:key :block/change
-         :data block-changes}
-        [[path new-file-content]]))
-      ;; (alter-file repo
-      ;;             path
-      ;;             new-file-content
-      ;;             {:re-render-root? true})
-)))
-
-(defn- move-block-in-different-files
-  [repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes]
-  (let [target-file (db/entity repo (:db/id (:block/file target-block)))
-        target-file-path (:file/path target-file)
-        target-file-content (db/get-file repo target-file-path)
-        to-file (db/entity repo (:db/id (:block/file to-block)))
-        to-block (assoc to-block :block/meta (:block/meta (db/entity (:db/id to-block))))
-        to-file-path (:file/path to-file)
-        target-block-end-pos (block-handler/get-block-end-pos-rec repo target-block)
-        to-block-start-pos (get-start-pos to-block)
-        to-block-end-pos (block-handler/get-block-end-pos-rec repo to-block)
-        new-target-file-content (utf8/delete! target-file-content
-                                              (get-start-pos target-block)
-                                              target-block-end-pos)
-        to-file-content (utf8/encode (db/get-file repo to-file-path))
-        new-to-file-content (let [separate-pos (cond nested?
-                                                     (get-end-pos to-block)
-                                                     top?
-                                                     to-block-start-pos
-                                                     :else
-                                                     to-block-end-pos)]
-                              (string/trim
-                               (util/join-newline
-                                (utf8/substring to-file-content 0 separate-pos)
-                                target-content
-                                (utf8/substring to-file-content separate-pos))))
-        target-after-blocks (rebuild-dnd-blocks repo target-file target-child?
-                                                (get-start-pos target-block)
-                                                block-changes nil {:delete? true})
-        to-after-blocks (cond
-                          top?
-                          (rebuild-dnd-blocks repo to-file target-child?
-                                              (get-start-pos to-block)
-                                              block-changes
-                                              nil
-                                              {:same-file? false})
-
-                          :else
-                          (let [offset-block-id (if nested?
-                                                  (:block/uuid to-block)
-                                                  (last (block-handler/get-block-ids to-block)))
-                                offset-end-pos (get-end-pos
-                                                (db/entity repo [:block/uuid offset-block-id]))]
-                            (rebuild-dnd-blocks repo to-file target-child?
-                                                offset-end-pos
-                                                block-changes
-                                                nil
-                                                {:same-file? false})))]
-    (profile
-     "Move block between different files: "
-     (repo-handler/transact-react-and-alter-file!
-      repo
-      (concat
-       target-after-blocks
-       to-after-blocks)
-      {:key :block/change
-       :data (conj block-changes target-block)}
-      [[target-file-path new-target-file-content]
-       [to-file-path new-to-file-content]]))))
+            [frontend.modules.outliner.core :as outliner-core]
+            [frontend.modules.outliner.tree :as tree]
+            [lambdaisland.glogi :as log]
+            [frontend.debug :as debug]))
+
+
+(defn- moveable?
+  [current-block target-block]
+  (let [current-block-uuid (:block/uuid current-block)]
+    (or
+     (not= (:block/page current-block) (:block/page target-block))
+     (and
+      (not= current-block-uuid (:block/uuid target-block))
+      (loop [loc target-block]
+        (if-let [parent (db/pull (:db/id (:block/parent loc)))]
+          (if (= (:block/uuid parent) current-block-uuid)
+            false
+            (recur parent))
+          true))))))
 
 (defn move-block
   "There can be at least 3 possible situations:
@@ -364,65 +32,25 @@
      we don't handle this now. TODO: transform between different formats in mldoc.
   2. Sometimes we might need to move a parent block to it's own child.
   "
-  [target-block to-block target-dom-id top? nested?]
-  (when (and
-         target-block
-         to-block
-         (:block/format target-block)
-         (:block/format to-block)
-         (not (:block/dummy? to-block))
-         (not (:block/dummy? target-block)))
-    (cond
-      (not= (:block/format target-block)
-            (:block/format to-block))
-      (notification/show!
-       (util/format "Sorry, you can't move a block of format %s to another file of format %s."
-                    (:block/format target-block)
-                    (:block/format to-block))
-       :error)
-
-      (= (:block/uuid target-block) (:block/uuid to-block))
-      nil
-
-      :else
-      (let [pattern (config/get-block-pattern (:block/format to-block))
-            target-block-repo (:block/repo target-block)
-            to-block-repo (:block/repo to-block)
-            target-block (assoc target-block
-                                :block/meta
-                                (:block/meta (db/entity target-block-repo [:block/uuid (:block/uuid target-block)])))
-            to-block (assoc to-block
-                            :block/meta
-                            (:block/meta (db/entity [:block/uuid (:block/uuid to-block)])))
-            same-repo? (= target-block-repo to-block-repo)
-            target-file (:block/file target-block)
-            same-file? (and
-                        same-repo?
-                        (= (:db/id target-file)
-                           (:db/id (:block/file to-block))))
-            [top-block bottom-block] (if same-file?
-                                       (if (< (get-start-pos target-block)
-                                              (get-start-pos to-block))
-                                         [target-block to-block]
-                                         [to-block target-block])
-                                       [nil nil])
-            target-child? (compute-target-child? target-block to-block)
-            direction (compute-direction target-block top-block nested? top? target-child?)
-            original-top-block-start-pos (get-start-pos top-block)
-            [target-content block-changes] (recompute-block-content-and-changes target-block to-block nested? same-repo? same-file?)]
-        (cond
-          same-file?
-          (move-block-in-same-file target-block-repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes)
-
-          ;; same repo but different files
-          same-repo?
-          (move-block-in-different-files target-block-repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes)
-
-          ;; different repos
-          :else
-          nil)
-
-        (when (state/get-git-auto-push?)
-          (doseq [repo (->> #{target-block-repo to-block-repo}
-                            (remove nil?))]
-            (repo-handler/push repo nil)))))))
+  [current-block target-block top? nested?]
+  (when (and (every? map? [current-block target-block])
+             (moveable? current-block target-block))
+    (let [[current-node target-node]
+          (mapv outliner-core/block [current-block target-block])]
+      (cond
+        top?
+        (let [first-child?
+              (= (tree/-get-parent-id target-node)
+                (tree/-get-left-id target-node))]
+          (if first-child?
+            (let [parent (tree/-get-parent target-node)]
+              (outliner-core/move-subtree current-node parent false))
+            (outliner-core/move-subtree current-node target-node true)))
+        nested?
+        (outliner-core/move-subtree current-node target-node false)
+
+        :else
+        (outliner-core/move-subtree current-node target-node true))
+      (let [repo (state/get-current-repo)]
+        (db/refresh! repo {:key :block/change
+                           :data [(:data current-node) (:data target-node)]})))))

+ 4 - 10
src/main/frontend/handler/draw.cljs

@@ -6,13 +6,8 @@
             [frontend.state :as state]
             [frontend.db :as db]
             [frontend.handler.file :as file-handler]
-            [frontend.handler.git :as git-handler]
             [frontend.date :as date]
-            [frontend.config :as config]
-            [frontend.storage :as storage]
-            [clojure.string :as string]
-            [cljs-time.core :as t]
-            [cljs-time.coerce :as tc]))
+            [frontend.config :as config]))
 
 (defn create-draws-directory!
   [repo]
@@ -33,12 +28,11 @@
          (p/do!
           (create-draws-directory! repo)
           (fs/write-file! repo repo-dir path data nil)
-          (git-handler/git-add repo path)
           (db/transact! repo
                         [{:file/path path
-                          :page/name file
-                          :page/file [:file/path path]
-                          :page/journal? false}]))
+                          :block/name file
+                          :block/file [:file/path path]
+                          :block/journal? false}]))
          (p/catch (fn [error]
                     (prn "Write file failed, path: " path ", data: " data)
                     (js/console.dir error))))))))

文件差異過大導致無法顯示
+ 382 - 628
src/main/frontend/handler/editor.cljs


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

@@ -18,14 +18,14 @@
        (let [target (.-target e)]
          (if (d/has-class? target "bottom-action") ;; FIXME: not particular case
            (.preventDefault e)
-           (let [{:keys [on-hide format value block id repo dummy?]} (editor-handler/get-state state)]
+           (let [{:keys [on-hide format value block id repo dummy?]} (editor-handler/get-state)]
              (when on-hide
                (on-hide value event))
              (when
               (or (= event :esc)
                   (= event :visibilitychange)
                   (and (= event :click)
-                       (not (editor-handler/in-auto-complete? (gobj/get target "id")))))
+                       (not (editor-handler/auto-complete?))))
                (state/clear-edit!))))))
      :node (gdom/getElement id)
     ;; :visibilitychange? true

+ 5 - 33
src/main/frontend/handler/editor/lifecycle.cljs

@@ -1,10 +1,8 @@
 (ns frontend.handler.editor.lifecycle
   (:require [frontend.state :as state]
-            [lambdaisland.glogi :as log]
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [clojure.string :as string]
-            [frontend.util :as util :refer-macros [profile]]
             [cljs-drag-n-drop.core :as dnd]
             [frontend.handler.editor.keyboards :as keyboards-handler]
             [frontend.handler.page :as page-handler]
@@ -22,11 +20,7 @@
         input (gdom/getElement id)]
     (when block-parent-id
       (state/set-editing-block-dom-id! block-parent-id))
-    (if (= :indent-outdent (state/get-editor-op))
-      (when input
-        (when-let [pos (state/get-edit-pos)]
-          (util/set-caret-pos! input pos)))
-      (editor-handler/restore-cursor-pos! id content dummy?))
+    (editor-handler/restore-cursor-pos! id content dummy?)
 
     (when input
       (dnd/subscribe!
@@ -50,7 +44,7 @@
 
 (defn will-unmount
   [state]
-  (let [{:keys [id value format block repo dummy? config]} (get-state state)
+  (let [{:keys [id value format block repo dummy? config]} (get-state)
         file? (:file? config)]
     (when-let [input (gdom/getElement id)]
       ;; (.removeEventListener input "paste" (fn [event]
@@ -64,31 +58,9 @@
               input
               :upload-images))))
     (editor-handler/clear-when-saved!)
-    (if file?
-      (let [path (:file-path config)
-            content (db/get-file-no-sub path)
-            value (some-> (gdom/getElement path)
-                          (gobj/get "value"))]
-        (when (and
-               (not (string/blank? value))
-               (not= (string/trim value) (string/trim content)))
-          (let [old-page-name (db/get-file-page path false)
-                journal? (date/valid-journal-title? path)]
-            (p/let [[journal? new-name] (page-handler/rename-when-alter-title-property! old-page-name path format content value)]
-              (if (and journal? new-name (not= old-page-name (string/lower-case new-name)))
-                (notification/show! "Journal title can't be changed." :warning)
-                (let [new-name (if journal? (date/journal-title->default new-name) new-name)
-                      new-path (if new-name
-                                (if (and
-                                     new-name old-page-name
-                                     (= (string/lower-case new-name) (string/lower-case old-page-name)))
-                                  path
-                                  (page-handler/compute-new-file-path path new-name))
-                                path)]
-                  (file/alter-file (state/get-current-repo) new-path (string/trim value)
-                                   {:re-render-root? true})))))))
-      (when-not (contains? #{:insert :indent-outdent :auto-save} (state/get-editor-op))
-        (editor-handler/save-block! (get-state state) value))))
+    ;; TODO: ugly
+    (when-not (contains? #{:insert :indent-outdent :auto-save :undo :redo} (state/get-editor-op))
+      (editor-handler/save-block! (get-state) value)))
   state)
 
 (def lifecycle

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

@@ -0,0 +1,80 @@
+(ns frontend.handler.events
+  (:refer-clojure :exclude [run!])
+  (:require [frontend.state :as state]
+            [clojure.core.async :as async]
+            [frontend.spec :as spec]
+            [frontend.ui :as ui]
+            [frontend.util :as util :refer-macros [profile]]
+            [frontend.config :as config]
+            [frontend.handler.notification :as notification]
+            [frontend.components.encryption :as encryption]
+            [frontend.fs.nfs :as nfs]))
+
+;; TODO: should we move all events here?
+
+(defn show-install-error!
+  [repo-url title]
+  (spec/validate :repos/url repo-url)
+  (notification/show!
+   [:p.content
+    title
+    " "
+    [:span.mr-2
+     (util/format
+      "Please make sure that you've installed the logseq app for the repo %s on GitHub. "
+      repo-url)
+     (ui/button
+      "Install Logseq on GitHub"
+      :href (str "https://github.com/apps/" config/github-app-name "/installations/new"))]]
+   :error
+   false))
+
+(defmulti handle first)
+
+(defmethod handle :repo/install-error [[_ repo-url title]]
+  (show-install-error! repo-url title))
+
+(defmethod handle :modal/encryption-setup-dialog [[_ repo-url close-fn]]
+  (state/set-modal!
+   (encryption/encryption-setup-dialog repo-url close-fn)))
+
+(defmethod handle :modal/encryption-input-secret-dialog [[_ repo-url db-encrypted-secret close-fn]]
+  (state/set-modal!
+   (encryption/encryption-input-secret-dialog
+    repo-url
+    db-encrypted-secret
+    close-fn)))
+
+
+(defn get-local-repo
+  []
+  (when-let [repo (state/get-current-repo)]
+    (when (config/local-db? repo)
+      repo)))
+
+(defn ask-permission
+  [repo]
+  (when-not (util/electron?)
+    (fn [close-fn]
+      [:div
+       [:p
+        "Grant native filesystem permission for directory: "
+        [:b (config/get-local-dir repo)]]
+       (ui/button
+        "Grant"
+        :on-click (fn []
+                    (nfs/check-directory-permission! repo)
+                    (close-fn)))])))
+
+(defmethod handle :modal/nfs-ask-permission []
+  (when-let [repo (get-local-repo)]
+    (state/set-modal! (ask-permission repo))))
+
+(defn run!
+  []
+  (let [chan (state/get-events-chan)]
+    (async/go-loop []
+      (let [payload (async/<! chan)]
+        (handle payload))
+      (recur))
+    chan))

+ 0 - 102
src/main/frontend/handler/expand.cljs

@@ -1,102 +0,0 @@
-(ns frontend.handler.expand
-  (:require [dommy.core :as d]
-            [goog.dom :as gdom]
-            [goog.object :as gobj]
-            [frontend.util :as util]
-            [frontend.state :as state]
-            [frontend.handler.block :as block-handler]))
-
-(defn- hide!
-  [element]
-  (d/set-style! element :display "none"))
-
-(defn- show!
-  [element]
-  (d/set-style! element :display ""))
-
-(defn collapse!
-  [block]
-  (let [has-title? (seq (:block/title block))
-        uuid (:block/uuid block)
-        nodes (array-seq (js/document.getElementsByClassName (str uuid)))]
-    (doseq [node nodes]
-      (d/add-class! node "collapsed")
-      (when has-title?
-        (when-let [e (.querySelector node ".block-body")]
-         (hide! e)))
-      (when-let [e (.querySelector node ".block-children")]
-        (hide! e)
-        (let [elements (d/by-class node "ls-block")]
-          (doseq [element elements]
-            (hide! element))))
-      (block-handler/collapse-block! block))))
-
-(defn expand!
-  [block]
-  (let [uuid (:block/uuid block)
-        nodes (array-seq (js/document.getElementsByClassName (str uuid)))]
-    (doseq [node nodes]
-      (when-let [e (.querySelector node ".block-body")]
-        (show! e))
-      (when-let [e (.querySelector node ".block-children")]
-        (let [elements (d/by-class node "ls-block")]
-          (doseq [element elements]
-            (show! element)))
-        (show! e))
-      (block-handler/expand-block! block))))
-
-(defn set-bullet-closed!
-  [element]
-  (when element
-    (when-let [node (.querySelector element ".bullet-container")]
-      (d/add-class! node "bullet-closed"))))
-
-;; Collapse acts like TOC
-;; There are three modes to cycle:
-;; 1. Collapse all blocks which levels are greater than 2
-;; 2. Hide all block's body (user can still see the block title)
-;; 3. Show everything
-(defn cycle!
-  []
-  (let [mode (state/next-collapse-mode)
-        get-blocks (fn []
-                     (let [elements (d/by-class "ls-block")
-                           result (group-by (fn [e]
-                                              (let [level (d/attr e "level")]
-                                                (and level
-                                                     (> (util/parse-int level) 2)))) elements)]
-                       [(get result true) (get result false)]))]
-    (case mode
-      :show-all
-      (do
-        (doseq [element (d/by-class "ls-block")]
-          (show! element))
-        (let [elements (d/by-class "block-body")]
-          (doseq [element elements]
-            (show! element)))
-        (doseq [element (d/by-class "bullet-closed")]
-          (d/remove-class! element "bullet-closed"))
-        (doseq [element (d/by-class "block-children")]
-          (show! element)))
-
-      :hide-block-body
-      (let [elements (d/by-class "block-body")]
-        (doseq [element elements]
-          (d/set-style! element :display "none")
-          (when-let [parent (util/rec-get-block-node element)]
-            (set-bullet-closed! parent))))
-
-      :hide-block-children
-      (let [[elements top-level-elements] (get-blocks)
-            level-2-elements (filter (fn [e]
-                                       (let [level (d/attr e "level")]
-                                         (and level
-                                              (= (util/parse-int level) 2)
-                                              (not (d/has-class? e "pre-block")))))
-                                     top-level-elements)]
-        (doseq [element elements]
-          (hide! element))
-        (doseq [element level-2-elements]
-          (when (= "true" (d/attr element "haschild"))
-            (set-bullet-closed! element)))))
-    (state/cycle-collapse!)))

+ 160 - 66
src/main/frontend/handler/export.cljs

@@ -13,13 +13,46 @@
             [frontend.text :as text]
             [frontend.handler.common :as common-handler]
             [frontend.extensions.zip :as zip]
+            [frontend.modules.file.core :as outliner-file]
+            [frontend.modules.outliner.tree :as outliner-tree]
             [promesa.core :as p]))
 
+
+(defn- get-page-content
+  [page]
+  (outliner-file/tree->file-content
+   (outliner-tree/blocks->vec-tree
+    (db/get-page-blocks-no-cache page) page) {:init-level 1}))
+
+(defn- get-page-content-debug
+  [page]
+  (outliner-file/tree->file-content
+   (outliner-tree/blocks->vec-tree
+    (db/get-page-blocks-no-cache page) page) {:init-level 1
+                                              :heading-to-list? true}))
+
+(defn- get-file-content
+  [file-path]
+  (let [page-name
+        (ffirst (d/q '[:find ?pn
+                       :where
+                       [?e :file/path file-path]
+                       [?p :block/file ?e]
+                       [?p :block/name ?pn]] (db/get-conn)))]
+    (get-page-content page-name)))
+
+(defn- get-blocks-contents
+  [repo root-block-uuid]
+  (->
+   (db/get-block-and-children repo root-block-uuid)
+   (outliner-tree/blocks->vec-tree (str root-block-uuid))
+   (outliner-file/tree->file-content {:init-level 1})))
+
 (defn copy-block!
   [block-id]
   (when-let [block (db/pull [:block/uuid block-id])]
     (let [content (:block/content block)]
-      (common-handler/copy-to-clipboard-without-id-property! content))))
+      (common-handler/copy-to-clipboard-without-id-property! (:block/format block) content))))
 
 (defn copy-block-as-json!
   [block-id]
@@ -60,22 +93,21 @@
 
 (defn download-file!
   [file-path]
-  (when-let [repo (state/get-current-repo)]
-    (when-let [content (db/get-file repo file-path)]
-      (let [data (js/Blob. ["\ufeff" (array content)] ; prepend BOM
-                           (clj->js {:type "text/plain;charset=utf-8,"}))]
-        (let [anchor (gdom/getElement "download")
-              url (js/window.URL.createObjectURL data)]
-          (.setAttribute anchor "href" url)
-          (.setAttribute anchor "download" file-path)
-          (.click anchor))))))
+  (when-let [content (get-file-content file-path)]
+    (let [data (js/Blob. ["\ufeff" (array content)] ; prepend BOM
+                         (clj->js {:type "text/plain;charset=utf-8,"}))]
+      (let [anchor (gdom/getElement "download")
+            url (js/window.URL.createObjectURL data)]
+        (.setAttribute anchor "href" url)
+        (.setAttribute anchor "download" file-path)
+        (.click anchor)))))
 
 (defn export-repo-as-html!
   [repo]
   (when-let [db (db/get-conn repo)]
-    (let [db           (if (state/all-pages-public?)
-                         (db/clean-export! db)
-                         (db/filter-only-public-pages-and-blocks db))
+    (let [[db asset-filenames]           (if (state/all-pages-public?)
+                                           (db/clean-export! db)
+                                           (db/filter-only-public-pages-and-blocks db))
           db-str       (db/db->string db)
           state        (select-keys @state/state
                                     [:ui/theme :ui/cycle-collapse
@@ -88,39 +120,50 @@
           html-str     (str "data:text/html;charset=UTF-8,"
                             (js/encodeURIComponent raw-html-str))]
       (if (util/electron?)
-        (js/window.apis.exportPublishAssets raw-html-str (config/get-custom-css-path))
+        (js/window.apis.exportPublishAssets raw-html-str (config/get-custom-css-path) (config/get-repo-dir repo) (clj->js asset-filenames))
+
         (when-let [anchor (gdom/getElement "download-as-html")]
           (.setAttribute anchor "href" html-str)
           (.setAttribute anchor "download" "index.html")
           (.click anchor))))))
 
+(defn- get-file-contents
+  ([repo]
+   (get-file-contents repo {:init-level 1}))
+  ([repo file-opts]
+   (let [conn (db/get-conn repo)]
+     (->> (d/q '[:find ?n ?fp
+                 :where
+                 [?e :block/file ?f]
+                 [?f :file/path ?fp]
+                 [?e :block/name ?n]] conn)
+          (mapv (fn [[page-name file-path]]
+                  [file-path
+                   (outliner-file/tree->file-content
+                    (outliner-tree/blocks->vec-tree
+                     (db/get-page-blocks-no-cache page-name) page-name)
+                    file-opts)]))))))
+
 (defn export-repo-as-zip!
   [repo]
-  (let [files (db/get-file-contents repo)
+  (let [files (get-file-contents repo)
         [owner repo-name] (util/get-git-owner-and-repo repo)
         repo-name (str owner "-" repo-name)]
     (when (seq files)
-      (p/let [zipfile (zip/make-zip repo-name files)]
+      (p/let [zipfile (zip/make-zip repo-name files repo)]
         (when-let [anchor (gdom/getElement "download-as-zip")]
           (.setAttribute anchor "href" (js/window.URL.createObjectURL zipfile))
           (.setAttribute anchor "download" (.-name zipfile))
           (.click anchor))))))
 
-(defn- get-file-contents-with-suffix
+(defn- get-md-file-contents
   [repo]
   (let [conn (db/get-conn repo)]
-    (->>
-     (filterv (fn [[path _]]
-                (or (string/ends-with? path ".md")))
-              (db/get-file-contents repo))
-     (mapv (fn [[path content]] {:path path :content content
-                                 :names (d/q '[:find [?n ?n2]
-                                               :in $ ?p
-                                               :where [?e :file/path ?p]
-                                               [?e2 :page/file ?e]
-                                               [?e2 :page/name ?n]
-                                               [?e2 :page/original-name ?n2]] conn path)
-                                 :format (f/get-format path)})))))
+    (filter (fn [[path _]]
+              (let [path (string/lower-case path)]
+                (or (string/ends-with? path ".md")
+                    (string/ends-with? path ".markdown"))))
+            (get-file-contents repo {:init-level 1 :heading-to-list? true}))))
 
 (defn- get-embed-and-refs-blocks-pages-aux []
   (let [mem (atom {})]
@@ -130,12 +173,12 @@
                     (let [[ref-blocks ref-pages]
                           (->> (if is-block?
                                  [page-or-block]
-                                 (db/get-page-blocks
-                                  repo page-or-block {:use-cache? false
-                                                      :pull-keys '[:block/ref-pages :block/ref-blocks]}))
-                               (filterv #(or (:block/ref-blocks %) (:block/ref-pages %)))
-                               (mapv (fn [b] [(:block/ref-blocks b), (:block/ref-pages b)]))
-                               (apply mapv vector)
+                                 (db/get-page-blocks-no-cache
+                                  repo page-or-block {:pull-keys '[:block/refs]}))
+                               (filterv :block/refs)
+                               (mapcat :block/refs)
+                               (group-by #(boolean (:block/page (db/entity (:db/id %)))))
+                               ((fn [g] [(get g true []) (get g false [])]))
                                (mapv #(vec (distinct (flatten (remove nil? %))))))
                           ref-block-ids
                           (->> ref-blocks
@@ -152,6 +195,7 @@
                           ref-pages
                           (->> ref-page-ids
                                (db/pull-many repo '[*])
+                               (filterv :block/name)
                                (flatten))
                           [next-ref-blocks1 next-ref-pages1]
                           (->> ref-blocks
@@ -159,7 +203,7 @@
                                (apply mapv vector))
                           [next-ref-blocks2 next-ref-pages2]
                           (->> ref-pages
-                               (mapv #(f repo (:page/name %) false exclude-blocks (set (concat ref-page-ids exclude-pages))))
+                               (mapv #(f repo (:block/name %) false exclude-blocks (set (concat ref-page-ids exclude-pages))))
                                (apply mapv vector))
                           result
                           [(->> (concat ref-block-ids next-ref-blocks1 next-ref-blocks2)
@@ -184,26 +228,20 @@
              (d/q '[:find ?n ?n2 (pull ?p [:file/path])
                     :in $ [?e ...]
                     :where
-                    [?e :page/file ?p]
-                    [?e :page/name ?n]
-                    [?e :page/original-name ?n2]] (db/get-conn repo))
+                    [?e :block/file ?p]
+                    [?e :block/name ?n]
+                    [?e :block/original-name ?n2]] (db/get-conn repo))
              (mapv (fn [[name origin-name file-path]]
                      (if (= name origin-name)
                        [[name file-path]]
                        [[name file-path] [origin-name file-path]])))
              (apply concat)
-             (mapv (fn [[page-name file-path]] [page-name (:file/path file-path)]))
-             (d/q '[:find ?n ?c
-                    :in $ [[?n ?p] ...]
-                    :where
-                    [?e :file/path ?p]
-                    [?e :file/content ?c]] @(db/get-files-conn repo)))
+             (mapv (fn [[page-name file-path]]
+                     [page-name (get-page-content page-name)])))
         embed-blocks
         (mapv (fn [b] [(str (:block/uuid b))
-                       [(apply str
-                               (mapv #(:block/content %)
-                                     (db/get-block-and-children repo (:block/uuid b))))
-                        (:block/title b)]])
+                       [(get-blocks-contents repo (:block/uuid b))
+                        (:block/content b)]])
               blocks)]
     {:embed_blocks embed-blocks
      :embed_pages pages-name-and-content}))
@@ -214,10 +252,10 @@
         (->>
          (d/q '[:find ?pn ?pon ?bt ?bc ?bid ?e ?rb
                 :where
-                [?e :block/ref-blocks ?rb]
+                [?e :block/refs ?rb]
                 [?e :block/page ?p]
-                [?p :page/name ?pn]
-                [?p :page/original-name ?pon]
+                [?p :block/name ?pn]
+                [?p :block/original-name ?pon]
                 [?rb :block/title ?bt]
                 [?rb :block/content ?bc]
                 [?rb :block/uuid ?bid]] (db/get-conn repo)))
@@ -240,19 +278,19 @@
         page-refs
         (->> (d/q '[:find ?pn ?pon ?rpn ?rpon ?fp ?e
                     :where
-                    [?e :block/ref-pages ?rp]
+                    [?e :block/refs ?rp]
                     [?e :block/page ?p]
-                    [?p :page/name ?pn]
-                    [?p :page/original-name ?pon]
-                    [?rp :page/name ?rpn]
-                    [?rp :page/original-name ?rpon]
-                    [?rp :page/file ?pf]
+                    [?p :block/name ?pn]
+                    [?p :block/original-name ?pon]
+                    [?rp :block/name ?rpn]
+                    [?rp :block/original-name ?rpon]
+                    [?rp :block/file ?pf]
                     [?pf :file/path ?fp]] (db/get-conn repo))
              (d/q '[:find ?pn ?pon ?rpn ?rpon ?fc ?be
                     :in $ [[?pn ?pon ?rpn ?rpon ?fp ?be] ...]
                     :where
                     [?e :file/path ?fp]
-                    [?e :file/content ?fc]] @(db/get-files-conn repo)))
+                    [?e :file/content ?fc]] (db/get-conn repo)))
         page-page-refs
         (->>
          page-refs
@@ -347,12 +385,10 @@
   (let [[block-refs page-refs]
         (get-page&block-refs-aux repo page false page&block-refs #{} #{})]
     {:embed_blocks
-     (mapv (fn [[title _content uuid id]]
+     (mapv (fn [[_title content uuid id]]
              [(str uuid)
-              [(apply str
-                      (mapv #(:block/content %)
-                            (db/get-block-and-children repo uuid)))
-               title]])
+              [(get-blocks-contents repo uuid)
+               content]])
            block-refs)
      :embed_pages (vec page-refs)}))
 
@@ -372,6 +408,31 @@
                                              (clj->js (f (first names)))))])))
          (remove nil?))))
 
+(defn- convert-md-files-unordered-list-or-heading
+  [repo files heading-to-list?]
+  (->> files
+       (mapv (fn [{:keys [path content names format]}]
+               (when (first names)
+                 [path (fp/exportMarkdown f/mldoc-record content
+                                          (f/get-default-config format heading-to-list? true)
+                                          nil)])))
+       (remove nil?)))
+
+(defn- get-file-contents-with-suffix
+  [repo]
+  (let [conn (db/get-conn repo)
+        md-files (get-md-file-contents repo)]
+    (->>
+     md-files
+     (map (fn [[path content]] {:path path :content content
+                                :names (d/q '[:find [?n ?n2]
+                                              :in $ ?p
+                                              :where [?e :file/path ?p]
+                                              [?e2 :block/file ?e]
+                                              [?e2 :block/name ?n]
+                                              [?e2 :block/original-name ?n2]] conn path)
+                                :format (f/get-format path)})))))
+
 (defn export-repo-as-markdown!
   [repo]
   (when-let [repo (state/get-current-repo)]
@@ -380,7 +441,7 @@
             files
             (export-files-as-markdown repo files heading-to-list?)
             zip-file-name (str repo "_markdown_" (quot (util/time-ms) 1000))]
-        (p/let [zipfile (zip/make-zip zip-file-name files)]
+        (p/let [zipfile (zip/make-zip zip-file-name files repo)]
           (when-let [anchor (gdom/getElement "export-as-markdown")]
             (.setAttribute anchor "href" (js/window.URL.createObjectURL zipfile))
             (.setAttribute anchor "download" (.-name zipfile))
@@ -391,7 +452,7 @@
   (when-let [repo (state/get-current-repo)]
     (when-let [file (db/get-page-file page-name)]
       (when-let [path (:file/path file)]
-        (when-let [content (db/get-file path)]
+        (when-let [content (get-page-content page-name)]
           (let [names [page-name]
                 format (f/get-format path)
                 files [{:path path :content content :names names :format format}]]
@@ -404,3 +465,36 @@
                   (.setAttribute anchor "href" url)
                   (.setAttribute anchor "download" path)
                   (.click anchor))))))))))
+
+(defn convert-page-markdown-unordered-list-or-heading!
+  [page-name]
+  (when-let [repo (state/get-current-repo)]
+    (when-let [file (db/get-page-file page-name)]
+      (when-let [path (:file/path file)]
+        (when-let [content (get-page-content page-name)]
+          (let [names [page-name]
+                format (f/get-format path)
+                files [{:path path :content content :names names :format format}]]
+            (let [files (convert-md-files-unordered-list-or-heading repo files (state/export-heading-to-list?))]
+              (let [data (js/Blob. [(second (first files))]
+                                   (clj->js {:type "text/plain;charset=utf-8,"}))]
+                (let [anchor (gdom/getElement "convert-markdown-to-unordered-list-or-heading")
+                      url (js/window.URL.createObjectURL data)]
+                  (.setAttribute anchor "href" url)
+                  (.setAttribute anchor "download" path)
+                  (.click anchor))))))))))
+
+(defn convert-repo-markdown-v2!
+  [repo]
+  (when repo
+    (when-let [files (get-md-file-contents repo)]
+      (let [zip-file-name (-> (string/replace repo config/local-db-prefix "")
+                              (string/replace #"^/+" ""))
+            zip-file-name (str zip-file-name
+                               "_markdown_"
+                               (quot (util/time-ms) 1000))]
+        (p/let [zipfile (zip/make-zip zip-file-name files repo)]
+          (when-let [anchor (gdom/getElement "convert-markdown-to-unordered-list-or-heading")]
+            (.setAttribute anchor "href" (js/window.URL.createObjectURL zipfile))
+            (.setAttribute anchor "download" (.-name zipfile))
+            (.click anchor)))))))

+ 6 - 7
src/main/frontend/handler/external.cljs

@@ -7,12 +7,11 @@
             [frontend.date :as date]
             [frontend.config :as config]
             [clojure.string :as string]
-            [promesa.core :as p]
             [frontend.db :as db]))
 
 (defonce debug-files (atom nil))
 (defn index-files!
-  [repo files git-add-cb]
+  [repo files finish-handler]
   (let [titles (->> files
                     (map :title)
                     (map :text)
@@ -22,7 +21,7 @@
                            journal? (date/valid-journal-title? title)]
                        (when-let [text (:text file)]
                          (let [path (str (if journal?
-                                           config/default-journals-directory
+                                           (config/get-journals-directory)
                                            (config/get-pages-directory))
                                          "/"
                                          (if journal?
@@ -39,14 +38,14 @@
       (file-handler/alter-files repo files {:add-history? false
                                             :update-db? false
                                             :update-status? false
-                                            :git-add-cb git-add-cb}))
+                                            :finish-handler finish-handler}))
     (let [journal-pages-tx (let [titles (filter date/valid-journal-title? titles)]
                              (map
                               (fn [title]
                                 (let [page-name (string/lower-case title)]
-                                  {:page/name page-name
-                                   :page/journal? true
-                                   :page/journal-day (date/journal-title->int title)}))
+                                  {:block/name page-name
+                                   :block/journal? true
+                                   :block/journal-day (date/journal-title->int title)}))
                               titles))]
       (when (seq journal-pages-tx)
         (db/transact! repo journal-pages-tx)))))

+ 160 - 116
src/main/frontend/handler/extract.cljs

@@ -6,12 +6,18 @@
             [clojure.set :as set]
             [frontend.utf8 :as utf8]
             [frontend.date :as date]
+            [frontend.text :as text]
+            [frontend.util.property :as property]
             [clojure.string :as string]
             [frontend.format.mldoc :as mldoc]
             [frontend.format.block :as block]
             [frontend.format :as format]
             [cljs-time.core :as t]
-            [cljs-time.coerce :as tc]))
+            [cljs-time.coerce :as tc]
+            [medley.core :as medley]
+            [clojure.walk :as walk]
+            [frontend.state :as state]
+            [frontend.config :as config]))
 
 (defn- extract-page-list
   [content]
@@ -22,133 +28,163 @@
          (map string/lower-case)
          (distinct))))
 
+
+
+(defn get-page-name
+  [file ast]
+  ;; headline
+  (let [ast (map first ast)]
+    (if (string/includes? file "pages/contents.")
+      "Contents"
+      (let [first-block (last (first (filter block/heading-block? ast)))
+            property-name (when (and (contains? #{"Properties" "Property_Drawer"} (ffirst ast))
+                                     (not (string/blank? (:title (last (first ast))))))
+                            (:title (last (first ast))))
+            first-block-name (let [title (last (first (:title first-block)))]
+                               (and first-block
+                                    (string? title)
+                                    title))
+            file-name (when-let [file-name (last (string/split file #"/"))]
+                        (-> (first (util/split-last "." file-name))
+                            (string/replace "." "/")))]
+        (or property-name
+            (if (= (state/page-name-order) "heading")
+              (or first-block-name file-name)
+              (or file-name first-block-name)))))))
+
 ;; TODO: performance improvement
 (defn- extract-pages-and-blocks
-  [repo-url format ast properties file content utf8-content journal? pages-fn]
+  [repo-url format ast properties file content utf8-content journal?]
   (try
     (let [now (tc/to-long (t/now))
-          blocks (block/extract-blocks ast (utf8/length utf8-content) utf8-content)
-          pages (pages-fn blocks ast)
+          page (get-page-name file ast)
+          [page page-name journal-day] (block/convert-page-if-journal page)
+          blocks (->> (block/extract-blocks ast content false format)
+                      (block/with-parent-and-left [:block/name (string/lower-case page)]))
           ref-pages (atom #{})
           ref-tags (atom #{})
-          blocks (doall
-                  (mapcat
-                   (fn [[page blocks]]
-                     (if page
-                       (map (fn [block]
-                              (let [block-ref-pages (seq (:block/ref-pages block))
-                                    block-path-ref-pages (seq (:block/path-ref-pages block))]
-                                (when block-ref-pages
-                                  (swap! ref-pages set/union (set block-ref-pages)))
-                                (-> block
-                                    (dissoc :ref-pages)
-                                    (assoc :block/content (db/get-block-content utf8-content block)
-                                           :block/file [:file/path file]
-                                           :block/format format
-                                           :block/page [:page/name (string/lower-case page)]
-                                           :block/ref-pages (mapv
-                                                             (fn [page]
-                                                               (block/page-with-journal page))
-                                                             block-ref-pages)
-                                           :block/path-ref-pages block-path-ref-pages))))
-                            blocks)))
-                   (remove nil? pages)))
-          pages (doall
-                 (map
-                  (fn [page]
-                    (let [page-file? (= page (string/lower-case file))
-                          aliases (and (:alias properties)
-                                       (seq (remove #(= page %)
-                                                    (:alias properties))))
-                          journal-date-long (if journal?
-                                              (date/journal-title->long (string/capitalize page)))
-                          page-list (when-let [list-content (:list properties)]
-                                      (extract-page-list list-content))]
-                      (cond->
-                        (util/remove-nils
-                         {:page/name (string/lower-case page)
-                          :page/original-name page
-                          :page/file [:file/path file]
-                          :page/journal? journal?
-                          :page/journal-day (if journal?
-                                              (date/journal-title->int page)
-                                              0)})
-                        (seq properties)
-                        (assoc :page/properties properties)
+          blocks (map (fn [block]
+                        (let [block-ref-pages (seq (:block/refs block))
+                              block-path-ref-pages (seq (:block/path-refs block))]
+                          (when block-ref-pages
+                            (swap! ref-pages set/union (set block-ref-pages)))
+                          (-> block
+                              (dissoc :ref-pages)
+                              (assoc :block/file [:file/path file]
+                                     :block/format format
+                                     :block/page [:block/name (string/lower-case page)]
+                                     :block/refs block-ref-pages
+                                     :block/path-refs block-path-ref-pages))))
+                   blocks)
+          page-entity (let [page-file? (= page (string/lower-case file))
+                            aliases (and (:alias properties)
+                                         (seq (remove #(= page %)
+                                                      (:alias properties))))
+                            page-list (when-let [list-content (:list properties)]
+                                        (extract-page-list list-content))]
+                        (cond->
+                          (util/remove-nils
+                           (assoc
+                            (block/page-name->map page false)
+                            :block/file [:file/path file]))
+                          (seq properties)
+                          (assoc :block/properties properties)
 
-                        aliases
-                        (assoc :page/alias
-                               (map
-                                 (fn [alias]
-                                   (let [page-name (string/lower-case alias)
-                                         aliases (distinct
-                                                  (conj
-                                                   (remove #{alias} aliases)
-                                                   page))
-                                         aliases (if (seq aliases)
-                                                   (map
-                                                     (fn [alias]
-                                                       {:page/name alias})
-                                                     aliases))]
-                                     (if (seq aliases)
-                                       {:page/name page-name
-                                        :page/alias aliases}
-                                       {:page/name page-name})))
-                                 aliases))
+                          aliases
+                          (assoc :block/alias
+                                 (map
+                                   (fn [alias]
+                                     (let [page-name (string/lower-case alias)
+                                           aliases (distinct
+                                                    (conj
+                                                     (remove #{alias} aliases)
+                                                     page))
+                                           aliases (if (seq aliases)
+                                                     (map
+                                                       (fn [alias]
+                                                         {:block/name alias})
+                                                       aliases))]
+                                       (if (seq aliases)
+                                         {:block/name page-name
+                                          :block/alias aliases}
+                                         {:block/name page-name})))
+                                   aliases))
 
-                        (:tags properties)
-                        (assoc :page/tags (let [tags (->> (:tags properties)
-                                                          (remove string/blank?))]
-                                            (swap! ref-tags set/union (set tags))
-                                            (map (fn [tag] {:page/name (string/lower-case tag)
-                                                            :page/original-name tag})
-                                              tags))))))
-                  (->> (map first pages)
-                       (remove string/blank?))))
-          pages (concat
-                 pages
-                 (map
-                  (fn [page]
-                    {:page/original-name page
-                     :page/name (string/lower-case page)})
-                  @ref-tags)
-                 (map
-                  (fn [page]
-                    {:page/original-name page
-                     :page/name (string/lower-case page)})
-                  @ref-pages))
+                          (:tags properties)
+                          (assoc :block/tags (let [tags (->> (:tags properties)
+                                                             (remove string/blank?))]
+                                               (swap! ref-tags set/union (set tags))
+                                               (map (fn [tag] {:block/name (string/lower-case tag)
+                                                               :block/original-name tag})
+                                                 tags)))))
+          pages (->> (concat
+                      [page-entity]
+                      @ref-pages
+                      (map
+                        (fn [page]
+                          {:block/original-name page
+                           :block/name (string/lower-case page)})
+                        @ref-tags))
+                     ;; remove block references
+                     (remove vector?))
+          pages (util/distinct-by :block/name pages)
           block-ids (mapv (fn [block]
                             {:block/uuid (:block/uuid block)})
-                          (remove nil? blocks))]
-      [(remove nil? pages)
+                          (remove nil? blocks))
+          pages (remove nil? pages)
+          pages (map (fn [page] (assoc page :block/uuid (db/new-block-id))) pages)]
+      [pages
        block-ids
        (remove nil? blocks)])
     (catch js/Error e
-      (js/console.log e))))
+      (log/error :exception e))))
 
 (defn extract-blocks-pages
-  [repo-url file content utf8-content]
-  (if (string/blank? content)
-    []
-    (let [journal? (util/journal? file)
-          format (format/get-format file)
-          ast (mldoc/->edn content
-                           (mldoc/default-config format))
-          first-block (first ast)
-          properties (let [properties (and (seq first-block)
-                                           (= "Properties" (ffirst first-block))
-                                           (last (first first-block)))]
-                       (if (and properties (seq properties))
-                         properties))]
-      (extract-pages-and-blocks
-       repo-url
-       format ast properties
-       file content utf8-content journal?
-       (fn [blocks ast]
-         [[(db/get-page-name file ast) blocks]])))))
+  ([repo-url file content]
+   (extract-blocks-pages repo-url file content (utf8/encode content)))
+  ([repo-url file content utf8-content]
+   (if (string/blank? content)
+     []
+     (let [journal? (config/journal? file)
+           format (format/get-format file)
+           ast (mldoc/->edn content
+                            (mldoc/default-config format))
+           first-block (ffirst ast)
+           properties (let [properties (and (property/properties-ast? first-block)
+                                            (->> (last first-block)
+                                                 (into {})
+                                                 (walk/keywordize-keys)))]
+                        (when (and properties (seq properties))
+                          (if (:filters properties)
+                            (update properties :filters
+                                    (fn [v]
+                                      (string/replace (or v "") "\\" "")))
+                            properties)))]
+       (extract-pages-and-blocks
+        repo-url
+        format ast properties
+        file content utf8-content journal?)))))
+
+(defn with-block-uuid
+  [pages]
+  (->> (util/distinct-by :block/name pages)
+       (map (fn [page]
+              (if (:block/uuid page)
+                page
+                (assoc page :block/uuid (db/new-block-id)))))))
+
+(defn with-ref-pages
+  [pages blocks]
+  (let [ref-pages (->> (mapcat :block/refs blocks)
+                       (filter :block/name))]
+    (->> (concat pages ref-pages)
+         (group-by :block/name)
+         vals
+         (map (partial apply merge))
+         (with-block-uuid))))
 
 (defn extract-all-blocks-pages
-  [repo-url files]
+  [repo-url files metadata]
   (when (seq files)
     (let [result (->> files
                       (map
@@ -160,11 +196,19 @@
                       (remove empty?))]
       (when (seq result)
         (let [[pages block-ids blocks] (apply map concat result)
-              block-ids-set (set (map (fn [{:block/keys [uuid]}] [:block/uuid uuid]) block-ids))
+              pages (with-ref-pages pages blocks)
+              blocks (map (fn [block]
+                            (let [id (:block/uuid block)
+                                  properties (get-in metadata [:block/properties id])]
+                              (update block :block/properties merge properties)))
+                          blocks)
               ;; To prevent "unique constraint" on datascript
-              pages-index (map #(select-keys % [:page/name]) pages)
+              pages-index (map #(select-keys % [:block/name]) pages)
+              block-ids-set (set (map (fn [{:block/keys [uuid]}] [:block/uuid uuid]) block-ids))
               blocks (map (fn [b]
-                            (-> b
-                                (update :block/ref-blocks #(set/intersection (set %) block-ids-set))
-                                (update :block/embed-blocks #(set/intersection (set %) block-ids-set)))) blocks)]
+                            (update b :block/refs
+                                    (fn [refs]
+                                      (set/union
+                                       (filter :block/name refs)
+                                       (set/intersection (set refs) block-ids-set))))) blocks)]
           (apply concat [pages-index pages block-ids blocks]))))))

+ 41 - 52
src/main/frontend/handler/file.cljs

@@ -8,7 +8,6 @@
             [frontend.db :as db]
             [frontend.git :as git]
             [frontend.handler.common :as common-handler]
-            [frontend.handler.git :as git-handler]
             [frontend.handler.extract :as extract-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.route :as route-handler]
@@ -16,15 +15,13 @@
             [frontend.config :as config]
             [frontend.format :as format]
             [clojure.string :as string]
-            [frontend.history :as history]
-            [frontend.handler.project :as project-handler]
             [lambdaisland.glogi :as log]
             [clojure.core.async :as async]
             [cljs.core.async.interop :refer-macros [<p!]]
-            [goog.object :as gobj]
             [cljs-time.core :as t]
             [cljs-time.coerce :as tc]
             [frontend.utf8 :as utf8]
+            [borkdude.rewrite-edn :as rewrite]
             ["/frontend/utils" :as utils]))
 
 ;; TODO: extract all git ops using a channel
@@ -84,14 +81,7 @@
    (let [config-content (if config-content config-content
                             (common-handler/get-config repo-url))]
      (when config-content
-       (let [old-project (:project (state/get-config))
-             new-config (common-handler/reset-config! repo-url config-content)]
-         (when (and (not (config/local-db? repo-url))
-                    project-changed-check?)
-           (let [new-project (:project new-config)
-                 project-name (:name old-project)]
-             (when-not (= new-project old-project)
-               (project-handler/sync-project-settings! project-name new-project)))))))))
+       (common-handler/reset-config! repo-url config-content)))))
 
 (defn load-files
   [repo-url]
@@ -152,7 +142,8 @@
           file-content [{:file/path file}]
           tx (if (contains? config/mldoc-support-formats format)
                (let [delete-blocks (db/delete-file-blocks! repo-url file)
-                     [pages block-ids blocks] (extract-handler/extract-blocks-pages repo-url file content utf8-content)]
+                     [pages block-ids blocks] (extract-handler/extract-blocks-pages repo-url file content utf8-content)
+                     pages (extract-handler/with-ref-pages pages blocks)]
                  (concat file-content delete-blocks pages block-ids blocks))
                file-content)
           tx (concat tx [(let [t (tc/to-long (t/now))]
@@ -179,21 +170,20 @@
       (do
         (when-let [page-id (db/get-file-page-id path)]
           (db/transact! repo
-            [[:db/retract page-id :page/alias]
-             [:db/retract page-id :page/tags]]))
+            [[:db/retract page-id :block/alias]
+             [:db/retract page-id :block/tags]]))
         (reset-file! repo path content))
       (db/set-file-content! repo path content))
     (util/p-handle (write-file!)
                    (fn [_]
-                     (when-not from-disk?
-                       (git-handler/git-add repo path update-status?))
                      (when (= path (config/get-config-path repo))
                        (restore-config! repo true))
                      (when (= path (config/get-custom-css-path repo))
                        (ui-handler/add-style-if-exists!))
                      (when re-render-root? (ui-handler/re-render-root!))
-                     (when (and add-history? original-content)
-                       (history/add-history! repo [[path original-content content]])))
+                     ;; (when (and add-history? original-content)
+                     ;;   (history/add-history! repo [[path original-content content]]))
+                     )
                    (fn [error]
                      (println "Write file failed, path: " path ", content: " content)
                      (log/error :write/failed error)))))
@@ -216,7 +206,7 @@
                                    :path-params {:path path}}))))))
 
 (defn alter-files
-  [repo files {:keys [add-history? update-status? git-add-cb reset? update-db? chan chan-callback resolved-handler]
+  [repo files {:keys [add-history? update-status? finish-handler reset? update-db? chan chan-callback resolved-handler]
                :or {add-history? true
                     update-status? true
                     reset? false
@@ -240,7 +230,7 @@
           (chan-callback))))))
 
 (defn alter-files-handler!
-  [repo files {:keys [add-history? update-status? git-add-cb reset? chan]
+  [repo files {:keys [add-history? update-status? finish-handler reset? chan]
                :or {add-history? true
                     update-status? true
                     reset? false}} file->content]
@@ -253,29 +243,19 @@
                                         (log/error :write-file/failed {:path path
                                                                        :content content
                                                                        :error error}))))))
-        git-add-f (fn []
-                    (let [add-helper
-                          (fn []
-                            (map
-                              (fn [[path content]]
-                                (git-handler/git-add repo path update-status?))
-                              files))]
-                      (-> (p/all (add-helper))
-                          (p/then (fn [_]
-                                    (when git-add-cb
-                                      (git-add-cb))))
-                          (p/catch (fn [error]
-                                     (println "Git add failed:")
-                                     (js/console.error error)))))
-                    (ui-handler/re-render-file!)
-                    (when add-history?
-                      (let [files-tx (mapv (fn [[path content]]
-                                             (let [original-content (get file->content path)]
-                                               [path original-content content])) files)]
-                        (history/add-history! repo files-tx))))]
+        finish-handler (fn []
+                         (when finish-handler
+                           (finish-handler))
+                         (ui-handler/re-render-file!)
+                         ;; (when add-history?
+                         ;;   (let [files-tx (mapv (fn [[path content]]
+                         ;;                          (let [original-content (get file->content path)]
+                         ;;                            [path original-content content])) files)]
+                         ;;     (history/add-history! repo files-tx)))
+                         )]
     (-> (p/all (map write-file-f files))
         (p/then (fn []
-                  (git-add-f)
+                  (finish-handler)
                   (when chan
                     (async/put! chan true))))
         (p/catch (fn [error]
@@ -288,7 +268,7 @@
   (when-not (string/blank? file)
     (->
      (p/let [_ (or (config/local-db? repo) (git/remove-file repo file))
-             result (fs/unlink! (config/get-repo-path repo file) nil)]
+             _ (fs/unlink! (config/get-repo-path repo file) nil)]
        (when-let [file (db/entity repo [:file/path file])]
          (common-handler/check-changed-files-status)
          (let [file-id (:db/id file)
@@ -302,13 +282,6 @@
      (p/catch (fn [err]
                 (js/console.error "error: " err))))))
 
-(defn re-index!
-  [file]
-  (when-let [repo (state/get-current-repo)]
-    (let [path (:file/path file)
-          content (db/get-file path)]
-      (alter-file repo path content {:re-render-root? true}))))
-
 ;; TODO: batch writes, how to deal with file history?
 (defn run-writes-chan!
   []
@@ -339,5 +312,21 @@
     (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
             file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
       (when-not file-exists?
-        (reset-file! repo-url path default-content)
-        (git-handler/git-add repo-url path)))))
+        (reset-file! repo-url path default-content)))))
+
+(defn edn-file-set-key-value
+  [path k v ok-handler]
+  (when-let [repo (state/get-current-repo)]
+    (when-let [content (db/get-file-no-sub path)]
+      (let [result (try
+                     (rewrite/parse-string content)
+                     (catch js/Error e
+                       (println "Parsing config file failed: ")
+                       (js/console.dir e)
+                       {}))
+            ks (if (vector? k) k [k])
+            new-result (rewrite/assoc-in result ks v)]
+        (when ok-handler (ok-handler repo new-result))
+        (state/set-config! repo new-result)
+        (let [new-content (str new-result)]
+          (set-file-content! repo path new-content))))))

+ 1 - 18
src/main/frontend/handler/git.cljs

@@ -10,9 +10,7 @@
             [frontend.handler.route :as route-handler]
             [frontend.handler.common :as common-handler]
             [frontend.config :as config]
-            [cljs-time.local :as tl]
-            [clojure.string :as string]
-            [goog.object :as gobj]))
+            [cljs-time.local :as tl]))
 
 (defn- set-git-status!
   [repo-url value]
@@ -63,18 +61,3 @@
      (config/get-repo-dir repo-url)
      name
      email)))
-
-(defn show-commit-modal!
-  [modal]
-  (fn [e]
-    (when (and
-          (string/starts-with? (state/get-current-repo) "https://")
-          (not (util/input? (gobj/get e "target")))
-          (not (gobj/get e "shiftKey"))
-          (not (gobj/get e "ctrlKey"))
-          (not (gobj/get e "altKey"))
-          (not (gobj/get e "metaKey")))
-     (when-let [repo-url (state/get-current-repo)]
-       (when-not (state/get-edit-input-id)
-         (util/stop e)
-         (state/set-modal! modal))))))

+ 17 - 12
src/main/frontend/handler/graph.cljs

@@ -52,10 +52,11 @@
 
 (defn- uuid-or-asset?
   [id]
-  (or (util/uuid-string? id)
-      (string/starts-with? id "../assets/")
-      (= id "..")
-      (string/starts-with? id "assets/")))
+  (let [id (str id)]
+    (or (util/uuid-string? id)
+       (string/starts-with? id "../assets/")
+       (= id "..")
+       (string/starts-with? id "assets/"))))
 
 (defn- remove-uuids-and-files!
   [nodes]
@@ -70,10 +71,14 @@
                                     (map :source links)
                                     (map :target links)]))
                        (map string/lower-case))
-        names (db/pull-many '[:page/name :page/original-name] (mapv (fn [page] [:page/name page]) all-pages))
-        names (zipmap (map :page/name names)
-                      (map (fn [x] (get x :page/original-name (:page/name x))) names))
-        nodes (mapv (fn [node] (assoc node :id (get names (:id node)))) nodes)
+        names (db/pull-many '[:block/name :block/original-name] (mapv (fn [page]
+                                                                        (if (util/uuid-string? page)
+                                                                          [:block/uuid (uuid page)]
+                                                                          [:block/name page])) all-pages))
+        names (zipmap (map (fn [x] (get x :block/name)) names)
+                      (map (fn [x]
+                             (get x :block/original-name (:block/name x))) names))
+        nodes (mapv (fn [node] (assoc node :id (get names (:id node) (:id node)))) nodes)
         links (->>
                (mapv (fn [{:keys [source target]}]
                        (when (and (not (uuid-or-asset? source))
@@ -89,7 +94,7 @@
 (defn build-global-graph
   [theme show-journal?]
   (let [dark? (= "dark" theme)
-        current-page (:page/name (db/get-current-page))]
+        current-page (:block/name (db/get-current-page))]
     (when-let [repo (state/get-current-repo)]
       (let [relation (db/get-pages-relation repo show-journal?)
             tagged-pages (db/get-all-tagged-pages repo)
@@ -128,9 +133,9 @@
   (let [dark? (= "dark" theme)]
     (when-let [repo (state/get-current-repo)]
       (let [page (string/lower-case page)
-            page-entity (db/entity [:page/name page])
-            original-page-name (:page/original-name page-entity)
-            tags (:tags (:page/properties page-entity))
+            page-entity (db/entity [:block/name page])
+            original-page-name (:block/original-name page-entity)
+            tags (:tags (:block/properties page-entity))
             tags (remove #(= page %) tags)
             ref-pages (db/get-page-referenced-pages repo page)
             mentioned-pages (db/get-pages-that-mentioned-page repo page)

+ 24 - 47
src/main/frontend/handler/history.cljs

@@ -1,7 +1,6 @@
 (ns frontend.handler.history
   (:require [frontend.state :as state]
             [frontend.db :as db]
-            [frontend.history :as history]
             [frontend.handler.file :as file]
             [frontend.handler.editor :as editor]
             [frontend.handler.ui :as ui-handler]
@@ -11,7 +10,8 @@
             [goog.object :as gobj]
             [dommy.core :as d]
             [frontend.util :as util]
-            [medley.core :as medley]))
+            [medley.core :as medley]
+            [frontend.modules.editor.undo-redo :as undo-redo]))
 
 (defn- default-undo
   []
@@ -22,53 +22,30 @@
   (js/document.execCommand "redo" false nil))
 
 (defn restore-cursor!
-  [{:keys [block-container block-idx pos] :as state}]
+  [{:keys [last-edit-block container pos] :as state}]
   (ui-handler/re-render-root!)
-  ;; get the element
-  (when (and block-container block-idx pos)
-    (when-let [container (gdom/getElement block-container)]
-      (let [blocks (d/by-class container "ls-block")
-            block-node (util/nth-safe (seq blocks) block-idx)
-            id (and block-node (gobj/get block-node "id"))]
-        (when id
-          (let [block-id (->> (take-last 36 id)
-                              (apply str))
-                block-uuid (when (util/uuid-string? block-id)
-                             (uuid block-id))]
-            (when block-uuid
-              (when-let [block (db/pull [:block/uuid block-uuid])]
-                (editor/edit-block! block pos
-                                    (:block/format block)
-                                    (:block/uuid block))))))))))
+  (when (and container last-edit-block)
+    (when-let [container (gdom/getElement container)]
+      (when-let [block-uuid (:block/uuid last-edit-block)]
+        (when-let [block (db/pull [:block/uuid block-uuid])]
+          (editor/edit-block! block pos
+                              (:block/format block)
+                              (:block/uuid block)
+                              {:custom-content (:block/content block)}))))))
 
 (defn undo!
-  []
-  (when-not (state/get-editor-op)
-    (let [route (get-in (:route-match @state/state) [:data :name])]
-      (if (and (contains? #{:home :page :file} route)
-               (state/get-current-repo))
-        (let [repo (state/get-current-repo)
-              chan (async/promise-chan)
-              save-commited? (atom nil)
-              undo-fn (fn []
-                        (history/undo! repo file/alter-file restore-cursor!))]
-          (editor/save-current-block-when-idle! {:check-idle? false
-                                                 :chan chan
-                                                 :chan-callback (fn []
-                                                                  (reset! save-commited? true))})
-          (if @save-commited?
-            (async/go
-              (let [_ (async/<! chan)]
-                (undo-fn)))
-            (undo-fn)))
-        (default-undo)))))
+  [e]
+  (util/stop e)
+  (state/set-editor-op! :undo)
+  (editor/save-current-block!)
+  (let [{:keys [editor-cursor]} (undo-redo/undo)]
+    (restore-cursor! editor-cursor))
+  (state/set-editor-op! nil))
 
 (defn redo!
-  []
-  (when-not (state/get-editor-op)
-    (let [route (get-in (:route-match @state/state) [:data :name])]
-     (if (and (contains? #{:home :page :file} route)
-              (state/get-current-repo))
-       (let [repo (state/get-current-repo)]
-         (history/redo! repo file/alter-file restore-cursor!))
-       (default-redo)))))
+  [e]
+  (util/stop e)
+  (state/set-editor-op! :redo)
+  (let [{:keys [editor-cursor]} (undo-redo/redo)]
+    (restore-cursor! editor-cursor))
+  (state/set-editor-op! nil))

+ 31 - 2
src/main/frontend/handler/metadata.cljs

@@ -4,6 +4,7 @@
             [cljs.reader :as reader]
             [frontend.config :as config]
             [frontend.db :as db]
+            [datascript.db :as ddb]
             [clojure.string :as string]
             [promesa.core :as p]))
 
@@ -23,11 +24,16 @@
                            (println "Parsing metadata.edn failed: ")
                            (js/console.dir e)
                            {}))
-              ks (if (vector? k) k [k])
-              new-metadata (assoc-in metadata ks v)
+              new-metadata (cond
+                             (= k :block/properties)
+                             (update metadata :block/properties v) ; v should be a function
+                             :else
+                             (let [ks (if (vector? k) k [k])]
+                               (assoc-in metadata ks v)))
               new-metadata (if encrypted?
                              (assoc new-metadata :db/encrypted? true)
                              new-metadata)
+              _ (prn "New metadata:\n" new-metadata)
               new-content (pr-str new-metadata)]
           (file-handler/set-file-content! repo path new-content))))))
 
@@ -35,3 +41,26 @@
   [encrypted-secret]
   (when-not (string/blank? encrypted-secret)
     (set-metadata! :db/encrypted-secret encrypted-secret)))
+
+(defn- handler-properties!
+  [all-properties properties-tx]
+  (reduce
+   (fn [acc datom]
+     (let [v (:v datom)
+           id (or (get v :id)
+                  (get v :title))]
+       (if id
+         (let [added? (ddb/datom-added datom)
+               remove-all-properties? (and (not added?)
+                                           ;; only id
+                                           (= 1 (count v)))]
+           (if remove-all-properties?
+             (dissoc acc id)
+             (assoc acc id v)))
+         acc)))
+   all-properties
+   properties-tx))
+
+(defn update-properties!
+  [properties-tx]
+  (set-metadata! :block/properties #(handler-properties! % properties-tx)))

+ 185 - 330
src/main/frontend/handler/page.cljs

@@ -9,30 +9,31 @@
             [frontend.handler.route :as route-handler]
             [frontend.handler.file :as file-handler]
             [frontend.handler.repo :as repo-handler]
-            [frontend.handler.git :as git-handler]
             [frontend.handler.editor :as editor-handler]
-            [frontend.handler.project :as project-handler]
             [frontend.handler.web.nfs :as web-nfs]
             [frontend.handler.notification :as notification]
             [frontend.handler.config :as config-handler]
             [frontend.handler.ui :as ui-handler]
+            [frontend.modules.outliner.file :as outliner-file]
+            [frontend.modules.outliner.core :as outliner-core]
+            [frontend.modules.outliner.tree :as outliner-tree]
             [frontend.commands :as commands]
             [frontend.date :as date]
             [clojure.walk :as walk]
             [frontend.git :as git]
             [frontend.fs :as fs]
+            [frontend.util.property :as property]
             [promesa.core :as p]
             [lambdaisland.glogi :as log]
-            [frontend.format.mldoc :as mldoc]
-            [cljs-time.core :as t]
-            [cljs-time.coerce :as tc]
+            [frontend.format.block :as block]
             [cljs.reader :as reader]
-            [goog.object :as gobj]))
+            [goog.object :as gobj]
+            [clojure.data :as data]))
 
 (defn- get-directory
   [journal?]
   (if journal?
-    config/default-journals-directory
+    (config/get-journals-directory)
     (config/get-pages-directory)))
 
 (defn- get-file-name
@@ -43,122 +44,102 @@
     ;; Win10 file path has a length limit of 260 chars
     (util/safe-subs s 0 200)))
 
+(defn get-page-file-path
+  ([] (get-page-file-path (state/get-current-page)))
+  ([page-name] (when-let [page (db/entity [:block/name page-name])]
+                 (:file/path (:block/file page)))))
+
+(defn default-properties-block
+  [title format page]
+  (let [properties (common-handler/get-page-default-properties title)
+        content (property/build-properties-str format properties)]
+    {:block/pre-block? true
+     :block/uuid (db/new-block-id)
+     :block/properties properties
+     :block/left page
+     :block/format format
+     :block/content content
+     :block/parent page
+     :block/unordered true
+     :block/page page}))
+
 (defn create!
   ([title]
    (create! title {}))
   ([title {:keys [redirect?]
            :or {redirect? true}}]
-   (let [title (and title (string/trim title))
-         repo (state/get-current-repo)
-         dir (config/get-repo-dir repo)
-         journal-page? (date/valid-journal-title? title)
-         directory (get-directory journal-page?)]
-     (when dir
-       (p/let [_ (-> (fs/mkdir! (str dir "/" directory))
-                     (p/catch (fn [_e])))]
-         (let [format (name (state/get-preferred-format))
-               page (string/lower-case title)
-               path (str (get-file-name journal-page? title)
-                         "."
-                         (if (= format "markdown") "md" format))
-               path (str directory "/" path)
-               file-path (str "/" path)]
-           (p/let [exists? (fs/file-exists? dir file-path)]
-             (if exists?
-               (notification/show!
-                [:p.content
-                 (util/format "File %s already exists!" file-path)]
-                :error)
-               ;; Create the file
-               (let [content (util/default-content-with-title format title)]
-                 ;; Write to the db first, then write to the filesystem,
-                 ;; otherwise, the main electron ipc will notify that there's
-                 ;; a new file created.
-                 ;; Question: what if the fs write failed?
-                 (p/let [_ (file-handler/reset-file! repo path content)
-                         _ (fs/create-if-not-exists repo dir file-path content)
-                         _ (git-handler/git-add repo path)]
-                   (when redirect?
-                     (route-handler/redirect! {:to :page
-                                               :path-params {:name page}})
-
-                     ;; Edit the first block
-                     (let [blocks (db/get-page-blocks page)
-                           last-block (last blocks)]
-                       (when last-block
-                         (js/setTimeout
-                          #(editor-handler/edit-last-block-for-new-page! last-block 0)
-                          100))))))))))))))
-
-(defn page-add-properties!
-  [page-name properties]
-  (let [page (db/entity [:page/name page-name])
-        page-title (:or (:page/original-name page) (:page/name page))
-        file (:page/file page)]
-    (if file
-      (let [page-format (db/get-page-format page-name)
-            properties-content (db/get-page-properties-content page-name)
-            file (db/entity (:db/id (:page/file page)))
-            file-path (:file/path file)
-            file-content (db/get-file file-path)
-            after-content (if (string/blank? properties-content)
-                            file-content
-                            (subs file-content (inc (count properties-content))))
-            properties-content (if properties-content
-                                 (string/trim properties-content)
-                                 (config/properties-wrapper page-format))
-            new-properties-content (db/add-properties! page-format properties-content properties)
-            full-content (str new-properties-content "\n\n" (string/trim after-content))]
-        (file-handler/alter-file (state/get-current-repo)
-                                 file-path
-                                 full-content
-                                 {:reset? true
-                                  :re-render-root? true}))
-      (p/let [_ (create! page-name)]
-        (page-add-properties! page-name properties)))))
-
-(defn page-remove-property!
-  [page-name k]
-  (when-let [properties-content (string/trim (db/get-page-properties-content page-name))]
-    (let [page (db/entity [:page/name page-name])
-          file (db/entity (:db/id (:page/file page)))
-          file-path (:file/path file)
-          file-content (db/get-file file-path)
-          after-content (subs file-content (count properties-content))
-          page-format (db/get-page-format page-name)
-          new-properties-content (let [lines (string/split-lines properties-content)
-                                       prefix (case page-format
-                                                :org (str "#+" (string/upper-case k) ": ")
-                                                :markdown (str (string/lower-case k) ": ")
-                                                "")
-                                       exists? (atom false)
-                                       lines (remove #(util/starts-with? % prefix) lines)]
-                                   (string/join "\n" lines))
-          full-content (str new-properties-content "\n\n" (string/trim after-content))]
-      (file-handler/alter-file (state/get-current-repo)
-                               file-path
-                               full-content
-                               {:reset? true
-                                :re-render-root? true}))))
-
-(defn published-success-handler
-  [page-name]
-  (fn [result]
-    (let [permalink (:permalink result)]
-      (page-add-properties! page-name {"permalink" permalink})
-      (let [win (js/window.open (str
-                                 config/website
-                                 "/"
-                                 (state/get-current-project)
-                                 "/"
-                                 permalink))]
-        (.focus win)))))
-
-(defn published-failed-handler
-  [error]
-  (notification/show!
-   "Publish failed, please give it another try."
-   :error))
+   (let [title (string/trim title)
+         page (string/lower-case title)
+         tx (block/page-name->map title true)
+         format (state/get-preferred-format)
+         page-entity [:block/uuid (:block/uuid tx)]
+         create-title-property? (util/include-windows-reserved-chars? title)
+         default-properties (default-properties-block title format page-entity)
+         empty-block {:block/uuid (db/new-block-id)
+                      :block/left [:block/uuid (:block/uuid default-properties)]
+                      :block/format format
+                      :block/content ""
+                      :block/parent page-entity
+                      :block/unordered true
+                      :block/page page-entity}
+         txs (if create-title-property?
+               [tx default-properties empty-block]
+               [tx])]
+     (db/transact! txs)
+     (when redirect?
+      (route-handler/redirect! {:to :page
+                                :path-params {:name page}})
+      (when create-title-property?
+        (js/setTimeout (fn []
+                        (editor-handler/edit-block! empty-block 0 format (:block/uuid empty-block))) 50))))))
+
+(defn page-add-property!
+  [page-name key value]
+  (when-let [page (db/pull [:block/name (string/lower-case page-name)])]
+    (let [repo (state/get-current-repo)
+          key (keyword key)
+          pre-block (db/get-pre-block repo (:db/id page))
+          format (state/get-preferred-format)
+          page-id {:db/id (:db/id page)}
+          org? (= format :org)
+          value (if (contains? #{:filters} key) (pr-str value) value)]
+      (if pre-block
+        (let [properties (:block/properties pre-block)
+              new-properties (assoc properties key value)
+              content (:block/content pre-block)
+              front-matter? (property/front-matter? content)
+              new-content (property/insert-property format content key value front-matter?)
+              block {:db/id (:db/id pre-block)
+                     :block/properties new-properties
+                     :block/content new-content
+                     :block/page page-id}
+              tx [(assoc page-id :block/properties new-properties)
+                  block]]
+          ;; (util/pprint tx)
+          (db/transact! tx)
+          (db/refresh! repo {:key :block/change
+                             :data [block]}))
+        (let [block {:block/uuid (db/new-block-id)
+                     :block/left page-id
+                     :block/parent page-id
+                     :block/page page-id
+                     :block/title []
+                     :block/content (if org?
+                                      (str "#+" (string/upper-case (name key)) ": " value)
+                                      (str (name key) ":: " value))
+                     :block/format format
+                     :block/properties {key value}
+                     :block/file (:block/file page)
+                     :block/pre-block? true}]
+          (outliner-core/insert-node (outliner-core/block block)
+                                     (outliner-core/block page)
+                                     false)
+          (db/transact! [(assoc page-id :block/properties {key value})])
+          (db/refresh! repo {:key :block/change
+                             :data [block]})
+          ;; (ui-handler/re-render-root!)
+          ))
+      (outliner-file/sync-to-file page-id))))
 
 (defn get-plugins
   [blocks]
@@ -183,91 +164,6 @@
      (map :block/body blocks))
     @plugins))
 
-(defn publish-page-as-slide!
-  ([page-name project-add-modal export-page-html]
-   (publish-page-as-slide! page-name (db/get-page-blocks page-name) project-add-modal export-page-html))
-  ([page-name blocks project-add-modal export-page-html]
-   (project-handler/exists-or-create!
-    (fn [project]
-      (config-handler/set-config! [:project :name] project)
-      (page-add-properties! page-name {"published" true
-                                       "slide" true})
-      (let [properties (db/get-page-properties page-name)
-            plugins (get-plugins blocks)
-            data {:project project
-                  :title page-name
-                  :permalink (:permalink properties)
-                  :html (export-page-html page-name blocks notification/show!)
-                  :tags (:tags properties)
-                  :settings (merge
-                             (assoc properties
-                                    :slide true
-                                    :published true)
-                             plugins)
-                  :repo (state/get-current-repo)}]
-        (util/post (str config/api "pages")
-                   data
-                   (published-success-handler page-name)
-                   published-failed-handler)))
-    project-add-modal)))
-
-(defn publish-page!
-  [page-name project-add-modal export-page-html]
-  (project-handler/exists-or-create!
-   (fn [project]
-     (let [properties (db/get-page-properties page-name)
-           slide? (let [slide (:slide properties)]
-                    (or (true? slide)
-                        (= "true" slide)))
-           blocks (db/get-page-blocks page-name)
-           plugins (get-plugins blocks)]
-       (p/let [_ (config-handler/set-config! [:project :name] project)]
-         (if slide?
-           (publish-page-as-slide! page-name blocks project-add-modal)
-           (do
-             (page-add-properties! page-name {"published" true})
-             (let [data {:project project
-                         :title page-name
-                         :permalink (:permalink properties)
-                         :html (export-page-html page-name blocks notification/show!)
-                         :tags (:tags properties)
-                         :settings (merge properties plugins)
-                         :repo (state/get-current-repo)}]
-               (util/post (str config/api "pages")
-                          data
-                          (published-success-handler page-name)
-                          published-failed-handler))))
-         (state/close-modal!))))
-   project-add-modal))
-
-(defn unpublished-success-handler
-  [page-name]
-  (fn [result]
-    (state/close-modal!)
-    (notification/show!
-     "Un-publish successfully!"
-     :success)))
-
-(defn unpublished-failed-handler
-  [error]
-  (notification/show!
-   "Un-publish failed, please give it another try."
-   :error))
-
-(defn unpublish-page!
-  [page-name]
-  (page-add-properties! page-name {"published" false})
-  (let [properties (db/get-page-properties page-name)
-        permalink (:permalink properties)
-        project (state/get-current-project)]
-    (if (and project permalink)
-      (util/delete (str config/api project "/" permalink)
-                   (unpublished-success-handler page-name)
-                   unpublished-failed-handler)
-      (notification/show!
-       "Can't find the permalink of this page!"
-       :error))))
-
 (defn delete!
   [page-name ok-handler]
   (when page-name
@@ -278,9 +174,6 @@
           ;; delete file
           (when-not (string/blank? file-path)
             (db/transact! [[:db.fn/retractEntity [:file/path file-path]]])
-            (when-let [files-conn (db/get-files-conn repo)]
-              (d/transact! files-conn [[:db.fn/retractEntity [:file/path file-path]]]))
-
             (let [blocks (db/get-page-blocks page-name)
                   tx-data (mapv
                            (fn [block]
@@ -296,7 +189,7 @@
                (p/catch (fn [err]
                           (js/console.error "error: " err))))))
 
-          (db/transact! [[:db.fn/retractEntity [:page/name page-name]]])
+          (db/transact! [[:db.fn/retractEntity [:block/name page-name]]])
 
           (ok-handler))))))
 
@@ -312,18 +205,12 @@
 (defn rename-file!
   [file new-name ok-handler]
   (let [repo (state/get-current-repo)
+        file (db/pull (:db/id file))
         old-path (:file/path file)
         new-path (compute-new-file-path old-path new-name)]
     ;; update db
     (db/transact! repo [{:db/id (:db/id file)
                          :file/path new-path}])
-
-    ;; update files db
-    (let [conn (db/get-files-conn repo)]
-      (when-let [file (d/entity (d/db conn) [:file/path old-path])]
-        (d/transact! conn [{:db/id (:db/id file)
-                            :file/path new-path}])))
-
     (->
      (p/let [_ (fs/rename! repo
                            (if (util/electron?)
@@ -339,45 +226,80 @@
      (p/catch (fn [error]
                 (println "file rename failed: " error))))))
 
+;; FIXME: not safe
+(defn- replace-old-page!
+  [s old-name new-name]
+  (-> s
+      (string/replace (util/format "[[%s]]" old-name) (util/format "[[%s]]" new-name))
+      (string/replace (str "#" old-name) (str "#" new-name))))
+
+(defn- walk-replace-old-page!
+  [form old-name new-name]
+  (walk/postwalk (fn [f] (if (string? f)
+                          (if (= f old-name)
+                            new-name
+                            (replace-old-page! f old-name new-name))
+                           f)) form))
+
 (defn rename!
   [old-name new-name]
   (let [new-name (string/trim new-name)]
     (when-not (string/blank? new-name)
       (when (and old-name new-name)
-        (let [case-changed? (and (= (string/lower-case old-name)
-                                    (string/lower-case new-name))
-                                 (not= (string/trim old-name)
-                                       (string/trim new-name)))
-              name-changed? (not= (string/lower-case (string/trim old-name))
+        (let [name-changed? (not= (string/lower-case (string/trim old-name))
                                   (string/lower-case (string/trim new-name)))]
           (when-let [repo (state/get-current-repo)]
-            (when-let [page (db/entity [:page/name (string/lower-case old-name)])]
-              (let [old-original-name (:page/original-name page)
-                    file (:page/file page)
-                    journal? (:page/journal? page)]
-                (d/transact! (db/get-conn repo false)
-                  [{:db/id (:db/id page)
-                    :page/name (string/lower-case new-name)
-                    :page/original-name new-name}])
+            (when-let [page (db/pull [:block/name (string/lower-case old-name)])]
+              (let [old-original-name (:block/original-name page)
+                    file (:block/file page)
+                    journal? (:block/journal? page)
+                    properties-block (:data (outliner-tree/-get-down (outliner-core/block page)))
+                    properties-block-tx (when (and properties-block
+                                                   (string/includes? (string/lower-case (:block/content properties-block))
+                                                                     (string/lower-case old-name)))
+                                          {:db/id (:db/id properties-block)
+                                           :block/content (property/insert-property (:block/format properties-block)
+                                                                                 (:block/content properties-block)
+                                                                                 :title
+                                                                                 new-name)})
+                    page-txs [{:db/id (:db/id page)
+                               :block/uuid (:block/uuid page)
+                               :block/name (string/lower-case new-name)
+                               :block/original-name new-name}]
+                    page-txs (if properties-block-tx (conj page-txs properties-block-tx) page-txs)]
+
+                (d/transact! (db/get-conn repo false) page-txs)
 
                 (when (and file (not journal?) name-changed?)
-                  (rename-file! file new-name
-                                (fn []
-                                  (page-add-properties! (string/lower-case new-name) {:title new-name}))))
+                  (rename-file! file new-name (fn [] nil)))
 
                 ;; update all files which have references to this page
-                (let [files (db/get-files-that-referenced-page (:db/id page))]
-                  (doseq [file-path files]
-                    (let [file-content (db/get-file file-path)
-                          ;; FIXME: not safe
-                          new-content (string/replace file-content
-                                                      (util/format "[[%s]]" old-original-name)
-                                                      (util/format "[[%s]]" new-name))]
-                      (file-handler/alter-file repo
-                                               file-path
-                                               new-content
-                                               {:reset? true
-                                                :re-render-root? false})))))
+                (let [blocks (db/get-page-referenced-blocks-no-cache (:db/id page))
+                      page-ids (->> (map :block/page blocks)
+                                    (remove nil?)
+                                    (set))
+                      tx (->> (map (fn [{:block/keys [uuid title content properties] :as block}]
+                                     (let [title (let [title' (walk-replace-old-page! title old-original-name new-name)]
+                                                   (when-not (= title' title)
+                                                     title'))
+                                           content (let [content' (replace-old-page! content old-original-name new-name)]
+                                                     (when-not (= content' content)
+                                                       content'))
+                                           properties (let [properties' (walk-replace-old-page! properties old-original-name new-name)]
+                                                        (when-not (= properties' properties)
+                                                          properties'))]
+                                       (when (or title content properties)
+                                         (util/remove-nils-non-nested
+                                          {:block/uuid uuid
+                                           :block/title title
+                                           :block/content content
+                                           :block/properties properties})))) blocks)
+                              (remove nil?))]
+                  (db/transact! repo tx)
+                  (doseq [page-id page-ids]
+                    (outliner-file/sync-to-file page-id)))
+
+                (outliner-file/sync-to-file page))
 
               ;; TODO: update browser history, remove the current one
 
@@ -391,43 +313,15 @@
 
               (ui-handler/re-render-root!))))))))
 
-(defn rename-when-alter-title-property!
-  [page path format original-content content]
-  (when (and page (contains? config/mldoc-support-formats format))
-    (let [old-name page
-          new-name (let [ast (mldoc/->edn content (mldoc/default-config format))]
-                     (db/get-page-name path ast))
-          journal? (date/valid-journal-title? old-name)]
-      (if (not= old-name new-name)
-        (if journal?
-          [true old-name]
-          (do
-            (rename! old-name new-name)
-            [false new-name]))
-        [journal? old-name]))))
-
 (defn handle-add-page-to-contents!
   [page-name]
-  (let [last-block (last (db/get-page-blocks (state/get-current-repo) "contents"))
-        last-empty? (>= 3 (count (:block/content last-block)))
-        heading-pattern (config/get-block-pattern (state/get-preferred-format))
-        pre-str (str heading-pattern heading-pattern)
-        new-content (if last-empty?
-                      (str pre-str " [[" page-name "]]")
-                      (str (string/trimr (:block/content last-block))
-                           "\n"
-                           pre-str " [[" page-name "]]"))]
-    (editor-handler/insert-new-block-aux!
-     last-block
-     new-content
-     {:create-new-block? false
-      :ok-handler
-      (fn [_]
-        (notification/show! "Added to contents!" :success)
-        (editor-handler/clear-when-saved!))
-      :with-level? true
-      :new-level 2
-      :current-page "Contents"})))
+  (let [content (str "[[" page-name "]]")]
+    (editor-handler/api-insert-new-block!
+     content
+     {:page "Contents"
+      :sibling? true})
+    (notification/show! "Added to contents!" :success)
+    (editor-handler/clear-when-saved!)))
 
 (defn has-more-journals?
   []
@@ -441,7 +335,7 @@
 
 (defn update-public-attribute!
   [page-name value]
-  (page-add-properties! page-name {:public value}))
+  (page-add-property! page-name :public value))
 
 (defn get-page-ref-text
   [page]
@@ -452,7 +346,7 @@
         page-name (string/lower-case page)]
     (if (and edit-block-file-path
              (state/org-mode-file-link? (state/get-current-repo)))
-      (if-let [ref-file-path (:file/path (:page/file (db/entity [:page/name page-name])))]
+      (if-let [ref-file-path (:file/path (:file/file (db/entity [:file/name page-name])))]
         (util/format "[[file:%s][%s]]"
                      (util/get-relative-path edit-block-file-path ref-file-path)
                      page)
@@ -471,42 +365,6 @@
   []
   (commands/init-commands! get-page-ref-text))
 
-(defn delete-page-from-logseq
-  [project permalink]
-  (let [url (util/format "%s%s/%s" config/api project permalink)]
-    (js/Promise.
-     (fn [resolve reject]
-       (util/delete url
-                    (fn [result]
-                      (resolve result))
-                    (fn [error]
-                      (log/error :page/http-delete-failed error)
-                      (reject error)))))))
-
-(defn get-page-list-by-project-name
-  [project]
-  (js/Promise.
-   (fn [resolve _]
-     (if-not (string? project)
-       (resolve :project-name-is-invalid)
-       (let [url (util/format "%sprojects/%s/pages" config/api project)]
-         (util/fetch url
-                     (fn [result]
-                       (log/debug :page/get-page-list result)
-                       (let [data (:result result)]
-                         (if (sequential? data)
-                           (do (state/set-published-pages data)
-                               (resolve data))
-                           (log/error :page/http-get-list-result-malformed result))))
-                     (fn [error]
-                       (log/error :page/http-get-list-failed error)
-                       (resolve error))))))))
-
-(defn update-state-and-notify
-  [page-name]
-  (page-add-properties! page-name {:published false})
-  (notification/show! (util/format "Remove Page \"%s\" from Logseq server success" page-name) :success))
-
 (defn add-page-to-recent!
   [repo page]
   (let [pages (or (db/get-key-value repo :recent/pages)
@@ -528,28 +386,25 @@
    (fn []
      (init-commands!))))
 
-
 ;; TODO: add use :file/last-modified-at
 (defn get-pages-with-modified-at
   [repo]
   (->> (db/get-modified-pages repo)
        (remove util/file-page?)))
 
-(defn save-filter!
-  [page-name filter-state]
-  (if (empty? filter-state)
-    (page-remove-property! page-name "filters")
-    (page-add-properties! page-name {"filters" filter-state})))
-
-(defn get-filter
+(defn get-filters
   [page-name]
   (let [properties (db/get-page-properties page-name)]
-    (atom (reader/read-string (get-in properties [:filters] "{}")))))
+    (reader/read-string (get properties :filters "{}"))))
+
+(defn save-filter!
+  [page-name filter-state]
+  (page-add-property! page-name :filters filter-state))
 
 (defn page-exists?
   [page-name]
   (when page-name
-    (db/entity [:page/name page-name])))
+    (db/entity [:block/name page-name])))
 
 ;; Editor
 (defn page-not-exists-handler
@@ -584,16 +439,16 @@
     (if (state/sub :editor/show-page-search-hashtag?)
       (fn [chosen _click?]
         (state/set-editor-show-page-search! false)
-        (let [chosen (if (re-find #"\s+" chosen)
+        (let [wrapped? (= "[[" (util/safe-subs edit-content (- pos 2) pos))
+              chosen (if (and (re-find #"\s+" chosen) (not wrapped?))
                        (util/format "[[%s]]" chosen)
                        chosen)]
           (editor-handler/insert-command! id
-                                          (str "#" chosen)
+                                          (str "#" (when wrapped? "[[") chosen)
                                           format
                                           {:last-pattern (let [q (if @editor-handler/*selected-text "" q)]
-                                                           (if (and q (string/starts-with? q "#"))
-                                                             q
-                                                             (str "#" q)))})))
+                                                           (str "#" (when wrapped? "[[") q))
+                                           :forward-pos (if wrapped? 3 2)})))
       (fn [chosen _click?]
         (state/set-editor-show-page-search! false)
         (let [page-ref-text (get-page-ref-text chosen)]

+ 0 - 112
src/main/frontend/handler/project.cljs

@@ -1,112 +0,0 @@
-(ns frontend.handler.project
-  (:require [frontend.state :as state]
-            [frontend.util :as util :refer-macros [profile]]
-            [clojure.string :as string]
-            [frontend.config :as config]
-            [frontend.handler.notification :as notification]
-            [lambdaisland.glogi :as log]))
-
-(defn get-current-project
-  [current-repo projects]
-  (let [project (some (fn [p]
-                        (when (= (:repo p) current-repo)
-                          p))
-                  projects)
-        project-name (:name project)]
-    (when-not (string/blank? project-name) project-name)))
-
-;; project exists and current user owns it
-;; if project not exists, the server will create it
-(defn project-exists?
-  [project]
-  (let [projects (set (map :name (:projects (state/get-me))))]
-    (and (seq projects) (contains? projects project))))
-
-(defn create-project!
-  ([ok-handler]
-   (create-project! (state/get-current-project) ok-handler))
-  ([project ok-handler]
-   (when (state/logged?)
-     (let [config (state/get-config)
-           data {:name project
-                 :repo (state/get-current-repo)
-                 :settings (or (get config :project)
-                               {:name project})}]
-       (util/post (str config/api "projects")
-                  data
-                  (fn [result]
-                    (when-not (:message result) ; exists
-                      (swap! state/state
-                             update-in [:me :projects]
-                             (fn [projects]
-                               (util/distinct-by :name (conj projects (:result result)))))
-                      ;; update config
-                      (ok-handler project)))
-                  (fn [error]
-                    (js/console.dir error)
-                    (notification/show! (util/format "Project \"%s\" already taken, please change to another name." project) :error)))))))
-
-(defn exists-or-create!
-  [ok-handler modal-content]
-  (when (state/logged?)
-    (if-let [project (state/get-current-project)]
-      (if (project-exists? project)
-        (ok-handler project)
-        (create-project! ok-handler))
-      (state/set-modal! modal-content))))
-
-(defn add-project!
-  [project ok-handler]
-  (when (state/logged?)
-    (create-project! project
-                     (fn [project]
-                       (when ok-handler (ok-handler project))
-                       (notification/show! (util/format "Project \"%s\" was created successfully." project) :success)
-                       (state/close-modal!)))))
-
-(defn sync-project-settings!
-  ([]
-   (when-let [project-name (state/get-current-project)]
-     (let [settings (:project (state/get-config))]
-       (sync-project-settings! project-name settings))))
-  ([project-name settings]
-   (when (state/logged?)
-     (when-let [repo (state/get-current-repo)]
-       (if (project-exists? project-name)
-         (util/post (str config/api "projects/" project-name)
-                    {:name project-name
-                     :settings settings
-                     :repo repo}
-                    (fn [response]
-                      (notification/show! "Project settings changed successfully!" :success))
-                    (fn [error]
-                      (println "Project settings updated failed, reason: ")
-                      (js/console.dir error)))
-         (when (and settings
-                    (not (string/blank? (:name settings)))
-                    (>= (count (string/trim (:name settings))) 2))
-           (add-project! (:name settings) nil)))))))
-
-(defn update-project
-  [project-name data]
-  (let [url (util/format "%sprojects/%s" config/api project-name)]
-    (js/Promise.
-      (fn [resolve reject]
-        (util/post url data
-          (fn [result]
-            (resolve result))
-          (fn [error]
-            (log/error :project/http-update-failed error)
-            (reject error)))))))
-
-(defn delete-project
-  [project-name]
-  (let [url (util/format "%sprojects/%s" config/api project-name)]
-    (js/Promise.
-      (fn [resolve reject]
-        (util/delete url
-          (fn [result]
-            (resolve result))
-          (fn [error]
-            (log/error :project/http-delete-failed error)
-            (reject error)))))))

+ 63 - 162
src/main/frontend/handler/repo.cljs

@@ -1,57 +1,36 @@
 (ns frontend.handler.repo
   (:refer-clojure :exclude [clone])
-  (:require [frontend.util :as util :refer-macros [profile]]
+  (:require [cljs-bean.core :as bean]
+            [clojure.string :as string]
+            [frontend.config :as config]
+            [frontend.date :as date]
+            [frontend.db :as db]
+            [frontend.dicts :as dicts]
+            [frontend.encrypt :as encrypt]
+            [frontend.format :as format]
             [frontend.fs :as fs]
             [frontend.fs.nfs :as nfs]
-            [promesa.core :as p]
-            [lambdaisland.glogi :as log]
-            [frontend.state :as state]
-            [frontend.db :as db]
-            [frontend.idb :as idb]
             [frontend.git :as git]
-            [cljs-bean.core :as bean]
-            [frontend.date :as date]
-            [frontend.config :as config]
-            [frontend.format :as format]
-            [frontend.search :as search]
-            [frontend.handler.ui :as ui-handler]
-            [frontend.handler.git :as git-handler]
+            [frontend.handler.common :as common-handler]
+            [frontend.handler.extract :as extract-handler]
             [frontend.handler.file :as file-handler]
+            [frontend.handler.git :as git-handler]
             [frontend.handler.notification :as notification]
             [frontend.handler.route :as route-handler]
-            [frontend.handler.common :as common-handler]
-            [frontend.handler.extract :as extract-handler]
-            [frontend.ui :as ui]
-            [clojure.string :as string]
-            [frontend.dicts :as dicts]
+            [frontend.handler.ui :as ui-handler]
+            [frontend.idb :as idb]
+            [frontend.search :as search]
             [frontend.spec :as spec]
-            [frontend.encrypt :as encrypt]
-            [goog.dom :as gdom]
-            [goog.object :as gobj]
-            ;; TODO: remove component dependency from handlers, we can use a core.async channel
-            [frontend.components.encryption :as encryption]))
+            [frontend.state :as state]
+            [frontend.util :as util]
+            [lambdaisland.glogi :as log]
+            [promesa.core :as p]
+            [shadow.resource :as rc]))
 
 ;; Project settings should be checked in two situations:
 ;; 1. User changes the config.edn directly in logseq.com (fn: alter-file)
 ;; 2. Git pulls the new change (fn: load-files)
 
-(defn show-install-error!
-  [repo-url title]
-  (spec/validate :repos/url repo-url)
-  (notification/show!
-   [:p.content
-    title
-    " "
-    [:span.mr-2
-     (util/format
-      "Please make sure that you've installed the logseq app for the repo %s on GitHub. "
-      repo-url)
-     (ui/button
-      "Install Logseq on GitHub"
-      :href (str "https://github.com/apps/" config/github-app-name "/installations/new"))]]
-   :error
-   false))
-
 (defn create-config-file-if-not-exists
   [repo-url]
   (spec/validate :repos/url repo-url)
@@ -64,8 +43,7 @@
         (p/let [file-exists? (fs/create-if-not-exists repo-url repo-dir (str app-dir "/" config/config-file) default-content)]
           (when-not file-exists?
             (file-handler/reset-file! repo-url path default-content)
-            (common-handler/reset-config! repo-url default-content)
-            (git-handler/git-add repo-url path)))))))
+            (common-handler/reset-config! repo-url default-content)))))))
 
 (defn create-contents-file
   [repo-url]
@@ -77,16 +55,13 @@
                   (config/get-file-extension format))
         file-path (str "/" path)
         default-content (case (name format)
-                          "org"
-                          "** What's **Contents**?\n*** It's a normal page called [[Contents]], you can use it for:\n**** 1. table of content/index/MOC\n**** 2. pinning/bookmarking favorites pages/blocks (e.g. [[Logseq]])\n**** 3. You can also put many different things, depending on your personal workflow."
-                          "markdown"
-                          "## What's **Contents**?\n### It's a normal page called [[Contents]], you can use it for:\n#### 1. table of content/index/MOC\n#### 2. pinning/bookmarking favorites pages/blocks (e.g. [[Logseq]])\n#### 3. You can also put many different things, depending on your personal workflow."
+                          "org" (rc/inline "contents.org")
+                          "markdown" (rc/inline "contents.md")
                           "")]
     (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" (state/get-pages-directory)))
             file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
       (when-not file-exists?
-        (file-handler/reset-file! repo-url path default-content)
-        (git-handler/git-add repo-url path)))))
+        (file-handler/reset-file! repo-url path default-content)))))
 
 (defn create-custom-theme
   [repo-url]
@@ -98,8 +73,7 @@
     (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
             file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
       (when-not file-exists?
-        (file-handler/reset-file! repo-url path default-content)
-        (git-handler/git-add repo-url path)))))
+        (file-handler/reset-file! repo-url path default-content)))))
 
 (defn create-dummy-notes-page
   [repo-url content]
@@ -122,7 +96,7 @@
            format (state/get-preferred-format repo-url)
            title (date/today)
            file-name (date/journal-title->default title)
-           default-content (util/default-content-with-title format title false)
+           default-content (util/default-content-with-title format)
            template (state/get-journal-template)
            template (if (and template
                              (not (string/blank? template)))
@@ -135,16 +109,16 @@
                      (str default-content template)
 
                      :else
-                     (util/default-content-with-title format title true))
-           path (str config/default-journals-directory "/" file-name "."
+                     default-content)
+           path (str (config/get-journals-directory) "/" file-name "."
                      (config/get-file-extension format))
            file-path (str "/" path)
-           page-exists? (db/entity repo-url [:page/name (string/lower-case title)])
+           page-exists? (db/entity repo-url [:block/name (string/lower-case title)])
            empty-blocks? (empty? (db/get-page-blocks-no-cache repo-url (string/lower-case title)))]
        (when (or empty-blocks?
                  (not page-exists?))
          (p/let [_ (nfs/check-directory-permission! repo-url)
-                 _ (fs/mkdir-if-not-exists (str repo-dir "/" config/default-journals-directory))
+                 _ (fs/mkdir-if-not-exists (str repo-dir "/" (config/get-journals-directory)))
                  file-exists? (fs/file-exists? repo-dir file-path)]
            (when-not file-exists?
              (file-handler/reset-file! repo-url path content)
@@ -184,23 +158,41 @@
    (create-contents-file repo-url)
    (create-custom-theme repo-url)))
 
+(defn- remove-non-exists-refs!
+  [data]
+  (let [block-ids (->> (map :block/uuid data)
+                       (remove nil?)
+                       (set))
+        keep-block-ref-f (fn [refs]
+                           (filter (fn [ref]
+                                     (if (and (vector? ref)
+                                              (= :block/uuid (first ref)))
+                                       (contains? block-ids (second ref))
+                                       ref)) refs))]
+    (map (fn [item]
+           (if (and (map? item)
+                    (:block/uuid item))
+             (update item :block/refs keep-block-ref-f)
+             item)) data)))
+
 (defn- reset-contents-and-blocks!
   [repo-url files blocks-pages delete-files delete-blocks]
   (db/transact-files-db! repo-url files)
   (let [files (map #(select-keys % [:file/path :file/last-modified-at]) files)
         all-data (-> (concat delete-files delete-blocks files blocks-pages)
-                     (util/remove-nils))]
+                     (util/remove-nils)
+                     (remove-non-exists-refs!))]
     (db/transact! repo-url all-data)))
 
 (defn- parse-files-and-create-default-files-inner!
-  [repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts]
+  [repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata]
   (let [parsed-files (filter
                       (fn [file]
                         (let [format (format/get-format (:file/path file))]
                           (contains? config/mldoc-support-formats format)))
                       files)
         blocks-pages (if (seq parsed-files)
-                       (extract-handler/extract-all-blocks-pages repo-url parsed-files)
+                       (extract-handler/extract-all-blocks-pages repo-url parsed-files metadata)
                        [])]
     (let [config-file (config/get-config-path)]
       (when (contains? (set file-paths) config-file)
@@ -210,25 +202,23 @@
     (reset-contents-and-blocks! repo-url files blocks-pages delete-files delete-blocks)
     (when first-clone?
       (if (and (not db-encrypted?) (state/enable-encryption? repo-url))
-        (state/set-modal!
-         (encryption/encryption-setup-dialog
-          repo-url
-          #(create-default-files! repo-url %)))
+        (state/pub-event! [:modal/encryption-setup-dialog repo-url
+                           #(create-default-files! repo-url %)])
         (create-default-files! repo-url db-encrypted?)))
     (when re-render?
       (ui-handler/re-render-root! re-render-opts))
     (state/set-importing-to-db! false)))
 
 (defn- parse-files-and-create-default-files!
-  [repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts]
+  [repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata]
   (if db-encrypted?
     (p/let [files (p/all
                    (map (fn [file]
                           (p/let [content (encrypt/decrypt (:file/content file))]
                             (assoc file :file/content content)))
                         files))]
-      (parse-files-and-create-default-files-inner! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts))
-    (parse-files-and-create-default-files-inner! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts)))
+      (parse-files-and-create-default-files-inner! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata))
+    (parse-files-and-create-default-files-inner! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata)))
 
 (defn parse-files-and-load-to-db!
   [repo-url files {:keys [first-clone? delete-files delete-blocks re-render? re-render-opts] :as opts
@@ -244,12 +234,11 @@
           db-encrypted? (:db/encrypted? metadata)
           db-encrypted-secret (if db-encrypted? (:db/encrypted-secret metadata) nil)]
       (if db-encrypted?
-        (state/set-modal!
-         (encryption/encryption-input-secret-dialog
-          repo-url
-          db-encrypted-secret
-          #(parse-files-and-create-default-files! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts)))
-        (parse-files-and-create-default-files! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts)))))
+        (let [close-fn #(parse-files-and-create-default-files! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata)]
+          (state/pub-event! [:modal/encryption-input-secret-dialog repo-url
+                             db-encrypted-secret
+                             close-fn]))
+        (parse-files-and-create-default-files! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata)))))
 
 (defn load-repo-to-db!
   [repo-url {:keys [first-clone? diffs nfs-files]
@@ -308,92 +297,6 @@
     (load-repo-to-db! repo-url {:first-clone? first-clone?
                                 :diffs diffs})))
 
-(defn rebuild-page-blocks-children
-  "For performance reason, we can update the :block/children value after every operation,
-  but it's hard to make sure that it's correct, also it needs more time to implement it.
-  We can improve it if the performance is really an issue."
-  [repo page]
-  (let [blocks (->>
-                (db/get-page-blocks-no-cache repo page {:pull-keys '[:db/id :block/uuid :block/level :block/pre-block? :block/meta]})
-                (remove :block/pre-block?)
-                (map #(select-keys % [:db/id :block/uuid :block/level]))
-                (reverse))
-        original-blocks blocks]
-    (loop [blocks blocks
-           tx []
-           children {}
-           last-level 10000]
-      (if (seq blocks)
-        (let [[{:block/keys [uuid level] :as block} & others] blocks
-              [tx children] (cond
-                              (< level last-level)        ; parent
-                              (let [cur-children (get children last-level)
-                                    tx (if (seq cur-children)
-                                         (vec
-                                          (concat
-                                           tx
-                                           (map
-                                            (fn [child]
-                                              [:db/add (:db/id block) :block/children [:block/uuid child]])
-                                            cur-children)))
-                                         tx)
-                                    children (-> children
-                                                 (dissoc last-level)
-                                                 (update level conj uuid))]
-                                [tx children])
-
-                              (> level last-level)        ; child of sibling
-                              (let [children (update children level conj uuid)]
-                                [tx children])
-
-                              :else                       ; sibling
-                              (let [children (update children last-level conj uuid)]
-                                [tx children]))]
-          (recur others tx children level))
-        ;; TODO: add top-level children to the "Page" block (we might remove the Page from db schema)
-        (when (seq tx)
-          (let [delete-tx (map (fn [block]
-                                 [:db/retract (:db/id block) :block/children])
-                               original-blocks)]
-            (->> (concat delete-tx tx)
-                 (remove nil?))))))))
-
-(defn transact-react-and-alter-file!
-  ([repo tx transact-option files]
-   (transact-react-and-alter-file! repo tx transact-option files {}))
-  ([repo tx transact-option files opts]
-   (spec/validate :repos/url repo)
-   (let [files (remove nil? files)
-         pages (->> (map db/get-file-page (map first files))
-                    (remove nil?))]
-     (let [edit-block (state/get-edit-block)
-           edit-input-id (state/get-edit-input-id)
-           block-element (when edit-input-id (gdom/getElement (string/replace edit-input-id "edit-block" "ls-block")))
-           {:keys [idx container]} (when block-element
-                                     (util/get-block-idx-inside-container block-element))]
-       (when (and idx container)
-         (state/set-state! :editor/last-edit-block {:block edit-block
-                                                    :idx idx
-                                                    :container (gobj/get container "id")})))
-
-     ;; try catch so that if db transaction failed, it'll not write to the files
-     (try
-       (do
-         (db/transact-react!
-          repo
-          tx
-          transact-option)
-
-         (when (seq pages)
-           (let [children-tx (mapcat #(rebuild-page-blocks-children repo %) pages)]
-             (when (seq children-tx)
-               (db/transact! repo children-tx))))
-
-         (when (seq files)
-           (file-handler/alter-files repo files opts)))
-       (catch js/Error e
-         (log/error :transact-react/failed e))))))
-
 (declare push)
 
 (defn get-diff-result
@@ -481,7 +384,7 @@
                     (cond
                       (string/includes? (str error) "404")
                       (do (log/error :git/pull-error error)
-                          (show-install-error! repo-url (util/format "Failed to fetch %s." repo-url)))
+                          (state/pub-event! [:repo/install-error repo-url (util/format "Failed to fetch %s." repo-url)]))
 
                       (string/includes? (str error) "401")
                       (let [remain-times (dec try-times)]
@@ -585,7 +488,7 @@
          (state/set-cloning! false)
          (git-handler/set-git-status! repo-url :clone-failed)
          (git-handler/set-git-error! repo-url e)
-         (show-install-error! repo-url (util/format "Failed to clone %s." repo-url)))))))
+         (state/pub-event! [:repo/install-error repo-url (util/format "Failed to clone %s." repo-url)]))))))
 
 (defn remove-repo!
   [{:keys [id url] :as repo}]
@@ -593,11 +496,10 @@
   (let [delete-db-f (fn []
                       (db/remove-conn! url)
                       (db/remove-db! url)
-                      (db/remove-files-db! url)
                       (search/remove-db! url)
                       (fs/rmdir! (config/get-repo-dir url))
                       (state/delete-repo! repo))]
-    (if (config/local-db? url)
+    (if (or (config/local-db? url) (= url "local"))
       (p/let [_ (idb/clear-local-db! url)] ; clear file handles
         (delete-db-f))
       (util/delete (str config/api "repos/" id)
@@ -696,7 +598,6 @@
     (db/remove-conn! url)
     (db/clear-query-state!)
     (-> (p/do! (db/remove-db! url)
-               (db/remove-files-db! url)
                (fs/rmdir! (config/get-repo-dir url))
                (clone-and-load-db url))
         (p/catch (fn [error]

+ 3 - 3
src/main/frontend/handler/route.cljs

@@ -55,9 +55,9 @@
               (str (subs content 0 48) "...")
               content))
           "Page no longer exists!!")
-        (let [page (db/pull [:page/name (string/lower-case name)])]
-          (or (:page/original-name page)
-              (:page/name page)
+        (let [page (db/pull [:block/name (string/lower-case name)])]
+          (or (:block/original-name page)
+              (:block/name page)
               "Logseq"))))
     :tag
     (str "#"  (:name path-params))

+ 14 - 13
src/main/frontend/handler/search.cljs

@@ -17,13 +17,11 @@
                  limit 20}
             :as opts}]
    (let [page-db-id (if (string? page-db-id)
-                      (:db/id (db/entity repo [:page/name (string/lower-case page-db-id)]))
-                      page-db-id)]
+                      (:db/id (db/entity repo [:block/name (string/lower-case page-db-id)]))
+                      page-db-id)
+         opts (if page-db-id (assoc opts :page (str page-db-id)) opts)]
      (p/let [blocks (search/block-search repo q opts)]
-      (let [blocks (if page-db-id
-                     (filter (fn [block] (= (get-in block [:block/page :db/id]) page-db-id)) blocks)
-                     blocks)
-            result (merge
+      (let [result (merge
                     {:blocks blocks
                      :has-more? (= limit (count blocks))}
                     (when-not page-db-id
@@ -33,13 +31,16 @@
         (swap! state/state assoc search-key result))))))
 
 (defn clear-search!
-  []
-  (swap! state/state assoc
-         :search/result nil
-         :search/q ""
-         :search/mode :global)
-  (when-let [input (gdom/getElement "search-field")]
-    (gobj/set input "value" "")))
+  ([]
+   (clear-search! true))
+  ([clear-search-mode?]
+   (let [m (cond-> {:search/result nil
+                    :search/q ""}
+             clear-search-mode?
+             (assoc :search/mode :global))]
+     (swap! state/state merge m))
+   (when-let [input (gdom/getElement "search-field")]
+     (gobj/set input "value" ""))))
 
 (defn rebuild-indices!
   []

部分文件因文件數量過多而無法顯示