瀏覽代碼

Merge branch 'master' into feat/config-for-property-value-links-and-text

Gabriel Horner 3 年之前
父節點
當前提交
a8dcb3e9c5
共有 74 個文件被更改,包括 1872 次插入1230 次删除
  1. 13 1
      .github/workflows/build-android.yml
  2. 28 4
      .github/workflows/build-desktop-release.yml
  3. 1 1
      CODEBASE_OVERVIEW.md
  4. 2 2
      android/app/build.gradle
  5. 1 1
      android/app/src/main/java/com/logseq/app/FolderPicker.java
  6. 1 1
      android/app/src/main/java/com/logseq/app/FsWatcher.java
  7. 60 0
      e2e-tests/context-menu.spec.ts
  8. 335 33
      e2e-tests/editor.spec.ts
  9. 30 1
      e2e-tests/fixtures.ts
  10. 1 3
      e2e-tests/hotkey.spec.ts
  11. 39 41
      e2e-tests/logseq-url.spec.ts
  12. 3 3
      e2e-tests/page-search.spec.ts
  13. 8 1
      e2e-tests/types.ts
  14. 2 0
      e2e-tests/utils.ts
  15. 4 4
      ios/App/App.xcodeproj/project.pbxproj
  16. 1 1
      libs/package.json
  17. 1 1
      libs/src/LSPlugin.caller.ts
  18. 1 1
      libs/src/LSPlugin.core.ts
  19. 10 0
      libs/src/LSPlugin.ts
  20. 5 1
      libs/src/LSPlugin.user.ts
  21. 25 9
      resources/css/common.css
  22. 0 0
      resources/js/lsplugin.core.js
  23. 1 1
      resources/package.json
  24. 3 3
      src/electron/electron/backup_file.cljs
  25. 1 1
      src/electron/electron/utils.cljs
  26. 56 30
      src/main/frontend/components/block.cljs
  27. 208 193
      src/main/frontend/components/content.cljs
  28. 2 4
      src/main/frontend/components/content.css
  29. 20 11
      src/main/frontend/components/editor.cljs
  30. 8 7
      src/main/frontend/components/journal.cljs
  31. 1 1
      src/main/frontend/components/onboarding.cljs
  32. 5 4
      src/main/frontend/components/page.cljs
  33. 5 3
      src/main/frontend/components/plugins.cljs
  34. 1 1
      src/main/frontend/components/plugins_settings.cljs
  35. 4 2
      src/main/frontend/components/query_table.cljs
  36. 126 66
      src/main/frontend/components/reference.cljs
  37. 8 0
      src/main/frontend/components/reference.css
  38. 4 4
      src/main/frontend/components/repo.cljs
  39. 10 9
      src/main/frontend/components/settings.cljs
  40. 68 50
      src/main/frontend/components/sidebar.cljs
  41. 24 3
      src/main/frontend/config.cljs
  42. 57 91
      src/main/frontend/db/model.cljs
  43. 86 81
      src/main/frontend/db/react.cljs
  44. 2 0
      src/main/frontend/dicts.cljc
  45. 4 4
      src/main/frontend/external/roam_export.cljs
  46. 107 93
      src/main/frontend/fs/capacitor_fs.cljs
  47. 5 2
      src/main/frontend/fs/watcher_handler.cljs
  48. 36 23
      src/main/frontend/handler.cljs
  49. 22 41
      src/main/frontend/handler/block.cljs
  50. 2 14
      src/main/frontend/handler/common.cljs
  51. 32 82
      src/main/frontend/handler/editor.cljs
  52. 21 11
      src/main/frontend/handler/editor/keyboards.cljs
  53. 11 7
      src/main/frontend/handler/events.cljs
  54. 22 7
      src/main/frontend/handler/file.cljs
  55. 2 0
      src/main/frontend/handler/history.cljs
  56. 85 80
      src/main/frontend/handler/plugin.cljs
  57. 15 13
      src/main/frontend/handler/repo.cljs
  58. 2 1
      src/main/frontend/handler/ui.cljs
  59. 10 33
      src/main/frontend/mixins.cljs
  60. 3 2
      src/main/frontend/mobile/core.cljs
  61. 5 9
      src/main/frontend/modules/file/core.cljs
  62. 27 7
      src/main/frontend/modules/outliner/core.cljs
  63. 12 8
      src/main/frontend/modules/outliner/file.cljs
  64. 11 7
      src/main/frontend/modules/outliner/pipeline.cljs
  65. 31 27
      src/main/frontend/modules/outliner/tree.cljs
  66. 10 8
      src/main/frontend/modules/shortcut/data_helper.cljs
  67. 1 0
      src/main/frontend/modules/shortcut/dicts.cljc
  68. 41 15
      src/main/frontend/state.cljs
  69. 34 39
      src/main/frontend/ui.cljs
  70. 0 5
      src/main/frontend/ui.css
  71. 7 11
      src/main/frontend/util.cljc
  72. 15 0
      src/main/frontend/utils.js
  73. 1 1
      src/main/frontend/version.cljs
  74. 27 6
      src/main/logseq/api.cljs

+ 13 - 1
.github/workflows/build-android.yml

@@ -19,11 +19,19 @@ on:
         description: "Build from Git Ref(master)"
         required: true
         default: "master"
+      enable-file-sync:
+        description: 'Build with file sync support'
+        type: boolean
+        required: true
+        default: false
   workflow_call:
     inputs:
       build-target:
         type: string
         required: true
+      enable-file-sync:
+        description: 'Build with file sync support'
+        type: boolean
     secrets:
       ANDROID_KEYSTORE:
         required: true
@@ -93,11 +101,15 @@ jobs:
           sed -i 's/defonce version ".*"/defonce version "${{ steps.ref.outputs.version }}"/g' src/main/frontend/version.cljs
           sed -i 's/versionName ".*"/versionName "${{ steps.ref.outputs.version }}"/g' android/app/build.gradle
 
+      - name: Set Build Environment Variables
+        run: |
+          echo "ENABLE_FILE_SYNC=${{ inputs.enable-file-sync == 'true' || github.event.inputs.enable-file-sync == 'true' }}" >> $GITHUB_ENV
+
       - name: Compile CLJS - android variant, use es6 instead of es-next
         run: yarn install && yarn release-android-app
 
       - name: Upload Sentry Sourcemaps (beta only)
-        if: ${{ inputs.build-target == 'beta' || github.event.inputs.build-target == 'beta' }}
+        if: ${{ github.repository == 'logseq/logseq' && (inputs.build-target == 'beta' || github.event.inputs.build-target == 'beta') }}
         run: |
           curl -sL https://sentry.io/get-cli/ | bash
           release_name="logseq-android@${{ steps.ref.outputs.version }}"

+ 28 - 4
.github/workflows/build-desktop-release.yml

@@ -38,6 +38,11 @@ on:
         type: boolean
         required: true
         default: true
+      build-android:
+        description: 'Build Android App'
+        type: boolean
+        required: true
+        default: true
   schedule: # Every workday at the 2 P.M. (UTC) we run a scheduled nightly build
     - cron: '0 14 * * MON-FRI'
 
@@ -49,6 +54,12 @@ jobs:
   compile-cljs:
     runs-on: ubuntu-18.04
     steps:
+      - name: Check build options
+        if: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.build-target == 'nightly' || github.event.inputs.build-target == 'beta') && github.event.inputs.git-ref != 'master' }}
+        run: |
+          echo "::error title=CheckFail::Nightly and Beta Release MUST be built from master"
+          exit 1
+
       - name: Check out Git repository
         uses: actions/checkout@v2
         with:
@@ -96,6 +107,14 @@ jobs:
           pkgver=$(node ./scripts/get-pkg-version.js "${{ github.event.inputs.build-target }}")
           echo ::set-output name=version::$pkgver
 
+      - name: Do Not Overwrite Existing Release
+        if: ${{ github.event.inputs.build-target == 'beta' }}
+        run: |
+          if curl -f "https://api.github.com/repos/${{ github.repository }}/releases/tags/${{ steps.ref.outputs.version }}" &>/dev/null; then
+            echo "::error title=CheckFail::Release already exists"
+            exit 1
+          fi
+
       - name: Update Nightly APP Version
         if: ${{ github.event.inputs.build-target == 'nightly' || github.event_name == 'schedule' }}
         run: |
@@ -128,7 +147,7 @@ jobs:
         working-directory: ./static
 
       - name: Upload Sentry Sourcemaps (beta only)
-        if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.build-target == 'beta' }}
+        if: ${{ github.repository == 'logseq/logseq' && github.event_name == 'workflow_dispatch' && github.event.inputs.build-target == 'beta' }}
         run: |
           curl -sL https://sentry.io/get-cli/ | bash
           release_name="logseq@${{ steps.ref.outputs.version }}"
@@ -284,6 +303,7 @@ jobs:
             ${{ runner.os }}-yarn-
 
       - name: Signing By Apple Developer ID
+        if: ${{ github.repository == 'logseq/logseq' }}
         uses: apple-actions/import-codesign-certs@v1
         with:
           p12-file-base64: ${{ secrets.APPLE_CERTIFICATES_P12 }}
@@ -351,6 +371,7 @@ jobs:
             ${{ runner.os }}-arm64-yarn-
 
       - name: Signing By Apple Developer ID
+        if: ${{ github.repository == 'logseq/logseq' }}
         uses: apple-actions/import-codesign-certs@v1
         with:
           p12-file-base64: ${{ secrets.APPLE_CERTIFICATES_P12 }}
@@ -389,9 +410,11 @@ jobs:
 
   # reuse workflow via workflow_call
   build-android:
-    uses: logseq/logseq/.github/workflows/build-android.yml@master
+    uses: ./.github/workflows/build-android.yml
+    if: ${{ github.event_name == 'schedule' || github.event.inputs.build-android == 'true' }}
     with:
       build-target: "${{ github.event.inputs.build-target }}"
+      enable-file-sync: "${{ github.event.inputs.enable-file-sync == 'true' }}"
     secrets:
       ANDROID_KEYSTORE: "${{ secrets.ANDROID_KEYSTORE }}"
       ANDROID_KEYSTORE_PASSWORD: "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}"
@@ -466,8 +489,8 @@ jobs:
   release:
     # NOTE: For now, we only have beta channel to be released on Github
     if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.build-target == 'beta' }}
-    needs: [ build-macos-x64, build-macos-arm64, build-linux, build-windows, build-android ]
-    runs-on: ubuntu-18.04
+    needs: [ build-macos-x64, build-macos-arm64, build-linux, build-windows ]
+    runs-on: ubuntu-latest
     steps:
       - name: Download MacOS x64 Artifacts
         uses: actions/download-artifact@v2
@@ -495,6 +518,7 @@ jobs:
 
       - name: Download Android Artifacts
         uses: actions/download-artifact@v2
+        if: ${{ github.event_name == 'schedule' || github.event.inputs.build-android == 'true' }}
         with:
           name: logseq-android-builds
           path: ./

+ 1 - 1
CODEBASE_OVERVIEW.md

@@ -24,7 +24,7 @@ For other tasks like bundling static resources and building the desktop app, whi
 
 [React](https://reactjs.org/) is a library for building data-driven UI declaratively. Comparing to the imperative ways (such as DOM manipulation or using jQuery), it's simpler and easier to code correctly.
 
-[Rum](https://github.com/tonsky/rum) is a React wrapper in ClojureScript. More than just providing the familiar React APIs, Rum adds many Clojure flavors to React, especially on the state management part. As a result, if you have experience with React, read Rum's [README]((https://github.com/tonsky/rum) before diving into the code.
+[Rum](https://github.com/tonsky/rum) is a React wrapper in ClojureScript. More than just providing the familiar React APIs, Rum adds many Clojure flavors to React, especially on the state management part. As a result, if you have experience with React, read Rum's [README](https://github.com/tonsky/rum) before diving into the code.
 
 ### DataScript
 

+ 2 - 2
android/app/build.gradle

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

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

@@ -63,7 +63,7 @@ public class FolderPicker extends Plugin {
         if (path == null || path.isEmpty()) {
             call.reject("Cannot support this directory type: " + docUri);
         } else {
-            ret.put("path", path);
+            ret.put("path", "file://" + path);
             call.resolve(ret);
         }
     }

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

@@ -133,7 +133,7 @@ public class FsWatcher extends Plugin {
         // path.
         File f = new File(path);
         obj.put("path", Uri.fromFile(f));
-        obj.put("dir", mPath);
+        obj.put("dir", "file://" + mPath);
 
         switch (event) {
             case FileObserver.CLOSE_WRITE:

+ 60 - 0
e2e-tests/context-menu.spec.ts

@@ -0,0 +1,60 @@
+import { expect } from '@playwright/test'
+import { test } from './fixtures'
+import { createRandomPage } from './utils'
+
+test('open context menu', async ({ page }) => {
+    await createRandomPage(page)
+
+    await page.locator('span.bullet-container >> nth=0').click({button: "right"})
+
+    await expect(page.locator('#custom-context-menu')).toBeVisible()
+})
+
+test('close context menu on esc', async ({ page }) => {
+    await createRandomPage(page)
+
+    await page.locator('span.bullet-container >> nth=0').click({button: "right"})
+
+    await page.keyboard.press('Escape')
+
+    await expect(page.locator('#custom-context-menu')).toHaveCount(0)
+})
+
+test('close context menu by left clicking on empty space', async ({ page }) => {
+    await createRandomPage(page)
+
+    await page.locator('span.bullet-container >> nth=0').click({button: "right"})
+
+    await page.mouse.click(0, 200, {button: "left"})
+
+    await expect(page.locator('#custom-context-menu')).toHaveCount(0)
+})
+
+test('close context menu by clicking on a menu item', async ({ page }) => {
+    await createRandomPage(page)
+
+    await page.locator('span.bullet-container >> nth=0').click({button: "right"})
+
+    await page.locator('#custom-context-menu .menu-link >> nth=1').click()
+
+    await expect(page.locator('#custom-context-menu')).toHaveCount(0)
+})
+
+test('close context menu by clicking on a block', async ({ page, block }) => {
+    await createRandomPage(page)
+
+    await block.mustType('fist Block')
+    await block.enterNext()
+
+    await page.locator('span.bullet-container >> nth=-1').click({button: "right"})
+
+    const elementHandle = page.locator('.block-content >> nth=0');
+
+    const box = await elementHandle.boundingBox();
+    expect(box).toBeTruthy()
+    if (box) {
+        await page.mouse.click(box.x + box.width - 5, box.y + box.height / 2);
+    }
+
+    await expect(page.locator('#custom-context-menu')).toHaveCount(0)
+})

+ 335 - 33
e2e-tests/editor.spec.ts

@@ -143,44 +143,44 @@ test(
   })
 
 test('copy & paste block ref and replace its content', async ({ page, block }) => {
-    await createRandomPage(page)
+  await createRandomPage(page)
 
-    await block.mustFill('Some random text')
-    // FIXME: copy instantly will make content disappear
-    await page.waitForTimeout(1000)
-    if (IsMac) {
-        await page.keyboard.press('Meta+c')
-    } else {
-        await page.keyboard.press('Control+c')
-    }
+  await block.mustFill('Some random text')
+  // FIXME: copy instantly will make content disappear
+  await page.waitForTimeout(1000)
+  if (IsMac) {
+    await page.keyboard.press('Meta+c')
+  } else {
+    await page.keyboard.press('Control+c')
+  }
 
-    await page.press('textarea >> nth=0', 'Enter')
-    if (IsMac) {
-        await page.keyboard.press('Meta+v')
-    } else {
-        await page.keyboard.press('Control+v')
-    }
-    await page.keyboard.press('Enter')
+  await page.press('textarea >> nth=0', 'Enter')
+  if (IsMac) {
+    await page.keyboard.press('Meta+v')
+  } else {
+    await page.keyboard.press('Control+v')
+  }
+  await page.keyboard.press('Enter')
 
-    const blockRef = page.locator('.block-ref >> text="Some random text"');
+  const blockRef = page.locator('.block-ref >> text="Some random text"');
 
-    // Check if the newly created block-ref has the same referenced content
-    await expect(blockRef).toHaveCount(1);
+  // Check if the newly created block-ref has the same referenced content
+  await expect(blockRef).toHaveCount(1);
 
-    // Move cursor into the block ref
-    for (let i = 0; i < 4; i++) {
-        await page.press('textarea >> nth=0', 'ArrowLeft')
-}
+  // Move cursor into the block ref
+  for (let i = 0; i < 4; i++) {
+    await page.press('textarea >> nth=0', 'ArrowLeft')
+  }
 
-    // Trigger replace-block-reference-with-content-at-point
-    if (IsMac) {
-        await page.keyboard.press('Meta+Shift+r')
-    } else {
-        await page.keyboard.press('Control+Shift+v')
-    }
+  // Trigger replace-block-reference-with-content-at-point
+  if (IsMac) {
+    await page.keyboard.press('Meta+Shift+r')
+  } else {
+    await page.keyboard.press('Control+Shift+v')
+  }
 })
 
-test('copy and paste block after editing new block', async ({ page, block }) => {
+test('copy and paste block after editing new block #5962', async ({ page, block }) => {
   await createRandomPage(page)
 
   // Create a block and copy it in block-select mode
@@ -197,17 +197,319 @@ test('copy and paste block after editing new block', async ({ page, block }) =>
   await page.keyboard.press('Enter')
   await page.waitForTimeout(100)
   await page.keyboard.press('Enter')
-  
+
   await page.waitForTimeout(100)
   // Create a new block with some text
   await page.keyboard.insertText("Typed block")
 
   // Quickly paste the copied block
   if (IsMac) {
-      await page.keyboard.press('Meta+v')
+    await page.keyboard.press('Meta+v')
   } else {
-      await page.keyboard.press('Control+v')
+    await page.keyboard.press('Control+v')
   }
 
   await expect(page.locator('text="Typed block"')).toHaveCount(1);
 })
+
+test('undo and redo after starting an action should not destroy text #6267', async ({ page, block }) => {
+  await createRandomPage(page)
+
+  // Get one piece of undo state onto the stack
+  await block.mustType('text1 ')
+  await page.waitForTimeout(500) // Wait for 500ms autosave period to expire
+
+  // Then type more, start an action prompt, and undo
+  await page.keyboard.type('text2 ', { delay: 50 })
+  for (const char of '[[') {
+    await page.keyboard.type(char, { delay: 50 })
+  }
+  await expect(page.locator(`[data-modal-name="page-search"]`)).toBeVisible()
+  if (IsMac) {
+    await page.keyboard.press('Meta+z')
+  } else {
+    await page.keyboard.press('Control+z')
+  }
+  await page.waitForTimeout(100)
+
+  // Should close the action menu when we undo the action prompt
+  await expect(page.locator(`[data-modal-name="page-search"]`)).not.toBeVisible()
+
+  // It should undo to the last saved state, and not erase the previous undo action too
+  await expect(page.locator('text="text1"')).toHaveCount(1)
+
+  // And it should keep what was undone as a redo action
+  if (IsMac) {
+    await page.keyboard.press('Meta+Shift+z')
+  } else {
+    await page.keyboard.press('Control+Shift+z')
+  }
+  await expect(page.locator('text="text2"')).toHaveCount(1)
+})
+
+test('undo after starting an action should close the action menu #6269', async ({ page, block }) => {
+  for (const [commandTrigger, modalName] of [['/', 'commands'], ['[[', 'page-search']]) {
+    await createRandomPage(page)
+
+    // Open the action modal
+    await block.mustType('text1 ')
+    await page.waitForTimeout(550)
+    for (const char of commandTrigger) {
+      await page.keyboard.type(char)
+    }
+    await expect(page.locator(`[data-modal-name="${modalName}"]`)).toBeVisible()
+
+    // Undo, removing "/today", and closing the action modal
+    if (IsMac) {
+      await page.keyboard.press('Meta+z')
+    } else {
+      await page.keyboard.press('Control+z')
+    }
+    await page.waitForTimeout(100)
+    await expect(page.locator('text="/today"')).toHaveCount(0)
+    await expect(page.locator(`[data-modal-name="${modalName}"]`)).not.toBeVisible()
+  }
+})
+
+test('#6266 moving cursor outside of brackets should close autocomplete menu', async ({ page, block, autocompleteMenu }) => {
+  for (const [commandTrigger, modalName] of [['[[', 'page-search'], ['((', 'block-search']]) {
+    // First, left arrow
+    await createRandomPage(page)
+
+    await block.mustFill('')
+    for (const char of commandTrigger) {
+      await page.keyboard.type(char)
+      await page.waitForTimeout(10) // Sometimes it doesn't trigger without this
+    }
+    await autocompleteMenu.expectVisible(modalName)
+
+    await page.keyboard.press('ArrowLeft')
+    await page.waitForTimeout(100)
+    await autocompleteMenu.expectHidden(modalName)
+
+    // Then, right arrow
+    await createRandomPage(page)
+
+    await block.mustFill('')
+    for (const char of commandTrigger) {
+      await page.keyboard.type(char)
+      await page.waitForTimeout(10) // Sometimes it doesn't trigger without this
+    }
+    await page.waitForTimeout(100)
+    await autocompleteMenu.expectVisible(modalName)
+
+    await page.waitForTimeout(100)
+    // Move cursor outside of the space strictly between the double brackets
+    await page.keyboard.press('ArrowRight')
+    await page.waitForTimeout(100)
+    await autocompleteMenu.expectHidden(modalName)
+  }
+})
+
+// Old logic would fail this because it didn't do the check if @search-timeout was set
+test('#6266 moving cursor outside of parens immediately after searching should still close autocomplete menu', async ({ page, block, autocompleteMenu }) => {
+  for (const [commandTrigger, modalName] of [['((', 'block-search']]) {
+    await createRandomPage(page)
+
+    // Open the autocomplete menu
+    // TODO: Maybe remove these "text " entries in tests that don't need them
+    await block.mustFill('')
+    await page.waitForTimeout(550)
+    for (const char of commandTrigger) {
+      await page.keyboard.type(char)
+      await page.waitForTimeout(10) // Sometimes it doesn't trigger without this
+    }
+    await page.waitForTimeout(100)
+    await page.keyboard.type("some block search text")
+    await autocompleteMenu.expectVisible(modalName)
+
+    // Move cursor outside of the space strictly between the double parens
+    await page.keyboard.press('ArrowRight')
+    await page.waitForTimeout(100)
+    await autocompleteMenu.expectHidden(modalName)
+  }
+})
+
+test('pressing up and down should NOT close autocomplete menu', async ({ page, block, autocompleteMenu }) => {
+  for (const [commandTrigger, modalName] of [['[[', 'page-search'], ['((', 'block-search']]) {
+    await createRandomPage(page)
+
+    // Open the autocomplete menu
+    await block.mustFill('')
+    for (const char of commandTrigger) {
+      await page.keyboard.type(char)
+      await page.waitForTimeout(10) // Sometimes it doesn't trigger without this
+    }
+    await autocompleteMenu.expectVisible(modalName)
+    const cursorPos = await block.selectionStart()
+
+    await page.keyboard.press('ArrowUp')
+    await page.waitForTimeout(100)
+    await autocompleteMenu.expectVisible(modalName)
+    await expect(await block.selectionStart()).toEqual(cursorPos)
+
+    await page.keyboard.press('ArrowDown')
+    await page.waitForTimeout(100)
+    await autocompleteMenu.expectVisible(modalName)
+    await expect(await block.selectionStart()).toEqual(cursorPos)
+  }
+})
+
+test('moving cursor inside of brackets should NOT close autocomplete menu', async ({ page, block, autocompleteMenu }) => {
+  for (const [commandTrigger, modalName] of [['[[', 'page-search'], ['((', 'block-search']]) {
+    await createRandomPage(page)
+
+    // Open the autocomplete menu
+    await block.mustFill('')
+    for (const char of commandTrigger) {
+      await page.keyboard.type(char)
+      await page.waitForTimeout(10) // Sometimes it doesn't trigger without this
+    }
+    await page.waitForTimeout(100)
+    if (commandTrigger === '[[') {
+      await autocompleteMenu.expectVisible(modalName)
+    }
+
+    await page.keyboard.type("search")
+    await page.waitForTimeout(100)
+    await autocompleteMenu.expectVisible(modalName)
+
+    // Move cursor, still inside the brackets
+    await page.keyboard.press('ArrowLeft')
+    await page.waitForTimeout(100)
+    await autocompleteMenu.expectVisible(modalName)
+  }
+})
+
+test('moving cursor inside of brackets when autocomplete menu is closed should NOT open autocomplete menu', async ({ page, block, autocompleteMenu }) => {
+  // Note: (( behaves differently and doesn't auto-trigger when typing in it after exiting the search prompt once
+  for (const [commandTrigger, modalName] of [['[[', 'page-search']]) {
+    await createRandomPage(page)
+
+    // Open the autocomplete menu
+    await block.mustFill('')
+    for (const char of commandTrigger) {
+      await page.keyboard.type(char)
+      await page.waitForTimeout(10) // Sometimes it doesn't trigger without this
+    }
+    await autocompleteMenu.expectVisible(modalName)
+
+    await block.escapeEditing()
+    await autocompleteMenu.expectHidden(modalName)
+
+    // Move cursor left until it's inside the brackets; shouldn't open autocomplete menu
+    await page.locator('.block-content').click()
+    await page.waitForTimeout(100)
+    await autocompleteMenu.expectHidden(modalName)
+
+    await page.keyboard.press('ArrowLeft')
+    await page.waitForTimeout(100)
+    await autocompleteMenu.expectHidden(modalName)
+
+    await page.keyboard.press('ArrowLeft')
+    await page.waitForTimeout(100)
+    await autocompleteMenu.expectHidden(modalName)
+
+    // Type a letter, this should open the autocomplete menu
+    await page.keyboard.type('z')
+    await page.waitForTimeout(100)
+    await autocompleteMenu.expectVisible(modalName)
+  }
+})
+
+test('selecting text inside of brackets should NOT close autocomplete menu', async ({ page, block, autocompleteMenu }) => {
+  for (const [commandTrigger, modalName] of [['[[', 'page-search'], ['((', 'block-search']]) {
+    await createRandomPage(page)
+
+    // Open the autocomplete menu
+    await block.mustFill('')
+    for (const char of commandTrigger) {
+      await page.keyboard.type(char)
+      await page.waitForTimeout(10) // Sometimes it doesn't trigger without this
+    }
+    await page.waitForTimeout(100)
+    await autocompleteMenu.expectVisible(modalName)
+
+    await page.keyboard.type("some page search text")
+    await page.waitForTimeout(100)
+    await autocompleteMenu.expectVisible(modalName)
+
+    // Select some text within the brackets
+    await page.keyboard.press('Shift+ArrowLeft')
+    await page.waitForTimeout(100)
+    await autocompleteMenu.expectVisible(modalName)
+  }
+})
+
+test('pressing backspace and remaining inside of brackets should NOT close autocomplete menu', async ({ page, block, autocompleteMenu }) => {
+  for (const [commandTrigger, modalName] of [['[[', 'page-search'], ['((', 'block-search']]) {
+    await createRandomPage(page)
+
+    // Open the autocomplete menu
+    await block.mustFill('')
+    for (const char of commandTrigger) {
+      await page.keyboard.type(char)
+      await page.waitForTimeout(10) // Sometimes it doesn't trigger without this
+    }
+    await page.waitForTimeout(100)
+    await autocompleteMenu.expectVisible(modalName)
+
+    await page.keyboard.type("some page search text")
+    await page.waitForTimeout(100)
+    await autocompleteMenu.expectVisible(modalName)
+
+    // Delete one character inside the brackets
+    await page.keyboard.press('Backspace')
+    await page.waitForTimeout(100)
+    await autocompleteMenu.expectVisible(modalName)
+  }
+})
+test('press escape when autocomplete menu is open, should close autocomplete menu only #6270', async ({ page, block }) => {
+  for (const [commandTrigger, modalName] of [['[[', 'page-search'], ['/', 'commands']]) {
+    await createRandomPage(page)
+
+    // Open the action modal
+    await block.mustFill('text ')
+    await page.waitForTimeout(550)
+    for (const char of commandTrigger) {
+      await page.keyboard.type(char) // Type it one character at a time, because too quickly can fail to trigger it sometimes
+    }
+    await page.waitForTimeout(100)
+    await expect(page.locator(`[data-modal-name="${modalName}"]`)).toBeVisible()
+    await page.waitForTimeout(100)
+
+    // Press escape; should close action modal instead of exiting edit mode
+    await page.keyboard.press('Escape')
+    await page.waitForTimeout(100)
+    await expect(page.locator(`[data-modal-name="${modalName}"]`)).not.toBeVisible()
+    await page.waitForTimeout(1000)
+    expect(await block.isEditing()).toBe(true)
+  }
+})
+
+test('press escape when link/image dialog is open, should restore focus to input', async ({ page, block }) => {
+  for (const [commandTrigger, modalName] of [['/link', 'commands']]) {
+    await createRandomPage(page)
+
+    // Open the action modal
+    await block.mustFill('')
+    await page.waitForTimeout(550)
+    for (const char of commandTrigger) {
+      await page.keyboard.type(char) // Type it one character at a time, because too quickly can fail to trigger it sometimes
+    }
+    await page.waitForTimeout(100)
+    await expect(page.locator(`[data-modal-name="${modalName}"]`)).toBeVisible()
+    await page.waitForTimeout(100)
+
+    // Press enter to open the link dialog
+    await page.keyboard.press('Enter')
+    await expect(page.locator(`[data-modal-name="input"]`)).toBeVisible()
+
+    // Press escape; should close link dialog and restore focus to the block textarea
+    await page.keyboard.press('Escape')
+    await page.waitForTimeout(100)
+    await expect(page.locator(`[data-modal-name="input"]`)).not.toBeVisible()
+    await page.waitForTimeout(1000)
+    expect(await block.isEditing()).toBe(true)
+  }
+})

+ 30 - 1
e2e-tests/fixtures.ts

@@ -3,7 +3,7 @@ import * as path from 'path'
 import { test as base, expect, ConsoleMessage, Locator } from '@playwright/test';
 import { ElectronApplication, Page, BrowserContext, _electron as electron } from 'playwright'
 import { loadLocalGraph, openLeftSidebar, randomString } from './utils';
-import { LogseqFixtures } from './types';
+import { autocompleteMenu, LogseqFixtures } from './types';
 
 let electronApp: ElectronApplication
 let context: BrowserContext
@@ -106,6 +106,11 @@ base.beforeEach(async () => {
   if (page) {
     await page.keyboard.press('Escape')
     await page.keyboard.press('Escape')
+
+    const rightSidebar = page.locator('.cp__right-sidebar-inner')
+    if (await rightSidebar.isVisible()) {
+      await page.click('button.toggle-right-sidebar', {delay: 100})
+    }
   }
 })
 
@@ -217,6 +222,30 @@ export const test = base.extend<LogseqFixtures>({
     use(block)
   },
 
+  autocompleteMenu: async ({ }, use) => {
+    const autocompleteMenu: autocompleteMenu = {
+      expectVisible: async (modalName?: string) => {
+        const modal = page.locator(modalName ? `[data-modal-name="${modalName}"]` : `[data-modal-name]`)
+        if (await modal.isVisible()) {
+          await page.waitForTimeout(100)
+          await expect(modal).toBeVisible()
+        } else {
+          await modal.waitFor({ state: 'visible', timeout: 1000 })
+        }
+      },
+      expectHidden: async (modalName?: string) => {
+        const modal = page.locator(modalName ? `[data-modal-name="${modalName}"]` : `[data-modal-name]`)
+        if (!await modal.isVisible()) {
+          await page.waitForTimeout(100)
+          await expect(modal).not.toBeVisible()
+        } else {
+          await modal.waitFor({ state: 'hidden', timeout: 1000 })
+        }
+      }
+    }
+    await use(autocompleteMenu)
+  },
+
   context: async ({ }, use) => {
     await use(context);
   },

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

@@ -5,10 +5,8 @@ import { createRandomPage, newBlock, lastBlock, IsMac, IsLinux } from './utils'
 test('open search dialog', async ({ page }) => {
   if (IsMac) {
     await page.keyboard.press('Meta+k')
-  } else if (IsLinux) {
-    await page.keyboard.press('Control+k')
   } else {
-    expect(false, "TODO: test on Windows and other platforms").toBeTruthy()
+    await page.keyboard.press('Control+k')
   }
 
   await page.waitForSelector('[placeholder="Search or create page"]')

+ 39 - 41
e2e-tests/logseq-url.spec.ts

@@ -2,46 +2,44 @@ import { expect } from '@playwright/test'
 import { test } from './fixtures'
 import { createRandomPage, lastBlock, IsMac, IsLinux } from './utils'
 
-test(
-  "Logseq URLs (same graph)",
-  async ({ page, block }) => {
-    let paste_key = IsMac ? 'Meta+v' : 'Control+v'
-    // create a page with identify block
-    let identify_text = "URL redirect target"
-    let page_title = await createRandomPage(page)
-    await block.mustFill(identify_text)
+test("Logseq URLs (same graph)", async ({ page, block }) => {
+  let paste_key = IsMac ? 'Meta+v' : 'Control+v'
+  // create a page with identify block
+  let identify_text = "URL redirect target"
+  let page_title = await createRandomPage(page)
+  await block.mustFill(identify_text)
 
-    // paste current page's URL to another page, then redirect throught the URL
-    await page.click('.ui__dropdown-trigger')
-    await page.locator("text=Copy page URL").click()
-    await createRandomPage(page)
-    await block.mustFill("") // to enter editing mode
-    await page.keyboard.press(paste_key)
-    let cursor_locator = page.locator('textarea >> nth=0')
-    expect(await cursor_locator.inputValue()).toContain("page=" + page_title)
-    await cursor_locator.press("Enter")
-    if (!IsLinux) { // FIXME: support Logseq URL on Linux (XDG)
-        page.locator('a.external-link >> nth=0').click()
-        await page.waitForNavigation()
-        await page.waitForTimeout(500)
-        cursor_locator = await lastBlock(page)
-        expect(await cursor_locator.inputValue()).toBe(identify_text)
-    }
+  // paste current page's URL to another page, then redirect throught the URL
+  await page.click('.ui__dropdown-trigger')
+  await page.locator("text=Copy page URL").click()
+  await createRandomPage(page)
+  await block.mustFill("") // to enter editing mode
+  await page.keyboard.press(paste_key)
+  let cursor_locator = page.locator('textarea >> nth=0')
+  expect(await cursor_locator.inputValue()).toContain("page=" + page_title)
+  await cursor_locator.press("Enter")
+  if (IsMac) { // FIXME: support Logseq URL on Linux (XDG)
+    page.locator('a.external-link >> nth=0').click()
+    await page.waitForNavigation()
+    await page.waitForTimeout(500)
+    cursor_locator = await lastBlock(page)
+    expect(await cursor_locator.inputValue()).toBe(identify_text)
+  }
 
-    // paste the identify block's URL to another page, then redirect throught the URL
-    await page.click('span.bullet >> nth=0', { button: "right" })
-    await page.locator("text=Copy block URL").click()
-    await createRandomPage(page)
-    await block.mustFill("") // to enter editing mode
-    await page.keyboard.press(paste_key)
-    cursor_locator = page.locator('textarea >> nth=0')
-    expect(await cursor_locator.inputValue()).toContain("block-id=")
-    await cursor_locator.press("Enter")
-    if (!IsLinux) { // FIXME: support Logseq URL on Linux (XDG)
-        page.locator('a.external-link >> nth=0').click()
-        await page.waitForNavigation()
-        await page.waitForTimeout(500)
-        cursor_locator = await lastBlock(page)
-        expect(await cursor_locator.inputValue()).toBe(identify_text)
-    }
-})
+  // paste the identify block's URL to another page, then redirect throught the URL
+  await page.click('span.bullet >> nth=0', { button: "right" })
+  await page.locator("text=Copy block URL").click()
+  await createRandomPage(page)
+  await block.mustFill("") // to enter editing mode
+  await page.keyboard.press(paste_key)
+  cursor_locator = page.locator('textarea >> nth=0')
+  expect(await cursor_locator.inputValue()).toContain("block-id=")
+  await cursor_locator.press("Enter")
+  if (IsMac) { // FIXME: support Logseq URL on Linux (XDG)
+    page.locator('a.external-link >> nth=0').click()
+    await page.waitForNavigation()
+    await page.waitForTimeout(500)
+    cursor_locator = await lastBlock(page)
+    expect(await cursor_locator.inputValue()).toBe(identify_text)
+  }
+})

+ 3 - 3
e2e-tests/page-search.spec.ts

@@ -38,7 +38,7 @@ import { IsMac, createRandomPage, newBlock, newInnerBlock, randomString, lastBlo
   await page.fill('[placeholder="Search or create page"]', 'Einführung in die Allgemeine Sprachwissenschaft' + rand)
 
   await page.waitForTimeout(500)
-  const results = await page.$$('#ui__ac-inner .block')
+  const results = await page.$$('#ui__ac-inner>div')
   expect(results.length).toEqual(3) // 2 blocks + 1 page
   await page.keyboard.press("Escape")
 })
@@ -68,7 +68,7 @@ async function alias_test(page: Page, page_name: string, search_kws: string[]) {
   await page.waitForTimeout(500)
 
   // build target Page with alias
-  // the target page will contains the content in 
+  // the target page will contains the content in
   //   alias_test_content_1,
   //   alias_test_content_2, and
   //   alias_test_content_3 sequentialy, to validate the target page state
@@ -127,7 +127,7 @@ async function alias_test(page: Page, page_name: string, search_kws: string[]) {
     await page.fill('[placeholder="Search or create page"]', kw_name)
     await page.waitForTimeout(500)
 
-    const results = await page.$$('#ui__ac-inner .block')
+    const results = await page.$$('#ui__ac-inner>div')
     expect(results.length).toEqual(3) // page + block + alias property
 
     // test search results

+ 8 - 1
e2e-tests/types.ts

@@ -38,11 +38,18 @@ export interface Block {
   selectionEnd(): Promise<number>;
 }
 
+export interface autocompleteMenu {
+  // Expect or wait for autocomplete menu to be or become visible
+  expectVisible(modalName?: string): Promise<void>
+  // Expect or wait for autocomplete menu to be or become hidden
+  expectHidden(modalName?: string): Promise<void>
+}
+
 export interface LogseqFixtures {
   page: Page;
   block: Block;
+  autocompleteMenu: autocompleteMenu;
   context: BrowserContext;
   app: ElectronApplication;
   graphDir: string;
 }
-

+ 2 - 0
e2e-tests/utils.ts

@@ -42,6 +42,8 @@ export async function createRandomPage(page: Page) {
   await page.fill('[placeholder="Search or create page"]', randomTitle)
   // Click text=/.*New page: "new page".*/
   await page.click('text=/.*New page: ".*/')
+  // Wait for h1 to be from our new page
+  await page.waitForSelector(`h1 >> text="${randomTitle}"`, { state: 'visible' })
   // wait for textarea of first block
   await page.waitForSelector('textarea >> nth=0', { state: 'visible' })
 

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

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

+ 1 - 1
libs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@logseq/libs",
-  "version": "0.0.7",
+  "version": "0.0.8",
   "description": "Logseq SDK libraries",
   "main": "dist/lsplugin.user.js",
   "typings": "index.d.ts",

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

@@ -266,7 +266,7 @@ class LSPluginCaller extends EventEmitter {
     return new Promise((resolve, reject) => {
       timer = setTimeout(() => {
         reject(new Error(`handshake Timeout`))
-      }, 3 * 1000) // 3secs
+      }, 8 * 1000) // 8 secs
 
       handshake
         .then((refChild: ParentAPI) => {

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

@@ -907,7 +907,7 @@ class PluginLocal extends EventEmitter<'loaded'
 
       this._dispose(cleanInjectedScripts.bind(this))
     } catch (e) {
-      debug('[Load Plugin Error] ', e)
+      console.error('[Load Plugin Error] ', e)
       this.logger?.error(e)
 
       this._status = PluginLocalLoadStatus.ERROR

+ 10 - 0
libs/src/LSPlugin.ts

@@ -140,6 +140,8 @@ export interface AppUserConfigs {
 
   currentGraph: string
   showBracket: boolean
+  enabledFlashcards: boolean
+  enabledJournals: boolean
 
   [key: string]: any
 }
@@ -410,6 +412,7 @@ export interface IAppProxy {
 
   // hook events
   onCurrentGraphChanged: IUserHook
+  onGraphAfterIndexed: IUserHook<{repo: string}>
   onThemeModeChanged: IUserHook<{ mode: 'dark' | 'light' }>
   onThemeChanged: IUserHook<Partial<{name: string, mode: string, pid: string, url: string}>>
   onBlockRendererSlotted: IUserSlotHook<{ uuid: BlockUUID }>
@@ -551,6 +554,12 @@ export interface IEditorProxy extends Record<string, any> {
     namespace: BlockPageName
   ) => Promise<Array<PageEntity> | null>
 
+  /**
+   * Create a unique UUID string which can then be assigned to a block.
+   * @added 0.0.8
+   */
+  newBlockUUID: () => Promise<string>
+
   /**
    * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-reddit-hot-news
    *
@@ -565,6 +574,7 @@ export interface IEditorProxy extends Record<string, any> {
       before: boolean
       sibling: boolean
       isPageBlock: boolean
+      customUUID: string
       properties: {}
     }>
   ) => Promise<BlockEntity | null>

+ 5 - 1
libs/src/LSPlugin.user.ts

@@ -189,6 +189,10 @@ const app: Partial<IAppProxy> = {
 let registeredCmdUid = 0
 
 const editor: Partial<IEditorProxy> = {
+  newBlockUUID(this: LSPluginUser): Promise<string> {
+    return this._execCallableAPIAsync('new_block_uuid')
+  },
+
   registerSlashCommand(
     this: LSPluginUser,
     tag: string,
@@ -273,7 +277,7 @@ const editor: Partial<IEditorProxy> = {
     } else {
       this.App.pushState('page', { name: pageName }, { anchor })
     }
-  },
+  }
 }
 
 const db: Partial<IDBProxy> = {

+ 25 - 9
resources/css/common.css

@@ -842,8 +842,14 @@ i.ti {
 
 .heading-bg {
   border-radius: 50%;
-  width: 12px;
-  height: 12px;
+  width: 14px;
+  height: 14px;
+
+  &.remove {
+    @apply border flex items-center justify-center;
+
+    border-color: var(--border-color);
+  }
 }
 
 /** endregion **/
@@ -900,20 +906,30 @@ button.menu:focus {
   background-color: var(--ls-menu-hover-color, #f4f5f7);
 }
 
+.menu-links-wrapper {
+  @apply py-2 rounded-md shadow-lg overflow-y-auto;
+
+  max-height: calc(100vh - 100px) !important;
+  background-color: var(--ls-primary-background-color, #fff);
+  min-width: 12rem;
+}
+
+.menu-backdrop {
+  @apply w-full h-full fixed top-0 left-0;
+
+  z-index: var(--ls-z-index-level-1);
+}
+
 .menu-link {
   background-color: var(--ls-primary-background-color, #fff);
   color: var(--ls-primary-text-color);
   user-select: none;
 }
 
-.menu-link:first-of-type {
-  border-top-left-radius: var(--ls-border-radius-low);
-  border-top-right-radius: var(--ls-border-radius-low);
-}
+.menu-separator {
+  @apply my-1;
 
-.menu-link:last-of-type {
-  border-bottom-left-radius: var(--ls-border-radius-low);
-  border-bottom-right-radius: var(--ls-border-radius-low);
+  opacity: .5;
 }
 
 a.login {

文件差異過大導致無法顯示
+ 0 - 0
resources/js/lsplugin.core.js


+ 1 - 1
resources/package.json

@@ -1,6 +1,6 @@
 {
   "name": "Logseq",
-  "version": "0.8.0",
+  "version": "0.8.1",
   "main": "electron.js",
   "author": "Logseq",
   "license": "AGPL-3.0",

+ 3 - 3
src/electron/electron/backup_file.cljs

@@ -25,11 +25,11 @@
   (get-backup-dir* repo relative-path version-file-dir))
 
 (defn- truncate-old-versioned-files!
-  "reserve the latest 3 version files"
+  "reserve the latest 6 version files"
   [dir]
   (let [files (fs/readdirSync dir (clj->js {:withFileTypes true}))
         files (mapv #(.-name %) files)
-        old-versioned-files (drop 3 (reverse (sort files)))]
+        old-versioned-files (drop 6 (reverse (sort files)))]
     (doseq [file old-versioned-files]
       (fs-extra/removeSync (path/join dir file)))))
 
@@ -44,7 +44,7 @@
                :version-file-dir (get-version-file-dir repo relative-path))
         new-path (path/join dir*
                             (str (string/replace (.toISOString (js/Date.)) ":" "_")
-                                 ext))]
+                                 ".Desktop" ext))]
     (fs-extra/ensureDirSync dir*)
     (fs/writeFileSync new-path content)
     (fs/statSync new-path)

+ 1 - 1
src/electron/electron/utils.cljs

@@ -35,7 +35,7 @@
   []
   (let [lg-dir (str (.getPath app "home") "/.logseq")]
     (if-not (fs/existsSync lg-dir)
-      (and (fs/mkdirSync lg-dir) lg-dir)
+      (do (fs/mkdirSync lg-dir) lg-dir)
       lg-dir)))
 
 (defn get-ls-default-plugins

+ 56 - 30
src/main/frontend/components/block.cljs

@@ -2098,14 +2098,18 @@
                  (not (:slide? config)))
         (properties-cp config block))
 
-      (when (and (not block-ref-with-title?) (seq body))
-        [:div.block-body {:style {:display (if (and collapsed? (seq title)) "none" "")}}
-         ;; TODO: consistent id instead of the idx (since it could be changed later)
-         (let [body (block/trim-break-lines! (:block/body block))]
-           (for [[idx child] (medley/indexed body)]
-             (when-let [block (markup-element-cp config child)]
-               (rum/with-key (block-child block)
-                 (str uuid "-" idx)))))])
+      (let [title-collapse-enabled? (:outliner/block-title-collapse-enabled? (state/get-config))]
+        (when (and (not block-ref-with-title?)
+                   (seq body)
+                   (or (not title-collapse-enabled?)
+                       (and title-collapse-enabled? (not collapsed?))))
+         [:div.block-body
+          ;; TODO: consistent id instead of the idx (since it could be changed later)
+          (let [body (block/trim-break-lines! (:block/body block))]
+            (for [[idx child] (medley/indexed body)]
+              (when-let [block (markup-element-cp config child)]
+                (rum/with-key (block-child block)
+                  (str uuid "-" idx)))))]))
 
       (case (:block/warning block)
         :multiple-blocks
@@ -2147,9 +2151,10 @@
       [:div.more (ui/icon "dots-circle-horizontal" {:style {:fontSize 16}})])]])
 
 (rum/defcs block-content-or-editor < rum/reactive
-  (rum/local true :hide-block-refs?)
+  (rum/local true ::hide-block-refs?)
   [state config {:block/keys [uuid format] :as block} edit-input-id block-id heading-level edit?]
-  (let [*hide-block-refs? (get state :hide-block-refs?)
+  (let [*hide-block-refs? (get state ::hide-block-refs?)
+        hide-block-refs? @*hide-block-refs?
         editor-box (get config :editor-box)
         editor-id (str "editor-" edit-input-id)
         slide? (:slide? config)
@@ -2205,7 +2210,7 @@
 
            (block-refs-count block *hide-block-refs?)]]
 
-         (when (and (not @*hide-block-refs?) (> refs-count 0))
+         (when (and (not hide-block-refs?) (> refs-count 0))
            (let [refs-cp (state/get-component :block/linked-references)]
              (refs-cp uuid)))]))))
 
@@ -2360,7 +2365,6 @@
     (when (and
            (state/in-selection-mode?)
            (non-dragging? e))
-      (util/stop e)
       (editor-handler/highlight-selection-area! block-id))))
 
 (defn- block-mouse-leave
@@ -2419,7 +2423,7 @@
         *navigating-block (get state ::navigating-block)
         navigating-block (rum/react *navigating-block)
         navigated? (and (not= (:block/uuid block) navigating-block) navigating-block)
-        block (if (or navigated? custom-query?)
+        block (if navigated?
                 (let [block (db/pull [:block/uuid navigating-block])
                       blocks (db/get-paginated-blocks repo (:db/id block)
                                                       {:scoped-block-id (:db/id block)})
@@ -2464,8 +2468,7 @@
         edit? (state/sub [:editor/editing? edit-input-id])
         card? (string/includes? data-refs-self "\"card\"")
         review-cards? (:review-cards? config)
-        selected-blocks (set (state/get-selection-block-ids))
-        selected? (contains? selected-blocks uuid)]
+        selected? (state/sub-block-selected? uuid)]
     [:div.ls-block
      (cond->
        {:id block-id
@@ -2563,7 +2566,7 @@
   (let [repo (state/get-current-repo)
         ref? (:ref? config)
         custom-query? (boolean (:custom-query? config))]
-    (if (and ref? (not custom-query?) (not (:ref-query-child? config)))
+    (if (and (or ref? custom-query?) (not (:ref-query-child? config)))
       (ui/lazy-visible
        (fn [] (block-container-inner state repo config block)))
       (block-container-inner state repo config block))))
@@ -2804,7 +2807,8 @@
   (let [dsl-query? (:dsl-query? config)
         query-atom (:query-atom state)
         repo (state/get-current-repo)
-        query-time (react/get-query-time query)
+        query-time (or (react/get-query-time query)
+                       (react/get-query-time q))
         view-fn (if (keyword? view) (state/sub [:config repo :query/views view]) view)
         current-block-uuid (or (:block/uuid (:block config))
                                (:block/uuid config))
@@ -2946,7 +2950,8 @@
    (ui/block-error "Query Error:" {:content (:query q)})
    (ui/lazy-visible
     (fn [] (custom-query* config q))
-    {:debug-id q})))
+    {:debug-id q
+     :trigger-once? false})))
 
 (defn admonition
   [config type result]
@@ -3287,13 +3292,13 @@
          {:class (when doc-mode? "document-mode")}
          (lazy-blocks config blocks' flat-blocks)]))))
 
-(rum/defcs breadcrumb-with-container < rum/reactive
+(rum/defcs breadcrumb-with-container < rum/reactive db-mixins/query
   {:init (fn [state]
            (let [first-block (ffirst (:rum/args state))]
              (assoc state
                     ::initial-block    first-block
                     ::navigating-block (atom (:block/uuid first-block)))))}
-  [state blocks config]
+  [state block config]
   (let [repo (state/get-current-repo)
         *navigating-block (::navigating-block state)
         navigating-block (rum/react *navigating-block)
@@ -3306,10 +3311,10 @@
                  (let [block navigating-block-entity]
                    (db/get-paginated-blocks repo (:db/id block)
                                             {:scoped-block-id (:db/id block)}))
-                 blocks)]
+                 [block])]
     [:div
      (when (:breadcrumb-show? config)
-       (breadcrumb config (state/get-current-repo) navigating-block
+       (breadcrumb config (state/get-current-repo) (or navigating-block (:block/uuid block))
                    {:show-page? false
                     :navigating-block *navigating-block}))
      (blocks-container blocks (assoc config
@@ -3324,8 +3329,7 @@
    (cond-> option
      (:document/mode? config) (assoc :class "doc-mode"))
    (cond
-     (and (or (:ref? config) (:custom-query? config))
-          (:group-by-page? config))
+     (and (:custom-query? config) (:group-by-page? config))
      [:div.flex.flex-col
       (let [blocks (sort-by (comp :block/journal-day first) > blocks)]
         (for [[page blocks] blocks]
@@ -3333,8 +3337,7 @@
            (fn []
              (let [alias? (:block/alias? page)
                    page (db/entity (:db/id page))
-                   blocks (tree/non-consecutive-blocks->vec-tree blocks)
-                   parent-blocks (group-by :block/parent blocks)]
+                   blocks' (tree/non-consecutive-blocks->vec-tree blocks)]
                [:div.my-2 (cond-> {:key (str "page-" (:db/id page))}
                             (:ref? config)
                             (assoc :class "color-level px-2 sm:px-7 py-2 rounded"))
@@ -3342,11 +3345,34 @@
                  [:div
                   (page-cp config page)
                   (when alias? [:span.text-sm.font-medium.opacity-50 " Alias"])]
-                 (for [[parent blocks] parent-blocks]
+                 (for [block blocks']
                    (rum/with-key
-                     (breadcrumb-with-container blocks config)
-                     (:db/id parent)))
-                 {:debug-id page})])))))]
+                     (breadcrumb-with-container block config)
+                     (:db/id block)))
+                 {:debug-id page
+                  :trigger-once? false})])))))]
+
+     (and (:ref? config) (:group-by-page? config))
+     [:div.flex.flex-col
+      (let [blocks (sort-by (comp :block/journal-day first) > blocks)]
+        (for [[page parent-blocks] blocks]
+         (ui/lazy-visible
+          (fn []
+            (let [alias? (:block/alias? page)
+                  page (db/entity (:db/id page))]
+              [:div.my-2 (cond-> {:key (str "page-" (:db/id page))}
+                           (:ref? config)
+                           (assoc :class "color-level px-2 sm:px-7 py-2 rounded"))
+               (ui/foldable
+                [:div
+                 (page-cp config page)
+                 (when alias? [:span.text-sm.font-medium.opacity-50 " Alias"])]
+                (for [block parent-blocks]
+                  (let [block' (update block :block/children tree/non-consecutive-blocks->vec-tree)]
+                    (rum/with-key
+                      (breadcrumb-with-container block' config)
+                      (:db/id block'))))
+                {:debug-id page})])))))]
 
      (and (:group-by-page? config)
           (vector? (first blocks)))

+ 208 - 193
src/main/frontend/components/content.cljs

@@ -55,35 +55,43 @@
 
 (rum/defc custom-context-menu-content
   []
-  [:div#custom-context-menu
-   [:div.py-1.rounded-md.bg-base-3.shadow-xs
-    (ui/menu-link
-     {:key "cut"
-      :on-click #(editor-handler/cut-selection-blocks true)}
-     "Cut")
-    (ui/menu-link
-     {:key "copy"
-      :on-click editor-handler/copy-selection-blocks}
-     "Copy")
-    (ui/menu-link
-     {:key "copy as"
-      :on-click (fn [_]
-                  (let [block-uuids (editor-handler/get-selected-toplevel-block-uuids)]
-                    (state/set-modal!
-                     #(export/export-blocks block-uuids))))}
-     "Copy as")
-    (ui/menu-link
-     {:key "copy block refs"
-      :on-click editor-handler/copy-block-refs}
-     "Copy block refs")
-    (ui/menu-link
-     {:key "copy block embeds"
-      :on-click editor-handler/copy-block-embeds}
-     "Copy block embeds")
-    (ui/menu-link
-     {:key "cycle todos"
-      :on-click editor-handler/cycle-todos!}
-     "Cycle todos")]])
+  [:.menu-links-wrapper
+   (ui/menu-link
+    {:key "cut"
+     :on-click #(editor-handler/cut-selection-blocks true)}
+    "Cut"
+    nil)
+   (ui/menu-link
+    {:key "copy"
+     :on-click editor-handler/copy-selection-blocks}
+    "Copy"
+    nil)
+   (ui/menu-link
+    {:key "copy as"
+     :on-click (fn [_]
+                 (let [block-uuids (editor-handler/get-selected-toplevel-block-uuids)]
+                   (state/set-modal!
+                    #(export/export-blocks block-uuids))))}
+    "Copy as..."
+    nil)
+   (ui/menu-link
+    {:key "copy block refs"
+     :on-click editor-handler/copy-block-refs}
+    "Copy block refs"
+    nil)
+   (ui/menu-link
+    {:key "copy block embeds"
+     :on-click editor-handler/copy-block-embeds}
+    "Copy block embeds"
+    nil)
+   
+   [:hr.menu-separator]
+
+   (ui/menu-link
+    {:key "cycle todos"
+     :on-click editor-handler/cycle-todos!}
+    "Cycle todos"
+    nil)])
 
 ;; FIXME: Make it configurable
 (def block-background-colors
@@ -145,192 +153,199 @@
                                           (editor-handler/set-block-property! block-id :template-including-parent false))
                                         (state/hide-custom-context-menu!)))))))])
       (ui/menu-link
-       {:key "Make template"
+       {:key "Make a Template"
         :on-click (fn [e]
                     (util/stop e)
                     (reset! edit? true))}
-       "Make template"))))
+       "Make a Template"
+       nil))))
 
 (rum/defc ^:large-vars/cleanup-todo block-context-menu-content
   [_target block-id]
-
-  (let [*el-ref (rum/use-ref nil)]
-
-    (rum/use-effect!
-     (fn []
-       (js/setTimeout
-        (fn []
-          (let [^js el (rum/deref *el-ref)
-               {:keys [x y]} (util/calc-delta-rect-offset el js/document.documentElement)]
-           (set! (.. el -style -transform)
-                 (str "translate3d(" (if (neg? x) x 0) "px," (if (neg? y) (- y 10) 0) "px" ",0)"))))
-        10)
-       #())
-     [])
-
     (when-let [block (db/entity [:block/uuid block-id])]
       (let [properties (:block/properties block)
             heading? (true? (:heading properties))]
-        [:div#custom-context-menu
-         {:ref *el-ref}
-         [:div.py-1.rounded-md.bg-base-3.shadow-xs
-          [:div.flex-row.flex.justify-between.py-4.pl-2
-           [:div.flex-row.flex.justify-between
-            (for [color block-background-colors]
-              [:a.m-2.shadow-sm
-               {:on-click (fn [_e]
-                            (editor-handler/set-block-property! block-id "background-color" color))}
-               [:div.heading-bg {:style {:background-color color}}]])]
-           [:a.text-sm
+        [:.menu-links-wrapper
+         [:div.flex-row.flex.justify-between.pb-2.pt-1.px-2
+          [:div.flex-row.flex.justify-between
+           (for [color block-background-colors]
+             [:a.m-2.shadow-sm
+              {:on-click (fn [_e]
+                           (editor-handler/set-block-property! block-id "background-color" color))}
+              [:div.heading-bg {:style {:background-color color}}]])
+           [:a.m-2.shadow-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"))}
-            "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-property! block-id :heading true)))}
-           (if heading?
-             "Convert back to a block"
-             "Convert to a heading"))
-
-          (ui/menu-link
-           {:key      "Open in sidebar"
-            :on-click (fn [_e]
-                        (editor-handler/open-block-in-sidebar! block-id))}
-           "Open in sidebar")
-
-          (ui/menu-link
-           {:key      "Copy block ref"
-            :on-click (fn [_e]
-                        (editor-handler/copy-block-ref! block-id block-ref/->block-ref))}
-           "Copy block ref")
-
-          (ui/menu-link
-           {:key      "Copy block embed"
-            :on-click (fn [_e]
-                        (editor-handler/copy-block-ref! block-id #(util/format "{{embed ((%s))}}" %)))}
-           "Copy block embed")
+            [:div.heading-bg.remove "-"]]]]
+         
+         [:hr.menu-separator]
+
+         (ui/menu-link
+          {:key      "Open in sidebar"
+           :on-click (fn [_e]
+                       (editor-handler/open-block-in-sidebar! block-id))}
+          "Open in sidebar"
+          ["shift" "click"])
+
+         [:hr.menu-separator]
+
+         (ui/menu-link
+          {:key      "Copy block ref"
+           :on-click (fn [_e]
+                       (editor-handler/copy-block-ref! block-id block-ref/->block-ref))}
+          "Copy block ref"
+          nil)
+
+         (ui/menu-link
+          {:key      "Copy block embed"
+           :on-click (fn [_e]
+                       (editor-handler/copy-block-ref! block-id #(util/format "{{embed ((%s))}}" %)))}
+          "Copy block embed"
+          nil)
 
           ;; TODO Logseq protocol mobile support
-          (when (util/electron?)
-            (ui/menu-link
-             {:key      "Copy block URL"
-              :on-click (fn [_e]
-                          (let [current-repo (state/get-current-repo)
-                                tap-f (fn [block-id]
-                                        (url-util/get-logseq-graph-uuid-url nil current-repo block-id))]
-                            (editor-handler/copy-block-ref! block-id tap-f)))}
-             "Copy block URL"))
-
-          (block-template block-id)
-
-          (ui/menu-link
-           {:key      "Copy as"
-            :on-click (fn [_]
-                        (state/set-modal! #(export/export-blocks [block-id])))}
-           "Copy as")
-
-          (if (srs/card-block? block)
-            (ui/menu-link
-             {:key      "Preview Card"
-              :on-click #(srs/preview (:db/id block))}
-             "Preview Card")
-            (ui/menu-link
-             {:key      "Make a Card"
-              :on-click #(srs/make-block-a-card! block-id)}
-             "Make a Card"))
-
-          (ui/menu-link
-           {:key      "Cut"
-            :on-click (fn [_e]
-                        (editor-handler/cut-block! block-id))}
-           "Cut")
-
-          (ui/menu-link
-           {:key      "Expand all"
-            :on-click (fn [_e]
-                        (editor-handler/expand-all! block-id))}
-           "Expand all")
-
-          (ui/menu-link
-           {:key      "Collapse all"
-            :on-click (fn [_e]
-                        (editor-handler/collapse-all! block-id {}))}
-           "Collapse all")
-
-          (when (state/sub [:plugin/simple-commands])
-            (when-let [cmds (state/get-plugins-commands-with-type :block-context-menu-item)]
-              (for [[_ {:keys [key label] :as cmd} action pid] cmds]
-                (ui/menu-link
-                 {:key      key
-                  :on-click #(commands/exec-plugin-simple-command!
-                              pid (assoc cmd :uuid block-id) action)}
-                 label))))
-
-          (when (state/sub [:ui/developer-mode?])
-            (ui/menu-link
-             {:key      "(Dev) Show block data"
-              :on-click (fn []
-                          (let [block-data (with-out-str (pprint/pprint (db/pull [:block/uuid block-id])))]
-                            (println block-data)
-                            (notification/show!
-                             [:div
-                              [:pre.code block-data]
-                              [:br]
-                              (ui/button "Copy to clipboard"
-                                :on-click #(.writeText js/navigator.clipboard block-data))]
-                             :success
-                             false)))}
-             "(Dev) Show block data"))]]))))
+         (when (util/electron?)
+           (ui/menu-link
+            {:key      "Copy block URL"
+             :on-click (fn [_e]
+                         (let [current-repo (state/get-current-repo)
+                               tap-f (fn [block-id]
+                                       (url-util/get-logseq-graph-uuid-url nil current-repo block-id))]
+                           (editor-handler/copy-block-ref! block-id tap-f)))}
+            "Copy block URL"
+            nil))
+
+         (ui/menu-link
+          {:key      "Copy as"
+           :on-click (fn [_]
+                       (state/set-modal! #(export/export-blocks [block-id])))}
+          "Copy as..."
+          nil)
+
+         (ui/menu-link
+          {:key      "Cut"
+           :on-click (fn [_e]
+                       (editor-handler/cut-block! block-id))}
+          "Cut"
+          nil)
+
+         [:hr.menu-separator]
+
+         (ui/menu-link
+          {:key      "Convert heading"
+           :on-click (fn [_e]
+                       (if heading?
+                         (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")
+          nil)
+
+         (block-template block-id)
+
+         (if (srs/card-block? block)
+           (ui/menu-link
+            {:key      "Preview Card"
+             :on-click #(srs/preview (:db/id block))}
+            "Preview Card"
+            nil)
+           (ui/menu-link
+            {:key      "Make a Card"
+             :on-click #(srs/make-block-a-card! block-id)}
+            "Make a Flashcard"
+            nil))
+
+         [:hr.menu-separator]
+
+         (ui/menu-link
+          {:key      "Expand all"
+           :on-click (fn [_e]
+                       (editor-handler/expand-all! block-id))}
+          "Expand all"
+          nil)
+
+         (ui/menu-link
+          {:key      "Collapse all"
+           :on-click (fn [_e]
+                       (editor-handler/collapse-all! block-id {}))}
+          "Collapse all"
+          nil)
+
+         (when (state/sub [:plugin/simple-commands])
+           (when-let [cmds (state/get-plugins-commands-with-type :block-context-menu-item)]
+             (for [[_ {:keys [key label] :as cmd} action pid] cmds]
+               (ui/menu-link
+                {:key      key
+                 :on-click #(commands/exec-plugin-simple-command!
+                             pid (assoc cmd :uuid block-id) action)}
+                label
+                nil))))
+
+         (when (state/sub [:ui/developer-mode?])
+           (ui/menu-link
+            {:key      "(Dev) Show block data"
+             :on-click (fn []
+                         (let [block-data (with-out-str (pprint/pprint (db/pull [:block/uuid block-id])))]
+                           (println block-data)
+                           (notification/show!
+                            [:div
+                             [:pre.code block-data]
+                             [:br]
+                             (ui/button "Copy to clipboard"
+                                        :on-click #(.writeText js/navigator.clipboard block-data))]
+                            :success
+                            false)))}
+            "(Dev) Show block data"
+            nil))])))
 
 (rum/defc block-ref-custom-context-menu-content
   [block block-ref-id]
   (when (and block block-ref-id)
-    [:div#custom-context-menu
-     [:div.py-1.rounded-md.bg-base-3.shadow-xs
-      (ui/menu-link
-       {:key "open-in-sidebar"
-        :on-click (fn []
-                    (state/sidebar-add-block!
-                     (state/get-current-repo)
-                     block-ref-id
-                     :block-ref))}
-       "Open in sidebar")
-      (ui/menu-link
-       {:key "copy"
-        :on-click (fn [] (editor-handler/copy-current-ref block-ref-id))}
-       "Copy this reference")
-      (ui/menu-link
-       {:key "delete"
-        :on-click (fn [] (editor-handler/delete-current-ref! block block-ref-id))}
-       "Delete this reference")
-      (ui/menu-link
-       {:key "replace-with-text"
-        :on-click (fn [] (editor-handler/replace-ref-with-text! block block-ref-id))}
-       "Replace with text")
-      (ui/menu-link
-       {:key "replace-with-embed"
-        :on-click (fn [] (editor-handler/replace-ref-with-embed! block block-ref-id))}
-       "Replace with embed")]]))
+    [:.menu-links-wrapper
+     (ui/menu-link
+      {:key "open-in-sidebar"
+       :on-click (fn []
+                   (state/sidebar-add-block!
+                    (state/get-current-repo)
+                    block-ref-id
+                    :block-ref))}
+      "Open in sidebar"
+      ["shift" "click"])
+     (ui/menu-link
+      {:key "copy"
+       :on-click (fn [] (editor-handler/copy-current-ref block-ref-id))}
+      "Copy this reference"
+      nil)
+     (ui/menu-link
+      {:key "delete"
+       :on-click (fn [] (editor-handler/delete-current-ref! block block-ref-id))}
+      "Delete this reference"
+      nil)
+     (ui/menu-link
+      {:key "replace-with-text"
+       :on-click (fn [] (editor-handler/replace-ref-with-text! block block-ref-id))}
+      "Replace with text"
+      nil)
+     (ui/menu-link
+      {:key "replace-with-embed"
+       :on-click (fn [] (editor-handler/replace-ref-with-embed! block block-ref-id))}
+      "Replace with embed"
+      nil)]))
 
 (rum/defc page-title-custom-context-menu-content
   [page]
   (when-not (string/blank? page)
     (let [page-menu-options (page-menu/page-menu page)]
-      [:div#custom-context-menu
-       [:div.py-1.rounded-md.bg-base-3.shadow-xs
-        (for [{:keys [title options]} page-menu-options]
-          (ui/menu-link
-           (merge
-            {:key title}
-            options)
-           title))]])))
+      [:.menu-links-wrapper
+       (for [{:keys [title options]} page-menu-options]
+         (ui/menu-link
+          (merge
+           {:key title}
+           options)
+          title
+          nil))])))
 
 ;; TODO: content could be changed
 ;; Also, keyboard bindings should only be activated after
@@ -359,7 +374,7 @@
                              (block-ref-custom-context-menu-content block block-ref))
                             (state/set-state! :block-ref/context nil))
 
-                          (state/selection?)
+                          (and (state/selection?) (not (d/has-class? target "bullet")))
                           (common-handler/show-custom-context-menu!
                            e
                            (custom-context-menu-content))

+ 2 - 4
src/main/frontend/components/content.css

@@ -12,10 +12,8 @@
 }
 
 #custom-context-menu {
-  @apply rounded-md shadow-lg transition ease-out duration-100 transform
-  opacity-100 scale-100 absolute overflow-y-auto;
+  @apply transition ease-out duration-100 transform
+  opacity-100 scale-100 absolute;
 
-  max-height: calc(100vh - 100px) !important;;
-  overflow-y: scroll;
   z-index: calc(var(--ls-z-index-level-1) + 1);
 }

+ 20 - 11
src/main/frontend/components/editor.cljs

@@ -298,8 +298,12 @@
                 (let [[_id on-submit] (:rum/args state)
                       command (:command (first input-option))]
                   (on-submit command @input-value))
-                (reset! input-value nil))))})))
-  [state _id on-submit]
+                (reset! input-value nil))))
+       ;; escape
+       27 (fn [_state _e]
+            (let [[id _on-submit on-cancel] (:rum/args state)]
+              (on-cancel id)))})))
+  [state _id on-submit _on-cancel]
   (when (= :input (state/sub :editor/action))
     (when-let [action-data (state/sub :editor/action-data)]
       (let [{:keys [pos options]} action-data
@@ -331,7 +335,7 @@
                  (on-submit command @input-value pos)))]))))))
 
 (rum/defc absolute-modal < rum/static
-  [cp set-default-width? {:keys [top left rect]}]
+  [cp modal-name set-default-width? {:keys [top left rect]}]
   (let [max-height 370
         max-width 300
         offset-top 24
@@ -376,6 +380,7 @@
                    {:left (if (or (nil? y-diff) (and y-diff (= y-diff 0))) left 0)})))]
     [:div.absolute.rounded-md.shadow-lg.absolute-modal
      {:ref *el
+      :data-modal-name modal-name
       :class (if y-overflow-vh? "is-overflow-vh-y" "")
       :on-mouse-down (fn [e]
                        (.stopPropagation e))
@@ -383,13 +388,13 @@
      cp]))
 
 (rum/defc transition-cp < rum/reactive
-  [cp set-default-width?]
+  [cp modal-name set-default-width?]
   (when-let [pos (:pos (state/sub :editor/action-data))]
     (ui/css-transition
      {:class-names "fade"
       :timeout     {:enter 500
                     :exit  300}}
-     (absolute-modal cp set-default-width? pos))))
+     (absolute-modal cp modal-name set-default-width? pos))))
 
 (rum/defc image-uploader < rum/reactive
   [id format]
@@ -408,6 +413,7 @@
         [:div.flex.flex-row.align-center.rounded-md.shadow-sm.bg-base-2.px-1.py-1
          (ui/loading
           (util/format "Uploading %s%" (util/format "%2d" processing)))]
+        "upload-file"
         false)))])
 
 (defn- set-up-key-down!
@@ -418,11 +424,11 @@
    {:not-matched-handler (editor-handler/keydown-not-matched-handler format)}))
 
 (defn- set-up-key-up!
-  [state input input-id search-timeout]
+  [state input input-id]
   (mixins/on-key-up
    state
    {}
-   (editor-handler/keyup-handler state input input-id search-timeout)))
+   (editor-handler/keyup-handler state input input-id)))
 
 (def search-timeout (atom nil))
 
@@ -432,7 +438,7 @@
         input-id id
         input (gdom/getElement input-id)]
     (set-up-key-down! state format)
-    (set-up-key-up! state input input-id search-timeout)))
+    (set-up-key-up! state input input-id)))
 
 (def starts-with? clojure.string/starts-with?)
 
@@ -506,10 +512,10 @@
     (mock-textarea content)))
 
 (rum/defc animated-modal < rum/reactive
-  [key component set-default-width?]
+  [modal-name component set-default-width?]
   (when-let [pos (:pos (state/get-editor-action-data))]
     (ui/css-transition
-     {:key key
+     {:key modal-name
       :class-names {:enter "origin-top-left opacity-0 transform scale-95"
                     :enter-done "origin-top-left transition opacity-100 transform scale-100"
                     :exit "origin-top-left transition opacity-0 transform scale-95"}
@@ -518,6 +524,7 @@
      (fn [_]
        (absolute-modal
         component
+        modal-name
         set-default-width?
         pos)))))
 
@@ -553,7 +560,9 @@
       (= :input action)
       (animated-modal "input" (input id
                                      (fn [command m]
-                                       (editor-handler/handle-command-input command id format m)))
+                                       (editor-handler/handle-command-input command id format m))
+                                     (fn []
+                                       (editor-handler/handle-command-input-close id)))
                       true)
 
       (= :zotero action)

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

@@ -19,12 +19,12 @@
 
 (rum/defc blocks-cp < rum/reactive db-mixins/query
   {}
-  [repo page _format]
+  [repo page]
   (when-let [page-e (db/pull [:block/name (util/page-name-sanity-lc page)])]
     (page/page-blocks-cp repo page-e {})))
 
 (rum/defc journal-cp < rum/reactive
-  [[title format]]
+  [title]
   (let [;; Don't edit the journal title
         page (string/lower-case title)
         repo (state/sub :git/current-repo)
@@ -56,10 +56,11 @@
         (gp-util/capitalize-all title)]]
 
       (if today?
-        (blocks-cp repo page format)
+        (blocks-cp repo page)
         (ui/lazy-visible
-         (fn [] (blocks-cp repo page format))
-         {:debug-id (str "journal-blocks " page)}))
+         (fn [] (blocks-cp repo page))
+         {:trigger-once? false
+          :debug-id (str "journal-blocks " page)}))
 
       {})
 
@@ -77,9 +78,9 @@
   [:div#journals
    (ui/infinite-list
     "main-content-container"
-    (for [{:block/keys [name format]} latest-journals]
+    (for [{:block/keys [name]} latest-journals]
       [:div.journal-item.content {:key name}
-       (journal-cp [name format])])
+       (journal-cp name)])
     {:has-more (page-handler/has-more-journals?)
      :more-class "text-4xl"
      :on-top-reached page-handler/create-today-journal!

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

@@ -34,7 +34,7 @@
           {:title "Development"
            :children [[(t :help/roadmap) "https://trello.com/b/8txSM12G/roadmap"]
                       [(t :help/bug) "https://github.com/logseq/logseq/issues/new?labels=from:in-app&template=bug_report.yaml"]
-                      [(t :help/feature) "https://github.com/logseq/logseq/issues/new?assignees=&labels=&template=feature_request.md&title="]
+                      [(t :help/feature) "https://discuss.logseq.com/c/feature-requests/"]
                       [(t :help/changelog) "https://docs.logseq.com/#/page/changelog"]]}
           
           {:title "About"

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

@@ -394,10 +394,11 @@
          (tagged-pages repo page-name))
 
        ;; referenced blocks
-       [:div {:key "page-references"}
-        (rum/with-key
-          (reference/references route-page-name)
-          (str route-page-name "-refs"))]
+       (when-not block?
+         [:div {:key "page-references"}
+          (rum/with-key
+            (reference/references route-page-name)
+            (str route-page-name "-refs"))])
 
        (when-not block?
          [:div

+ 5 - 3
src/main/frontend/components/plugins.cljs

@@ -964,12 +964,13 @@
       (when nav?
         [:aside.md:w-64 {:style {:min-width "10rem"}}
          (let [plugins (plugin-handler/get-enabled-plugins-if-setting-schema)]
-           [:ul
+           [:ul.settings-plugin-list
             (for [{:keys [id name title icon]} plugins]
               [:li
                {:class (util/classnames [{:active (= id focused)}])}
-               [:a.flex.items-center
-                {:on-click #(do (state/set-state! :plugin/focused-settings id))}
+               [:a.flex.items-center.settings-plugin-item
+                {:data-id id
+                 :on-click #(do (state/set-state! :plugin/focused-settings id))}
                 (if (and icon (not (string/blank? icon)))
                   [:img.icon {:src icon}]
                   svg/folder)
@@ -977,6 +978,7 @@
 
       [:article
        [:div.panel-wrap
+        {:data-id focused}
         (when-let [^js pl (and focused (= @*cache focused)
                                (plugin-handler/get-plugin-inst focused))]
           (ui/catch-error

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

@@ -79,7 +79,7 @@
     [:div.pl-1 (edit-settings-file pid nil)]]])
 
 (rum/defc render-item-heading
-  [{:keys [title]}]
+  [{:keys [key title]}]
 
   [:div.heading-item
    {:data-key key}

+ 4 - 2
src/main/frontend/components/query_table.cljs

@@ -10,7 +10,8 @@
             [frontend.util.property :as property]
             [frontend.format.block :as block]
             [medley.core :as medley]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [frontend.modules.outliner.tree :as tree]))
 
 ;; TODO: extract to table utils
 (defn- sort-result-by
@@ -82,7 +83,8 @@
   (rum/local false ::select?)
   [state config current-block result {:keys [page?]} map-inline page-cp ->elem inline-text]
   (when current-block
-    (let [p-sort-by (keyword (get-in current-block [:block/properties :query-sort-by]))
+    (let [result (tree/filter-top-level-blocks result)
+          p-sort-by (keyword (get-in current-block [:block/properties :query-sort-by]))
           p-desc? (get-in current-block [:block/properties :query-sort-desc])
           select? (get state ::select?)
           *sort-by-item (get state ::sort-by-item)

+ 126 - 66
src/main/frontend/components/reference.cljs

@@ -13,19 +13,49 @@
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [frontend.modules.outliner.tree :as tree]))
 
 (defn- frequencies-sort
   [references]
   (sort-by second #(> %1 %2) references))
 
+(defn filtered-refs
+  [page-name filters filters-atom filtered-references]
+  [:div.flex.gap-1.flex-wrap
+   (for [[ref-name ref-count] filtered-references]
+     (when ref-name
+       (let [lc-reference (string/lower-case ref-name)]
+         (ui/button
+           [:span
+            ref-name
+            (when ref-count [:sup " " ref-count])]
+           :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))
+           :small? true
+           :intent "link"
+           :key ref-name))))])
+
 (rum/defcs filter-dialog-inner < rum/reactive (rum/local "" ::filterSearch)
-  [state filters-atom _close-fn references page-name]
+  [state filters-atom *references page-name]
   (let [filter-search (get state ::filterSearch)
+        references (rum/react *references)
         filtered-references  (frequencies-sort
                               (if (= @filter-search "")
                                 references
-                                (search/fuzzy-search references @filter-search :limit 500 :extract-fn first)))]
+                                (search/fuzzy-search references @filter-search :limit 500 :extract-fn first)))
+        filters (rum/react filters-atom)
+        includes (keep (fn [[page include?]]
+                         (let [page' (model-db/get-page-original-name page)]
+                           (when include? [page'])))
+                       filters)
+        excludes (keep (fn [[page include?]]
+                         (let [page' (model-db/get-page-original-name page)]
+                           (when-not include? [page'])))
+                       filters)]
     [:div.ls-filters.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
@@ -34,6 +64,16 @@
        [: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 filters)
+       [:div.cp__filters.mb-4.ml-2
+        (when (seq includes)
+          [:div.flex.flex-row.flex-wrap.center-items
+           [:div.mr-1.font-medium.py-1 "Includes: "]
+           (filtered-refs page-name filters filters-atom includes)])
+        (when (seq excludes)
+          [:div.flex.flex-row.flex-wrap
+           [:div.mr-1.font-medium.py-1 "Excludes: " ]
+           (filtered-refs page-name filters filters-atom excludes)])])
      [:div.cp__filters-input-panel.flex
       (ui/icon "search")
       [:input.cp__filters-input.w-full
@@ -41,30 +81,17 @@
         :auto-focus true
         :on-change (fn [e]
                      (reset! filter-search (util/evalue e)))}]]
-     (when (seq filtered-references)
-       (let [filters (rum/react filters-atom)]
-         [:div.mt-5.sm:mt-4.sm:flex.sm.gap-1.flex-wrap
-          (for [[ref-name ref-count] filtered-references]
-            (when ref-name
-              (let [lc-reference (string/lower-case ref-name)
-                    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.select-none
-                 {:key ref-name :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))}
-                 ref-name [:sub " " ref-count]])))]))]))
+     (let [all-filters (set (keys filters))
+           refs (remove (fn [[page _]] (all-filters (util/page-name-sanity-lc page)))
+                        filtered-references)]
+       (when (seq refs)
+         [:div.mt-4
+          (filtered-refs page-name filters filters-atom refs)]))]))
 
 (defn filter-dialog
-  [filters-atom references page-name]
-  (fn [close-fn]
-    (filter-dialog-inner filters-atom close-fn references page-name)))
+  [filters-atom *references page-name]
+  (fn []
+    (filter-dialog-inner filters-atom *references page-name)))
 
 (rum/defc block-linked-references < rum/reactive db-mixins/query
   [block-id]
@@ -80,16 +107,10 @@
      (content/content block-id
                       {:hiccup ref-hiccup})]))
 
-(rum/defc references-inner < rum/reactive db-mixins/query
-  [page-name block-id filters *filtered-ref-blocks ref-pages]
+(rum/defc references-inner
+  [page-name filters filtered-ref-blocks]
   [:div.references-blocks
-   (let [ref-blocks (if block-id
-                      (db/get-block-referenced-blocks block-id)
-                      (db/get-page-referenced-blocks page-name))
-         filtered-ref-blocks (if block-id
-                               ref-blocks
-                               (block-handler/get-filtered-ref-blocks ref-blocks filters ref-pages))
-         ref-hiccup (block/->hiccup filtered-ref-blocks
+   (let [ref-hiccup (block/->hiccup filtered-ref-blocks
                                     {:id page-name
                                      :ref? true
                                      :breadcrumb-show? true
@@ -97,24 +118,21 @@
                                      :editor-box editor/box
                                      :filters filters}
                                     {})]
-     (reset! *filtered-ref-blocks filtered-ref-blocks)
      (content/content page-name {:hiccup ref-hiccup}))])
 
 (rum/defc references-cp
-  [repo page-entity page-name block-id filters-atom filter-state n-ref]
+  [page-name filters filters-atom filter-state total filter-n filtered-ref-blocks *ref-pages]
   (let [threshold (state/get-linked-references-collapsed-threshold)
-        default-collapsed? (>= n-ref threshold)
-        filters (when (seq filter-state)
-                  (-> (group-by second filter-state)
-                      (update-vals #(map first %))))
-        *filtered-ref-blocks (atom nil)
-        *collapsed? (atom nil)
-        ref-pages (when-not block-id
-                    (block-handler/get-blocks-refed-pages repo page-entity))]
+        default-collapsed? (>= total threshold)
+        *collapsed? (atom nil)]
     (ui/foldable
      [:div.flex.flex-row.flex-1.justify-between.items-center
-      [:h2.font-bold.opacity-50 (str n-ref " Linked Reference"
-                                     (when (> n-ref 1) "s"))]
+      [:h2.font-bold.opacity-50 (str
+                                 (when (seq filters)
+                                   (str filter-n " of "))
+                                 total
+                                 " Linked Reference"
+                                 (when (> total 1) "s"))]
       [:a.filter.fade-link
        {:title "Filter"
         :on-mouse-over (fn [_e]
@@ -124,10 +142,8 @@
         :on-mouse-down (fn [e]
                          (util/stop-propagation e))
         :on-click (fn []
-                    (let [ref-pages (map :block/original-name ref-pages)
-                          references (frequencies ref-pages)]
-                      (state/set-modal! (filter-dialog filters-atom references page-name)
-                                        {:center? true})))}
+                    (state/set-modal! (filter-dialog filters-atom *ref-pages page-name)
+                                      {:center? true}))}
        (ui/icon "filter" {:class (cond
                                    (empty? filter-state)
                                    ""
@@ -140,46 +156,90 @@
                           :style {:fontSize 24}})]]
 
      (fn []
-       (references-inner page-name block-id filters *filtered-ref-blocks ref-pages))
+       (references-inner page-name filters filtered-ref-blocks))
 
      {:default-collapsed? default-collapsed?
       :title-trigger? true
       :init-collapsed (fn [collapsed-atom]
                         (reset! *collapsed? collapsed-atom))})))
 
-(rum/defcs references* < rum/reactive
+(defn- get-filtered-children
+  [block parent->blocks]
+  (let [children (get parent->blocks (:db/id block))]
+    (set
+     (loop [blocks children
+            result (vec children)]
+       (if (empty? blocks)
+         result
+         (let [fb (first blocks)
+               children (get parent->blocks (:db/id fb))]
+           (recur
+            (concat children (rest blocks))
+            (conj result fb))))))))
+
+(rum/defcs references* < rum/reactive db-mixins/query
+  (rum/local nil ::ref-pages)
   {:init (fn [state]
            (let [page-name (first (:rum/args state))
                  filters (when page-name
-                           (atom (page-handler/get-filters (string/lower-case page-name))))]
+                           (atom (page-handler/get-filters (util/page-name-sanity-lc page-name))))]
              (assoc state ::filters filters)))}
   [state page-name]
   (when page-name
-    (let [page-name (string/lower-case page-name)
-          page-entity (db/entity [:block/name page-name])
+    (let [page-name (util/page-name-sanity-lc page-name)
+          *ref-pages (::ref-pages state)
           repo (state/get-current-repo)
           filters-atom (get state ::filters)
           filter-state (rum/react filters-atom)
-          block-id (parse-uuid page-name)
-          id (if block-id
-               (:db/id (db/pull [:block/uuid block-id]))
-               (:db/id page-entity))
-          n-ref (model-db/get-linked-references-count id)]
-      (when (or (seq filter-state) (> n-ref 0))
+          ref-blocks (db/get-page-referenced-blocks page-name)
+          page-id (:db/id (db/entity repo [:block/name page-name]))
+          aliases (db/page-alias-set repo page-name)
+          aliases-exclude-self (set (remove #{page-id} aliases))
+          top-level-blocks (filter (fn [b] (some aliases (set (map :db/id (:block/refs b))))) ref-blocks)
+          top-level-blocks-ids (set (map :db/id top-level-blocks))
+          filters (when (seq filter-state)
+                    (-> (group-by second filter-state)
+                        (update-vals #(map first %))))
+          filtered-ref-blocks (block-handler/filter-blocks ref-blocks filters)
+          total (count top-level-blocks)
+          filtered-top-blocks (filter (fn [b] (top-level-blocks-ids (:db/id b))) filtered-ref-blocks)
+          filter-n (count filtered-top-blocks)
+          parent->blocks (group-by (fn [x] (:db/id (x :block/parent))) filtered-ref-blocks)
+          result (->> (group-by :block/page filtered-top-blocks)
+                      (map (fn [[page blocks]]
+                             (let [blocks (sort-by (fn [b] (not= (:db/id page) (:db/id (:block/parent b)))) blocks)
+                                   result (map (fn [block]
+                                                 (let [filtered-children (get-filtered-children block parent->blocks)
+                                                       refs (when-not (contains? top-level-blocks-ids (:db/id (:block/parent block)))
+                                                              (block-handler/get-blocks-refed-pages aliases (cons block filtered-children)))
+                                                       block' (assoc (tree/block-entity->map block) :block/children filtered-children)]
+                                                   [block' refs])) blocks)
+                                   blocks' (map first result)
+                                   page' (if (contains? aliases-exclude-self (:db/id page))
+                                           {:db/id (:db/id page)
+                                            :block/alias? true
+                                            :block/journal-day (:block/journal-day page)}
+                                           page)]
+                               [[page' blocks'] (mapcat second result)]))))
+          filtered-ref-blocks' (map first result)
+          ref-pages (->>
+                     (mapcat second result)
+                     (map :block/original-name)
+                     frequencies)]
+      (reset! *ref-pages ref-pages)
+      (when (or (seq filter-state) (> filter-n 0))
         [:div.references.flex-1.flex-row
          [:div.content.pt-6
-          (references-cp repo page-entity page-name block-id
-                         filters-atom filter-state n-ref)]]))))
+          (references-cp page-name filters filters-atom filter-state total filter-n filtered-ref-blocks' *ref-pages)]]))))
 
 (rum/defc references
   [page-name]
   (ui/catch-error
-   (ui/component-error "Linked References: Unexpected error")
+   (ui/component-error "Linked References: Unexpected error. Please re-index your graph first.")
    (ui/lazy-visible
     (fn []
       (references* page-name))
-    {:trigger-once? true
-     :debug-id (str page-name " references")})))
+    {:debug-id (str page-name " references")})))
 
 (rum/defcs unlinked-references-aux
   < rum/reactive db-mixins/query

+ 8 - 0
src/main/frontend/components/reference.css

@@ -10,3 +10,11 @@
   padding-left: 0.5rem;
   align-items: center;
 }
+
+.references-blocks .breadcrumb {
+  margin-left: 1.5rem;
+}
+
+.ls-filters {
+    max-width: 704px;
+}

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

@@ -37,7 +37,7 @@
         repos (util/distinct-by :url repos)]
     (if (seq repos)
       [:div#graphs
-       [:h1.title "All Graphs"]
+       [:h1.title (t :all-graphs)]
        [:p.ml-2.opacity-70
         "A \"graph\" in Logseq means a local directory."]
 
@@ -49,9 +49,9 @@
             (ui/button
               (t :open-a-directory)
               :on-click #(page-handler/ls-dir-files! shortcut/refresh!))])]
-        (for [{:keys [id url] :as repo} repos]
+        (for [{:keys [url] :as repo} repos]
           (let [local? (config/local-db? url)]
-            [:div.flex.justify-between.mb-4 {:key id}
+            [:div.flex.justify-between.mb-4 {:key (str "id-" url)}
              (if local?
                (let [local-dir (config/get-local-dir url)
                      graph-name (text-util/get-graph-name-from-path local-dir)]
@@ -71,7 +71,7 @@
                {:title "No worries, unlink this graph will clear its cache only, it does not remove your files on the disk."
                 :on-click (fn []
                             (repo-handler/remove-repo! repo))}
-               "Unlink"]]]))]]
+               (t :unlink)]]]))]]
       (widgets/add-graph))))
 
 (defn refresh-cb []

+ 10 - 9
src/main/frontend/components/settings.cljs

@@ -653,23 +653,24 @@
      [:div.cp__settings-inner.md:flex
 
       [:aside.md:w-64 {:style {:min-width "10rem"}}
-       [:ul
-        (for [[label text icon]
-              [[:general (t :settings-page/tab-general) (ui/icon "adjustments" {:style {:font-size 20}})]
-               [:editor (t :settings-page/tab-editor) (ui/icon "writing" {:style {:font-size 20}})]
+       [:ul.settings-menu
+        (for [[label id text icon]
+              [[:general "general" (t :settings-page/tab-general) (ui/icon "adjustments" {:style {:font-size 20}})]
+               [:editor "editor" (t :settings-page/tab-editor) (ui/icon "writing" {:style {:font-size 20}})]
                (when-not (mobile-util/native-platform?)
-                 [:git (t :settings-page/tab-version-control) (ui/icon "history" {:style {:font-size 20}})])
-               [:advanced (t :settings-page/tab-advanced) (ui/icon "bulb" {:style {:font-size 20}})]
+                 [:git "git" (t :settings-page/tab-version-control) (ui/icon "history" {:style {:font-size 20}})])
+               [:advanced "advanced" (t :settings-page/tab-advanced) (ui/icon "bulb" {:style {:font-size 20}})]
                (when plugins-of-settings
-                 [:plugins-setting (t :settings-of-plugins) (ui/icon "puzzle")])]]
+                 [:plugins-setting "plugins" (t :settings-of-plugins) (ui/icon "puzzle")])]]
 
           (when label
-            [:li
+            [:li.settings-menu-item
              {:key      text
               :class    (util/classnames [{:active (= label (first @*active))}])
               :on-click #(reset! *active [label (first @*active)])}
 
-             [:a.flex.items-center
+             [:a.flex.items-center.settings-menu-link
+             {:data-id id}
               icon
               [:strong text]]]))]]
 

+ 68 - 50
src/main/frontend/components/sidebar.cljs

@@ -59,7 +59,7 @@
   [e]
   (when-let [target (.. e -target)]
     (let [rect (.. target getBoundingClientRect)]
-     (- (.. e -pageY) (.. rect -top)))))
+      (- (.. e -pageY) (.. rect -top)))))
 
 (defn- move-up?
   [e]
@@ -71,7 +71,7 @@
   (let [original-name (db-model/get-page-original-name name)]
     [:a {:on-click (fn [e]
                      (let [name (util/safe-page-name-sanity-lc name)
-                           source-page (db-model/get-alias-source-page (state/get-current-repo) name) 
+                           source-page (db-model/get-alias-source-page (state/get-current-repo) name)
                            name (if (empty? source-page) name (:block/name source-page))]
                        (if (gobj/get e "shiftKey")
                          (when-let [page-entity (if (empty? source-page) (db/entity [:block/name name]) source-page)]
@@ -231,54 +231,54 @@
        [:div.nav-header.flex.gap-1.flex-col
         (if-let [page (:page default-home)]
           (sidebar-item
-            {:class            "home-nav"
-             :title            page
-             :on-click-handler route-handler/redirect-to-home!
-             :active           (and (not srs-open?)
-                                    (= route-name :page)
-                                    (= page (get-in route-match [:path-params :name])))
-             :icon             "home"})
+           {:class            "home-nav"
+            :title            page
+            :on-click-handler route-handler/redirect-to-home!
+            :active           (and (not srs-open?)
+                                   (= route-name :page)
+                                   (= page (get-in route-match [:path-params :name])))
+            :icon             "home"})
           (sidebar-item
-            {:class            "journals-nav"
-             :active           (and (not srs-open?)
-                                 (or (= route-name :all-journals) (= route-name :home)))
-             :title            (t :left-side-bar/journals)
-             :on-click-handler route-handler/go-to-journals!
-             :icon             "calendar"}))
+           {:class            "journals-nav"
+            :active           (and (not srs-open?)
+                                   (or (= route-name :all-journals) (= route-name :home)))
+            :title            (t :left-side-bar/journals)
+            :on-click-handler route-handler/go-to-journals!
+            :icon             "calendar"}))
 
         (when (state/enable-flashcards? (state/get-current-repo))
           [:div.flashcards-nav
            (flashcards srs-open?)])
 
         (sidebar-item
-          {:class  "graph-view-nav"
-           :title  (t :right-side-bar/graph-view)
-           :href   (rfe/href :graph)
-           :active (and (not srs-open?) (= route-name :graph))
-           :icon   "hierarchy"})
+         {:class  "graph-view-nav"
+          :title  (t :right-side-bar/graph-view)
+          :href   (rfe/href :graph)
+          :active (and (not srs-open?) (= route-name :graph))
+          :icon   "hierarchy"})
 
         (sidebar-item
-          {:class  "all-pages-nav"
-           :title  (t :right-side-bar/all-pages)
-           :href   (rfe/href :all-pages)
-           :active (and (not srs-open?) (= route-name :all-pages))
-           :icon   "files"})]]
+         {:class  "all-pages-nav"
+          :title  (t :right-side-bar/all-pages)
+          :href   (rfe/href :all-pages)
+          :active (and (not srs-open?) (= route-name :all-pages))
+          :icon   "files"})]]
 
       (when left-sidebar-open? (favorites t))
 
       (when (and left-sidebar-open? (not config/publishing?)) (recent-pages t))
 
       (when-not (mobile-util/native-platform?)
-       [:nav.px-2 {:aria-label "Sidebar"
-                   :class      "new-page"}
-        (when-not config/publishing?
-          [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md.new-page-link
-           {:on-click (fn []
-                        (and (util/sm-breakpoint?)
-                             (state/toggle-left-sidebar!))
-                        (state/pub-event! [:go/search]))}
-           (ui/icon "circle-plus mr-3" {:style {:font-size 20}})
-           [:span.flex-1 (t :right-side-bar/new-page)]])])]]))
+        [:nav.px-2 {:aria-label "Sidebar"
+                    :class      "new-page"}
+         (when-not config/publishing?
+           [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md.new-page-link
+            {:on-click (fn []
+                         (and (util/sm-breakpoint?)
+                              (state/toggle-left-sidebar!))
+                         (state/pub-event! [:go/search]))}
+            (ui/icon "circle-plus mr-3" {:style {:font-size 20}})
+            [:span.flex-1 (t :right-side-bar/new-page)]])])]]))
 
 (rum/defc left-sidebar < rum/reactive
   [{:keys [left-sidebar-open? route-match]}]
@@ -421,7 +421,7 @@
                      (state/sidebar-add-block! current-repo db-id block-type)))
                  (reset! sidebar-inited? true))))
            (when (state/mobile?)
-                  (state/set-state! :mobile/show-tabbar? true))
+             (state/set-state! :mobile/show-tabbar? true))
            state)}
   []
   (let [default-home (get-default-home-if-valid)
@@ -470,15 +470,41 @@
          :else
          [:div])])))
 
+(defn- hide-context-menu-and-clear-selection
+  [e]
+  (state/hide-custom-context-menu!)
+  (when-not (or (gobj/get e "shiftKey")
+                (util/meta-key? e)
+                (state/get-edit-input-id))
+    (editor-handler/clear-selection!)))
+
+(rum/defc render-custom-context-menu
+  [links position]
+  (let [ref (rum/use-ref nil)]
+    (rum/use-effect!
+     #(let [el (rum/deref ref)
+            {:keys [x y]} (util/calc-delta-rect-offset el js/document.documentElement)]
+        (set! (.. el -style -transform)
+              (str "translate3d(" (if (neg? x) x 0) "px," (if (neg? y) (- y 10) 0) "px" ",0)"))))
+    [:<>
+     [:div.menu-backdrop {:on-mouse-down (fn [e] (hide-context-menu-and-clear-selection e))}]
+     [:div#custom-context-menu
+      {:ref ref
+       :style {:left (str (first position) "px")
+               :top (str (second position) "px")}} links]]))
+
 (rum/defc custom-context-menu < rum/reactive
   []
-  (when (state/sub :custom-context-menu/show?)
-    (when-let [links (state/sub :custom-context-menu/links)]
+  (let [show? (state/sub :custom-context-menu/show?)
+        links (state/sub :custom-context-menu/links)
+        position (state/sub :custom-context-menu/position)]
+    (when (and show? links position)
       (ui/css-transition
        {:class-names "fade"
         :timeout {:enter 500
                   :exit 300}}
-       links))))
+       (render-custom-context-menu links position)))))
+
 
 (rum/defc new-block-mode < rum/reactive
   []
@@ -507,14 +533,6 @@
                    (state/sidebar-add-block! (state/get-current-repo) "help" :help))}
       "?"]]))
 
-(defn- hide-context-menu-and-clear-selection
-  [e]
-  (state/hide-custom-context-menu!)
-  (when-not (or (gobj/get e "shiftKey")
-                (util/meta-key? e)
-                (state/get-edit-input-id))
-    (editor-handler/clear-selection!)))
-
 (rum/defcs ^:large-vars/cleanup-todo sidebar <
   (mixins/modal :modal/show?)
   rum/reactive
@@ -619,6 +637,6 @@
                                     :db-restoring? db-restoring?})
       [:a#download.hidden]
       (when
-          (and (not config/mobile?)
-               (not config/publishing?))
-          (help-button))])))
+       (and (not config/mobile?)
+            (not config/publishing?))
+        (help-button))])))

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

@@ -305,11 +305,15 @@
     path
     (util/node-path.join (get-repo-dir repo-url) path)))
 
+;; FIXME: There is another get-file-path at src/main/frontend/fs/capacitor_fs.cljs
 (defn get-file-path
   "Normalization happens here"
   [repo-url relative-path]
   (when (and repo-url relative-path)
     (let [path (cond
+                 (demo-graph?)
+                 nil
+
                  (and (util/electron?) (local-db? repo-url))
                  (let [dir (get-repo-dir repo-url)]
                    (if (string/starts-with? relative-path dir)
@@ -319,7 +323,7 @@
 
                  (and (mobile-util/native-ios?) (local-db? repo-url))
                  (let [dir (get-repo-dir repo-url)]
-                   (js/decodeURI (str dir relative-path)))
+                   (str dir relative-path))
 
                  (and (mobile-util/native-android?) (local-db? repo-url))
                  (let [dir (get-repo-dir repo-url)
@@ -327,14 +331,22 @@
                                    (string/starts-with? dir "content:"))
                              dir
                              (str "file:///" (string/replace dir #"^/+" "")))]
-                   (str (string/replace dir #"/+$" "") "/" relative-path))
+                   (util/safe-path-join dir relative-path))
 
                  (= "/" (first relative-path))
                  (subs relative-path 1)
 
                  :else
                  relative-path)]
-      (gp-util/path-normalize path))))
+      (and (not-empty path) (gp-util/path-normalize path)))))
+
+(defn get-page-file-path
+  "Get the path to the page file for the given page. This is used when creating new files."
+  [repo-url sub-dir page-name ext]
+  (let [page-basename (if (mobile-util/native-platform?)
+                        (util/url-encode page-name)
+                        page-name)]
+    (get-file-path repo-url (str sub-dir "/" page-basename "." ext))))
 
 (defn get-config-path
   ([]
@@ -373,6 +385,15 @@
      (get-file-path repo
                     (str app-name "/" export-css-file)))))
 
+(defn expand-relative-assets-path
+  ;; ../assets/xxx -> {assets|file}://{current-graph-root-path}/xxx
+  [source]
+  (when-let [protocol (and (string? source)
+                           (not (string/blank? source))
+                           (if (util/electron?) "assets" "file"))]
+
+    (string/replace
+     source "../assets" (util/format "%s://%s/assets" protocol (get-repo-dir (state/get-current-repo))))))
 
 (defn get-custom-js-path
   ([]

+ 57 - 91
src/main/frontend/db/model.cljs

@@ -355,28 +355,36 @@
   ([blocks parent]
    (sort-by-left blocks parent {:check? true}))
   ([blocks parent {:keys [check?]}]
-   (when check?
-     (when (not= (count blocks) (count (set (map :block/left blocks))))
-       (let [duplicates (->> (map (comp :db/id :block/left) blocks)
-                             frequencies
-                             (filter (fn [[_k v]] (> v 1)))
-                             (map (fn [[k _v]]
-                                    (let [left (db-utils/pull k)]
-                                      {:left left
-                                       :duplicates (->>
-                                                    (filter (fn [block]
-                                                              (= k (:db/id (:block/left block))))
-                                                            blocks)
-                                                    (map #(select-keys % [:db/id :block/level :block/content :block/file])))}))))]
-         #_(util/pprint duplicates)))
-     (assert (= (count blocks) (count (set (map :block/left blocks)))) "Each block should have a different left node"))
-
-   (let [left->blocks (reduce (fn [acc b] (assoc acc (:db/id (:block/left b)) b)) {} blocks)]
-     (loop [block parent
-            result []]
-       (if-let [next (get left->blocks (:db/id block))]
-         (recur next (conj result next))
-         (vec result))))))
+   (let [blocks (util/distinct-by :db/id blocks)]
+     (when check?
+      (when (not= (count blocks) (count (set (map :block/left blocks))))
+        (let [duplicates (->> (map (comp :db/id :block/left) blocks)
+                              frequencies
+                              (filter (fn [[_k v]] (> v 1)))
+                              (map (fn [[k _v]]
+                                     (let [left (db-utils/pull k)]
+                                       {:left left
+                                        :duplicates (->>
+                                                     (filter (fn [block]
+                                                               (= k (:db/id (:block/left block))))
+                                                             blocks)
+                                                     (map #(select-keys % [:db/id :block/level :block/content :block/file])))}))))]
+          (util/pprint duplicates)))
+      (assert (= (count blocks) (count (set (map :block/left blocks)))) "Each block should have a different left node"))
+
+     (let [left->blocks (reduce (fn [acc b] (assoc acc (:db/id (:block/left b)) b)) {} blocks)]
+       (loop [block parent
+              result []]
+         (if-let [next (get left->blocks (:db/id block))]
+           (recur next (conj result next))
+           (vec result)))))))
+
+(defn try-sort-by-left
+  [blocks parent]
+  (let [result' (sort-by-left blocks parent {:check? false})]
+    (if (= (count result') (count blocks))
+      result'
+      blocks)))
 
 (defn sort-by-left-recursive
   [form]
@@ -449,6 +457,11 @@
         parent-sibling
         (get-next-outdented-block db (:db/id parent))))))
 
+(defn top-block?
+  [block]
+  (= (:db/id (:block/parent block))
+     (:db/id (:block/page block))))
+
 (defn get-block-parent
   ([block-id]
    (get-block-parent (state/get-current-repo) block-id))
@@ -457,11 +470,6 @@
      (when-let [block (d/entity db [:block/uuid block-id])]
        (:block/parent block)))))
 
-(defn top-block?
-  [block]
-  (= (:db/id (:block/parent block))
-     (:db/id (:block/page block))))
-
 ;; non recursive query
 (defn get-block-parents
   ([repo block-id]
@@ -476,13 +484,6 @@
          (recur (:block/uuid parent) (conj parents parent) (inc d))
          parents)))))
 
-(comment
-  (defn get-immediate-children-v2
-    [repo block-id]
-    (d/pull (conn/get-db repo)
-            '[:block/_parent]
-            [:block/uuid block-id])))
-
 ;; Use built-in recursive
 (defn get-block-parents-v2
   [repo block-id]
@@ -858,17 +859,14 @@
 (defn get-block-children-ids
   [repo block-uuid]
   (when-let [db (conn/get-db repo)]
-    (let [eid (:db/id (db-utils/entity repo [:block/uuid block-uuid]))]
-      (->> (d/q
-             '[:find ?id
-               :in $ ?p %
-               :where
-               (child ?p ?c)
-               [?c :block/uuid ?id]]
-             db
-             eid
-             rules)
-           (apply concat)))))
+    (when-let [eid (:db/id (db-utils/entity repo [:block/uuid block-uuid]))]
+      (let [get-children-ids (fn get-children-ids [eid]
+                               (mapcat
+                                (fn [datom]
+                                  (let [id (first datom)]
+                                    (cons (:block/uuid (d/entity db id)) (get-children-ids id))))
+                                (d/datoms db :avet :block/parent eid)))]
+        (get-children-ids eid)))))
 
 (defn get-block-immediate-children
   "Doesn't include nested children."
@@ -1172,21 +1170,19 @@
    (get-page-referenced-blocks-full (state/get-current-repo) page options))
   ([repo page options]
    (when repo
-     (when (conn/get-db repo)
+     (when-let [db (conn/get-db repo)]
        (let [page-id (:db/id (db-utils/entity [:block/name (util/safe-page-name-sanity-lc page)]))
              pages (page-alias-set repo page)
              aliases (set/difference pages #{page-id})]
          (->>
-          (react/q repo
-            [:frontend.db.react/page<-blocks-or-block<-blocks page-id]
-            {}
+          (d/q
             '[:find [(pull ?block ?block-attrs) ...]
               :in $ [?ref-page ...] ?block-attrs
               :where
               [?block :block/path-refs ?ref-page]]
+            db
             pages
             (butlast block-attrs))
-          react
           (remove (fn [block] (= page-id (:db/id (:block/page block)))))
           db-utils/group-by-page
           (map (fn [[k blocks]]
@@ -1208,7 +1204,7 @@
              aliases (set/difference pages #{page-id})]
          (->>
           (react/q repo
-            [:frontend.db.react/page<-blocks-or-block<-blocks page-id]
+            [:frontend.db.react/refs page-id]
             {:use-cache? false
              :query-fn (fn []
                          (let [entities (mapcat (fn [id]
@@ -1221,35 +1217,7 @@
             nil)
           react
           :entities
-          (remove (fn [block] (= page-id (:db/id (:block/page block)))))
-          db-utils/group-by-page
-          (map (fn [[k blocks]]
-                 (let [k (if (contains? aliases (:db/id k))
-                           {:db/id (:db/id k)
-                            :block/alias? true
-                            :block/journal-day (:block/journal-day k)}
-                           k)]
-                   [k blocks])))))))))
-
-(defn get-linked-references-count
-  [id]
-  (when-let [block (db-utils/entity id)]
-    (let [repo (state/get-current-repo)
-          page? (:block/name block)
-          result (if page?
-                   (let [pages (page-alias-set repo (:block/name block))]
-                     (d/q
-                       '[:find [?block ...]
-                         :in $ [?ref-page ...] ?id
-                         :where
-                         [?block :block/refs ?ref-page]
-                         [?block :block/page ?p]
-                         [(not= ?p ?id)]]
-                       (conn/get-db repo)
-                       pages
-                       id))
-                   (:block/_refs block))]
-      (count result))))
+          (remove (fn [block] (= page-id (:db/id (:block/page block)))))))))))
 
 (defn get-date-scheduled-or-deadlines
   [journal-title]
@@ -1311,8 +1279,6 @@
              (sort-by-left-recursive)
              db-utils/group-by-page)))))
 
-;; TODO: Replace recursive queries with datoms index implementation
-;; see https://github.com/tonsky/datascript/issues/130#issuecomment-169520434
 (defn get-block-referenced-blocks
   ([block-uuid]
    (get-block-referenced-blocks block-uuid {}))
@@ -1320,16 +1286,16 @@
    (when-let [repo (state/get-current-repo)]
      (when (conn/get-db repo)
        (let [block (db-utils/entity [:block/uuid block-uuid])
-             query-result (->> (react/q repo [:frontend.db.react/page<-blocks-or-block<-blocks
+             query-result (->> (react/q repo [:frontend.db.react/refs
                                               (:db/id block)]
-                                        {:use-cache? false}
-                                        '[:find [(pull ?ref-block ?block-attrs) ...]
-                                          :in $ ?block-uuid ?block-attrs
-                                          :where
-                                          [?block :block/uuid ?block-uuid]
-                                          [?ref-block :block/refs ?block]]
-                                        block-uuid
-                                        block-attrs)
+                                 {}
+                                 '[:find [(pull ?ref-block ?block-attrs) ...]
+                                   :in $ ?block-uuid ?block-attrs
+                                   :where
+                                   [?block :block/uuid ?block-uuid]
+                                   [?ref-block :block/refs ?block]]
+                                 block-uuid
+                                 block-attrs)
                                react
                                (sort-by-left-recursive))]
          (db-utils/group-by-page query-result))))))

+ 86 - 81
src/main/frontend/db/react.cljs

@@ -24,38 +24,24 @@
 ;; get block&children react-query
 (s/def ::block-and-children (s/tuple #(= ::block-and-children %) uuid?))
 
-(s/def ::block-direct-children (s/tuple #(= ::block-direct-children %) uuid?))
 ;; ::journals
 ;; get journal-list react-query
 (s/def ::journals (s/tuple #(= ::journals %)))
-;; ::page->pages
-;; get PAGES referenced by PAGE
-(s/def ::page->pages (s/tuple #(= ::page->pages %) int?))
 ;; ::page<-pages
 ;; get PAGES referencing PAGE
 (s/def ::page<-pages (s/tuple #(= ::page<-pages %) int?))
-;; ::page<-blocks-or-block<-blocks
+;; ::refs
 ;; get BLOCKS referencing PAGE or BLOCK
-(s/def ::page<-blocks-or-block<-blocks
-  (s/tuple #(= ::page<-blocks-or-block<-blocks %) int?))
-;; FIXME: this react-query has performance issues
-(s/def ::page-unlinked-refs (s/tuple #(= ::page-unlinked-refs %) int?))
-;; ::block<-block-ids
-;; get BLOCK-IDS referencing BLOCK
-(s/def ::block<-block-ids (s/tuple #(= ::block<-block-ids %) int?))
+(s/def ::refs (s/tuple #(= ::refs %) int?))
 ;; custom react-query
 (s/def ::custom any?)
 
 (s/def ::react-query-keys (s/or :block ::block
                                 :page-blocks ::page-blocks
                                 :block-and-children ::block-and-children
-                                :block-direct-children ::block-direct-children
                                 :journals ::journals
-                                :page->pages ::page->pages
                                 :page<-pages ::page<-pages
-                                :page<-blocks-or-block<-blocks ::page<-blocks-or-block<-blocks
-                                :page-unlinked-refs ::page-unlinked-refs
-                                :block<-block-ids ::block<-block-ids
+                                :refs ::refs
                                 :custom ::custom))
 
 (s/def ::affected-keys (s/coll-of ::react-query-keys))
@@ -125,13 +111,14 @@
 
 (defn add-q!
   [k query time inputs result-atom transform-fn query-fn inputs-fn]
-  (swap! query-state assoc k {:query query
-                              :query-time time
-                              :inputs inputs
-                              :result result-atom
-                              :transform-fn transform-fn
-                              :query-fn query-fn
-                              :inputs-fn inputs-fn})
+  (let [time' (int (util/safe-parse-float time))]
+    (swap! query-state assoc k {:query query
+                               :query-time time'
+                               :inputs inputs
+                               :result result-atom
+                               :transform-fn transform-fn
+                               :query-fn query-fn
+                               :inputs-fn inputs-fn}))
   result-atom)
 
 (defn remove-q!
@@ -141,14 +128,16 @@
 (defn add-query-component!
   [key component]
   (when (and key component)
-    (swap! query-components assoc component key)))
+    (swap! query-components update component (fn [col] (set (conj col key))))))
 
 (defn remove-query-component!
   [component]
-  (when-let [query (get @query-components component)]
-    (let [matched-queries (filter #(= query %) (vals @query-components))]
-      (when (= 1 (count matched-queries))
-        (remove-q! query))))
+  (when-let [queries (get @query-components component)]
+    (let [all-queries (apply concat (vals @query-components))]
+      (doseq [query queries]
+        (let [matched-queries (filter #(= query %) all-queries)]
+          (when (= 1 (count matched-queries))
+            (remove-q! query))))))
   (swap! query-components dissoc component))
 
 ;; TODO: rename :custom to :query/custom
@@ -220,64 +209,80 @@
       (let [page-name (util/page-name-sanity-lc page)]
         (db-utils/entity [:block/name page-name])))))
 
+(defn- get-block-parents
+  [db id]
+  (let [get-parent (fn [id] (:db/id (:block/parent (d/entity db id))))]
+    (loop [result [id]
+           id id]
+      (if-let [parent (get-parent id)]
+        (recur (conj result parent) parent)
+        result))))
+
+(defn- get-blocks-parents-from-both-dbs
+  [db-after db-before block-entities]
+  (let [current-db-parent-ids (->> (set (keep :block/parent block-entities))
+                                   (mapcat (fn [parent]
+                                             (get-block-parents db-after (:db/id parent)))))
+        before-db-parent-ids (->> (map :db/id block-entities)
+                                  (mapcat (fn [id]
+                                            (get-block-parents db-before id))))]
+    (set (concat current-db-parent-ids before-db-parent-ids))))
+
 (defn get-affected-queries-keys
   "Get affected queries through transaction datoms."
-  [{:keys [tx-data db-before]}]
+  [{:keys [tx-data db-before db-after]}]
   {:post [(s/valid? ::affected-keys %)]}
   (let [blocks (->> (filter (fn [datom] (contains? #{:block/left :block/parent :block/page} (:a datom))) tx-data)
                     (map :v)
                     (distinct))
-        refs (->> (filter (fn [datom] (= :block/refs (:a datom))) tx-data)
+        refs (->> (filter (fn [datom]
+                            (when (contains? #{:block/refs :block/path-refs} (:a datom))
+                              (not= (:v datom)
+                                    (:db/id (:block/page (db-utils/entity (:e datom))))))) tx-data)
                   (map :v)
                   (distinct))
         other-blocks (->> (filter (fn [datom] (= "block" (namespace (:a datom)))) tx-data)
                           (map :e))
         blocks (-> (concat blocks other-blocks) distinct)
+        block-entities (keep (fn [block-id]
+                              (let [block-id (if (and (string? block-id) (util/uuid-string? block-id))
+                                               [:block/uuid block-id]
+                                               block-id)]
+                                (db-utils/entity block-id))) blocks)
         affected-keys (concat
                        (mapcat
-                        (fn [block-id]
-                          (let [block-id (if (and (string? block-id) (util/uuid-string? block-id))
-                                           [:block/uuid block-id]
-                                           block-id)]
-                            (when-let [block (db-utils/entity block-id)]
-                              (let [page-id (or
-                                             (when (:block/name block) (:db/id block))
-                                             (:db/id (:block/page block)))
-                                    blocks [[::block (:db/id block)]]
-                                    others (when page-id
-                                             (let [db-after-parent-uuid (:block/uuid (:block/parent block))
-                                                   db-before-parent-uuid (:block/uuid (:block/parent (d/entity db-before
-                                                                                                               [:block/uuid (:block/uuid block)])))]
-                                               [[::page-blocks page-id]
-                                                [::page->pages page-id]
-                                                [::block-direct-children db-after-parent-uuid]
-                                                (when (and db-before-parent-uuid
-                                                           (not= db-before-parent-uuid db-after-parent-uuid))
-                                                  [::block-direct-children db-before-parent-uuid])]))]
-                                (concat blocks others)))))
-                        blocks)
+                        (fn [block]
+                          (let [page-id (or
+                                         (when (:block/name block) (:db/id block))
+                                         (:db/id (:block/page block)))
+                                blocks [[::block (:db/id block)]]
+                                path-refs (:block/path-refs block)
+                                path-refs' (keep (fn [ref]
+                                                   (when-not (= (:db/id ref) page-id)
+                                                     [::refs (:db/id ref)])) path-refs)
+                                page-blocks (when page-id
+                                              [[::page-blocks page-id]])]
+                            (concat blocks page-blocks path-refs')))
+                        block-entities)
+
+                       (mapcat
+                        (fn [ref]
+                          [[::refs ref]])
+                        refs)
 
                        (when-let [current-page-id (:db/id (get-current-page))]
-                         [[::page->pages current-page-id]
-                          [::page<-pages current-page-id]])
-
-                       (map (fn [ref]
-                              (let [entity (db-utils/entity ref)]
-                                (if (:block/name entity) ; page
-                                  [::page-blocks ref]
-                                  [::page-blocks (:db/id (:block/page entity))])))
-                         refs))
-        others (->>
-                (keys @query-state)
-                (filter (fn [ks]
-                          (contains? #{::block-and-children
-                                       ::page<-blocks-or-block<-blocks}
-                                     (second ks))))
-                (map (fn [v] (vec (rest v)))))]
+                         [[::page<-pages current-page-id]]))
+        parent-ids (get-blocks-parents-from-both-dbs db-after db-before block-entities)
+        block-children-keys (->>
+                             (keys @query-state)
+                             (keep (fn [ks]
+                                     (when (and (= ::block-and-children (second ks))
+                                                (contains? parent-ids (last ks)))
+                                       (vec (rest ks))))))]
     (->>
      (util/concat-without-nil
       affected-keys
-      others)
+      block-children-keys)
      set)))
 
 (defn- execute-query!
@@ -314,8 +319,6 @@
   (when-let [outliner-op (:outliner-op tx-meta)]
     (not (or
           (contains? #{:collapse-expand-blocks :delete-blocks} outliner-op)
-          ;; ignore move up/down since it doesn't affect the refs for any blocks
-          (contains? #{:move-blocks-up-down} (:move-op tx-meta))
           (:undo? tx-meta) (:redo? tx-meta)))))
 
 (defn refresh!
@@ -335,16 +338,18 @@
                        custom?
                        kv?))
               (let [{:keys [query query-fn]} cache
-                    query-or-refs? (state/edit-in-query-or-refs-component)]
-                (when (or query query-fn)
-                  (try
-                    (let [f #(execute-query! repo-url db k tx cache {:skip-query-time-check? query-or-refs?})]
-                      ;; Detects whether user is editing in a custom query, if so, execute the query immediately
-                      (if (or query-or-refs? (not custom?))
-                        (f)
-                        (async/put! (state/get-reactive-custom-queries-chan) [f query])))
-                    (catch js/Error e
-                      (js/console.error e))))))))))))
+                    {:keys [custom-query?]} (state/edit-in-query-or-refs-component)]
+                (util/profile
+                 (str "refresh! " (rest k))
+                 (when (or query query-fn)
+                   (try
+                     (let [f #(execute-query! repo-url db k tx cache {:skip-query-time-check? custom-query?})]
+                       ;; Detects whether user is editing in a custom query, if so, execute the query immediately
+                       (if (and custom? (not custom-query?))
+                         (async/put! (state/get-reactive-custom-queries-chan) [f query])
+                         (f)))
+                     (catch js/Error e
+                       (js/console.error e)))))))))))))
 
 (defn set-key-value
   [repo-url key value]

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

@@ -4074,6 +4074,7 @@
         :right-side-bar/all-pages "Bütün sayfalar"
         :right-side-bar/flashcards "Bilgi kartları"
         :right-side-bar/new-page "Yeni sayfa"
+        :right-side-bar/show-journals "Günlükleri Göster"
         :left-side-bar/journals "Günlük"
         :left-side-bar/new-page "Yeni sayfa"
         :left-side-bar/nav-favorites "Sık kullanılanlar"
@@ -4122,6 +4123,7 @@
         :page/created-at "Oluşturulma Zamanı"
         :page/updated-at "Güncellenme Zamanı"
         :page/backlinks "Geri Bağlantılar"
+        :linked-references/filter-search "Bağlantılı sayfalarda ara"
         :editor/block-search "Blok ara"
         :editor/image-uploading "Karşıya yükleniyor"
         :draw/invalid-file "Bu geçersiz excalidraw dosyası yüklenemedi"

+ 4 - 4
src/main/frontend/external/roam_export.cljs

@@ -23,10 +23,10 @@
 (defn uuid->uid-map []
   (let [db (db/get-db (state/get-current-repo))]
     (->>
-     (d/q db '[:find (pull ?r [:block/uuid])
-               :in $
-               :where
-               [?b :block/refs ?r]])
+     (d/q '[:find (pull ?r [:block/uuid])
+            :in $
+            :where
+            [?b :block/refs ?r]] db)
      (map (comp :block/uuid first))
      (distinct)
      (map (fn [uuid] [uuid (nano-id)]))

+ 107 - 93
src/main/frontend/fs/capacitor_fs.cljs

@@ -18,27 +18,58 @@
     []
     (.ensureDocuments mobile-util/ios-file-container)))
 
-(defn check-permission-android []
-  (p/let [permission (.checkPermissions Filesystem)
-          permission (-> permission
-                         bean/->clj
-                         :publicStorage)]
-    (when-not (= permission "granted")
-      (p/do!
-       (.requestPermissions Filesystem)))))
+(when (mobile-util/native-android?)
+  (defn- android-check-permission []
+    (p/let [permission (.checkPermissions Filesystem)
+            permission (-> permission
+                           bean/->clj
+                           :publicStorage)]
+      (when-not (= permission "granted")
+        (p/do!
+         (.requestPermissions Filesystem))))))
 
-(defn- clean-uri
-  [uri]
-  (when (string? uri)
-    (util/url-decode uri)))
+(defn- <write-file-with-utf8
+  [path content]
+  (when-not (string/blank? path)
+    (-> (p/chain (.writeFile Filesystem (clj->js {:path path
+                                                  :data content
+                                                  :encoding (.-UTF8 Encoding)
+                                                  :recursive true}))
+                 #(js->clj % :keywordize-keys true))
+        (p/catch (fn [error]
+                   (js/console.error "writeFile Error: " path ": " error)
+                   nil)))))
 
-(defn- read-file-utf8
+(defn- <read-file-with-utf8
   [path]
   (when-not (string/blank? path)
-    (.readFile Filesystem
-               (clj->js
-                {:path path
-                 :encoding (.-UTF8 Encoding)}))))
+    (-> (p/chain (.readFile Filesystem (clj->js {:path path
+                                                 :encoding (.-UTF8 Encoding)}))
+                 #(js->clj % :keywordize-keys true)
+                 #(get % :data nil))
+        (p/catch (fn [error]
+                   (js/console.error "readFile Error: " path ": " error)
+                   nil)))))
+
+(defn- <readdir [path]
+  (-> (p/chain (.readdir Filesystem (clj->js {:path path}))
+               js->clj
+               #(get % "files" nil))
+      (p/catch (fn [error]
+                 (js/console.error "readdir Error: " path ": " error)
+                 nil))))
+
+(defn- <stat [path]
+  (-> (p/chain (.stat Filesystem (clj->js {:path path}))
+               #(js->clj % :keywordize-keys true)
+               #(update % :type (fn [v]
+                                  (case v
+                                    "NSFileTypeDirectory" "directory"
+                                    "NSFileTypeRegular" "file"
+                                    v))))
+      (p/catch (fn [error]
+                 (js/console.error "stat Error: " path ": " error)
+                 nil))))
 
 (defn readdir
   "readdir recursively"
@@ -48,10 +79,7 @@
                    (if (empty? dirs)
                      result
                      (p/let [d (first dirs)
-                             files (.readdir Filesystem (clj->js {:path d}))
-                             files (-> files
-                                       js->clj
-                                       (get "files" []))
+                             files (<readdir d)
                              files (->> files
                                         (remove (fn [file]
                                                   (or (string/starts-with? file ".")
@@ -61,44 +89,31 @@
                                                       (= file "bak")))))
                              files (->> files
                                         (map (fn [file]
+                                               ;; TODO: use uri-join
                                                (str (string/replace d #"/+$" "")
                                                     "/"
                                                     (if (mobile-util/native-ios?)
-                                                      (util/url-encode file)
+                                                      (js/encodeURI file)
                                                       file)))))
-                             files-with-stats (p/all
-                                               (mapv
-                                                (fn [file]
-                                                  (p/chain
-                                                   (.stat Filesystem (clj->js {:path file}))
-                                                   #(js->clj % :keywordize-keys true)))
-                                                files))
+                             files-with-stats (p/all (mapv <stat files))
                              files-dir (->> files-with-stats
-                                            (filterv
-                                             (fn [{:keys [type]}]
-                                               (contains? #{"directory" "NSFileTypeDirectory"} type)))
+                                            (filterv #(= (:type %) "directory"))
                                             (mapv :uri))
                              files-result
                              (p/all
                               (->> files-with-stats
-                                   (filter
-                                    (fn [{:keys [type]}]
-                                      (contains? #{"file" "NSFileTypeRegular"} type)))
+                                   (filter #(= (:type %) "file"))
                                    (filter
                                     (fn [{:keys [uri]}]
                                       (some #(string/ends-with? uri %)
                                             [".md" ".markdown" ".org" ".edn" ".css"])))
                                    (mapv
                                     (fn [{:keys [uri] :as file-result}]
-                                      (p/chain
-                                       (read-file-utf8 uri)
-                                       #(js->clj % :keywordize-keys true)
-                                       :data
-                                       #(assoc file-result :content %))))))]
+                                      (p/chain (<read-file-with-utf8 uri)
+                                               #(assoc file-result :content %))))))]
                        (p/recur (concat result files-result)
-                                (concat (rest dirs) files-dir)))))
-          result (js->clj result :keywordize-keys true)]
-    (map (fn [result] (update result :uri clean-uri)) result)))
+                                (concat (rest dirs) files-dir)))))]
+    (js->clj result :keywordize-keys true)))
 
 (defn- contents-matched?
   [disk-content db-content]
@@ -113,36 +128,50 @@
   [repo-dir path ext]
   (let [relative-path (-> (string/replace path repo-dir "")
                           (string/replace (str "." ext) ""))]
-    (str repo-dir backup-dir "/" relative-path)))
+    (util/safe-path-join repo-dir (str backup-dir "/" relative-path))))
 
 (defn- truncate-old-versioned-files!
-  "reserve the latest 3 version files"
+  "reserve the latest 6 version files"
   [dir]
   (p/let [files (readdir dir)
           files (js->clj files :keywordize-keys true)
-          old-versioned-files (drop 3 (reverse (sort-by :mtime files)))]
+          old-versioned-files (drop 6 (reverse (sort-by :mtime files)))]
     (mapv (fn [file]
-            (.deleteFile Filesystem (clj->js {:path (js/encodeURI (:uri file))})))
+            (.deleteFile Filesystem (clj->js {:path (:uri file)})))
           old-versioned-files)))
 
 (defn backup-file
   [repo-dir path content ext]
   (let [backup-dir (get-backup-dir repo-dir path ext)
         new-path (str backup-dir "/" (string/replace (.toISOString (js/Date.)) ":" "_") "." ext)]
-    (.writeFile Filesystem (clj->js {:data content
-                                     :path new-path
-                                     :encoding (.-UTF8 Encoding)
-                                     :recursive true}))
+    (<write-file-with-utf8 new-path content)
     (truncate-old-versioned-files! backup-dir)))
 
+(defn backup-file-handle-changed!
+  [repo-dir file-path content]
+  (let [divider-schema    "://"
+        file-schema       (string/split file-path divider-schema)
+        file-schema       (if (> (count file-schema) 1) (first file-schema) "")
+        dir-schema?       (and (string? repo-dir)
+                               (string/includes? repo-dir divider-schema))
+        repo-dir          (if-not dir-schema?
+                            (str file-schema divider-schema repo-dir) repo-dir)
+        backup-root       (util/safe-path-join repo-dir backup-dir)
+        backup-dir-parent (util/node-path.dirname file-path)
+        backup-dir-parent (string/replace backup-dir-parent repo-dir "")
+        backup-dir-name (util/node-path.name file-path)
+        file-extname (.extname util/node-path file-path)
+        file-root (util/safe-path-join backup-root backup-dir-parent backup-dir-name)
+        file-path (util/safe-path-join file-root
+                                       (str (string/replace (.toISOString (js/Date.)) ":" "_") "." (mobile-util/platform) file-extname))]
+    (<write-file-with-utf8 file-path content)
+    (truncate-old-versioned-files! file-root)))
+
 (defn- write-file-impl!
   [_this repo _dir path content {:keys [ok-handler error-handler old-content skip-compare?]} stat]
   (if skip-compare?
     (p/catch
-     (p/let [result (.writeFile Filesystem (clj->js {:path path
-                                                     :data content
-                                                     :encoding (.-UTF8 Encoding)
-                                                     :recursive true}))]
+     (p/let [result (<write-file-with-utf8 path content)]
        (when ok-handler
          (ok-handler repo path result)))
      (fn [error]
@@ -150,16 +179,12 @@
          (error-handler error)
          (log/error :write-file-failed error))))
 
-    (p/let [disk-content (-> (p/chain (read-file-utf8 path)
-                                      #(js->clj % :keywordize-keys true)
-                                      :data)
-                             (p/catch (fn [error]
-                                        (js/console.error error)
-                                        nil)))
+    ;; Compare with disk content and backup if not equal
+    (p/let [disk-content (<read-file-with-utf8 path)
             disk-content (or disk-content "")
             repo-dir (config/get-local-dir repo)
-            ext (string/lower-case (util/get-file-ext path))
-            db-content (or old-content (db/get-file repo (js/decodeURI path)) "")
+            ext (util/get-file-ext path)
+            db-content (or old-content (db/get-file repo path) "")
             contents-matched? (contents-matched? disk-content db-content)
             pending-writes (state/get-write-chan-length)]
       (cond
@@ -174,10 +199,7 @@
 
         :else
         (->
-         (p/let [result (.writeFile Filesystem (clj->js {:path path
-                                                         :data content
-                                                         :encoding (.-UTF8 Encoding)
-                                                         :recursive true}))
+         (p/let [result (<write-file-with-utf8 path content)
                  mtime (-> (js->clj stat :keywordize-keys true)
                            :mtime)]
            (when-not contents-matched?
@@ -186,7 +208,7 @@
            (p/let [content (if (encrypt/encrypted-db? (state/get-current-repo))
                              (encrypt/decrypt content)
                              content)]
-             (db/set-file-content! repo (js/decodeURI path) content))
+             (db/set-file-content! repo path content))
            (when ok-handler
              (ok-handler repo path result))
            result)
@@ -196,25 +218,19 @@
                       (log/error :write-file-failed error)))))))))
 
 (defn get-file-path [dir path]
-  (let [[dir path] (map #(some-> %
-                                 js/decodeURI)
-                        [dir path])
-        dir (some-> dir (string/replace #"/+$" ""))
-        path (some-> path (string/replace #"^/+" ""))
-        path (cond (nil? path)
-                   dir
+  (let [dir (some-> dir (string/replace #"/+$" ""))
+        path (some-> path (string/replace #"^/+" ""))]
+    (cond (nil? path)
+          dir
 
-                   (nil? dir)
-                   path
+          (nil? dir)
+          path
 
-                   (string/starts-with? path dir)
-                   path
+          (string/starts-with? path dir)
+          path
 
-                   :else
-                   (str dir "/" path))]
-    (if (mobile-util/native-ios?)
-      (js/encodeURI (js/decodeURI path))
-      path)))
+          :else
+          (str dir "/" path))))
 
 (defn- local-container-path?
   "Check whether `path' is logseq's container `localDocumentsPath' on iOS"
@@ -263,7 +279,7 @@
                    (string/replace-first path "file://" "")
                    path)
             repo-dir (config/get-local-dir repo)
-            recycle-dir (str repo-dir config/app-name "/.recycle")
+            recycle-dir (str repo-dir config/app-name "/.recycle") ;; logseq/.recycle
             file-name (-> (string/replace path repo-dir "")
                           (string/replace "/" "_")
                           (string/replace "\\" "_"))
@@ -276,11 +292,7 @@
   (read-file [_this dir path _options]
     (let [path (get-file-path dir path)]
       (->
-       (p/let [content (read-file-utf8 path)
-               content (-> (js->clj content :keywordize-keys true)
-                           :data
-                           clj->js)]
-         content)
+       (<read-file-with-utf8 path)
        (p/catch (fn [error]
                   (log/error :read-file-failed error))))))
   (write-file! [this repo dir path content opts]
@@ -288,6 +300,7 @@
       (p/let [stat (p/catch
                     (.stat Filesystem (clj->js {:path path}))
                     (fn [_e] :not-found))]
+        ;; `path` is full-path
         (write-file-impl! this repo dir path content opts stat))))
   (rename! [_this _repo old-path new-path]
     (let [[old-path new-path] (map #(get-file-path "" %) [old-path new-path])]
@@ -306,16 +319,17 @@
                                          }))]
         result)))
   (open-dir [_this _ok-handler]
-    (p/let [_    (when (= (mobile-util/platform) "android") (check-permission-android))
+    (p/let [_ (when (mobile-util/native-android?) (android-check-permission))
             {:keys [path localDocumentsPath]} (-> (.pickFolder mobile-util/folder-picker)
                                                   (p/then #(js->clj % :keywordize-keys true))
                                                   (p/catch (fn [e]
                                                              (js/alert (str e))
-                                                             nil))) ;; NOTE: Can not pick folder, let it crash
+                                                             nil))) ;; NOTE: If pick folder fails, let it crash
             _ (when (and (mobile-util/native-ios?)
                          (not (or (local-container-path? path localDocumentsPath)
                                   (mobile-util/iCloud-container-path? path))))
                 (state/pub-event! [:modal/show-instruction]))
+            _ (js/console.log "Opening or Creating graph at directory: " path)
             files (readdir path)
             files (js->clj files :keywordize-keys true)]
       (into [] (concat [{:path path}] files))))
@@ -324,6 +338,6 @@
   (watch-dir! [_this dir]
     (p/do!
      (.unwatch mobile-util/fs-watcher)
-     (.watch mobile-util/fs-watcher #js {:path dir})))
+     (.watch mobile-util/fs-watcher (clj->js {:path dir}))))
   (unwatch-dir! [_this _dir]
     (.unwatch mobile-util/fs-watcher)))

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

@@ -11,7 +11,6 @@
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util.block-ref :as block-ref]
             [lambdaisland.glogi :as log]
-            [electron.ipc :as ipc]
             [promesa.core :as p]
             [frontend.state :as state]
             [frontend.encrypt :as encrypt]
@@ -35,7 +34,11 @@
   [repo path content db-content mtime backup?]
   (p/let [
           ;; save the previous content in a versioned bak file to avoid data overwritten.
-          _ (when backup? (ipc/ipc "backupDbFile" (config/get-local-dir repo) path db-content content))
+          _ (when backup?
+              (-> (when-let [repo-dir (config/get-local-dir repo)]
+                    (file-handler/backup-file! repo-dir path db-content content))
+                  (p/catch #(js/console.error "❌ Bak Error: " path %))))
+
           _ (file-handler/alter-file repo path content {:re-render-root? true
                                                         :from-disk? true})]
     (set-missing-block-ids! content)

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

@@ -1,15 +1,18 @@
 (ns frontend.handler
-  (:require [electron.ipc :as ipc]
+  (:require [cljs.reader :refer [read-string]]
+            [clojure.string :as string]
+            [electron.ipc :as ipc]
             [electron.listener :as el]
             [frontend.components.page :as page]
             [frontend.components.reference :as reference]
             [frontend.config :as config]
-            [frontend.context.i18n :as i18n]
+            [frontend.context.i18n :as i18n :refer [t]]
             [frontend.db :as db]
-            [logseq.db.schema :as db-schema]
             [frontend.db.conn :as conn]
+            [frontend.db.persist :as db-persist]
             [frontend.db.react :as react]
             [frontend.error :as error]
+            [frontend.extensions.srs :as srs]
             [frontend.handler.command-palette :as command-palette]
             [frontend.handler.events :as events]
             [frontend.handler.file :as file-handler]
@@ -18,21 +21,20 @@
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.user :as user-handler]
-            [frontend.extensions.srs :as srs]
-            [frontend.mobile.util :as mobile-util]
             [frontend.idb :as idb]
+            [frontend.mobile.util :as mobile-util]
             [frontend.modules.instrumentation.core :as instrument]
+            [frontend.modules.outliner.datascript :as outliner-db]
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.state :as state]
             [frontend.storage :as storage]
+            [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util.persist-var :as persist-var]
-            [cljs.reader :refer [read-string]]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
-            [promesa.core :as p]
-            [frontend.db.persist :as db-persist]
-            [frontend.modules.outliner.datascript :as outliner-db]))
+            [logseq.db.schema :as db-schema]
+            [promesa.core :as p]))
 
 (defn set-global-error-notification!
   []
@@ -82,7 +84,7 @@
        {:repos repos}
        old-db-schema
        (fn [repo]
-         (file-handler/restore-config! repo false)))
+         (file-handler/restore-config! repo)))
       (p/then
        (fn []
          ;; try to load custom css only for current repo
@@ -104,7 +106,7 @@
            (state/set-db-restoring! false))))
       (p/then
        (fn []
-         (prn "db restored, setting up repo hooks")
+         (js/console.log "db restored, setting up repo hooks")
          (store-schema!)
 
          (state/pub-event! [:modal/nfs-ask-permission])
@@ -143,18 +145,6 @@
                                (.postMessage js/window #js {":datalog-console.remote/remote-message" (pr-str db)} "*")
 
                                nil)))))))
-(defn- get-repos
-  []
-  (p/let [nfs-dbs (db-persist/get-all-graphs)
-          nfs-dbs (map (fn [db]
-                         {:url db :nfs? true}) nfs-dbs)]
-    (cond
-      (seq nfs-dbs)
-      nfs-dbs
-
-      :else
-      [{:url config/local-repo
-        :example? true}])))
 
 (defn clear-cache!
   []
@@ -168,6 +158,29 @@
               (js/window.location.reload)))
      2000)))
 
+(defn- get-repos
+  []
+  (p/let [nfs-dbs (db-persist/get-all-graphs)]
+    ;; TODO: Better IndexDB migration handling
+    (cond
+      (and (mobile-util/native-platform?)
+           (some #(or (string/includes? % " ")
+                      (string/includes? % "logseq_local_/")) nfs-dbs))
+      (do (notification/show! ["DB version is not compatible, please clear cache then re-add your graph back."
+                               (ui/button
+                                (t :settings-page/clear-cache)
+                                :class    "text-sm p-1"
+                                :on-click clear-cache!)] :error false)
+          {:url config/local-repo
+           :example? true})
+
+      (seq nfs-dbs)
+      (map (fn [db] {:url db :nfs? true}) nfs-dbs)
+
+      :else
+      [{:url config/local-repo
+        :example? true}])))
+
 (defn- register-components-fns!
   []
   (state/set-page-blocks-cp! page/page-blocks-cp)

+ 22 - 41
src/main/frontend/handler/block.cljs

@@ -13,9 +13,7 @@
    [frontend.state :as state]
    [frontend.util :as util]
    [goog.dom :as gdom]
-   [logseq.graph-parser.block :as gp-block]
-   [frontend.modules.instrumentation.posthog :as posthog]
-   [cljs-bean.core :as bean]))
+   [logseq.graph-parser.block :as gp-block]))
 
 ;;  Fns
 
@@ -250,48 +248,31 @@
   (reset! *swipe nil))
 
 (defn get-blocks-refed-pages
-  [repo page-entity]
-  (let [pages (db-model/page-alias-set repo (:block/name page-entity))
-        refs (->> pages
-                  (mapcat (fn [id] (:block/_path-refs (db/entity id))))
-                  (mapcat (fn [b] (conj (:block/path-refs b) (:block/page b))))
-                  (remove (fn [r] (= (:db/id page-entity) (:db/id r)))))]
+  [aliases ref-blocks]
+  (let [refs (->> (mapcat (fn [b] (conj (:block/path-refs b) (:block/page b))) ref-blocks)
+                  distinct
+                  (remove #(aliases (:db/id %))))]
     (keep (fn [ref]
             (when (:block/name ref)
               {:db/id (:db/id ref)
                :block/name (:block/name ref)
                :block/original-name (:block/original-name ref)})) refs)))
 
-(defn- filter-blocks
-  [ref-blocks filters ref-pages]
-  (let [ref-pages (distinct ref-pages)]
-    (if (empty? filters)
-      ref-blocks
-      (let [ref-pages (zipmap (map :block/name ref-pages) (map :db/id ref-pages))
-            exclude-ids (->> (keep (fn [page] (get ref-pages page)) (get filters false))
-                             (set))
-            include-ids (->> (keep (fn [page] (get ref-pages page)) (get filters true))
-                             (set))]
-        (cond->> ref-blocks
-          (seq exclude-ids)
-          (remove (fn [block]
-                    (let [ids (set (map :db/id (:block/path-refs block)))]
-                      (seq (set/intersection exclude-ids ids)))))
+(defn filter-blocks
+  [ref-blocks filters]
+  (if (empty? filters)
+    ref-blocks
+    (let [exclude-ids (->> (keep (fn [page] (:db/id (db/entity [:block/name (util/page-name-sanity-lc page)]))) (get filters false))
+                           (set))
+          include-ids (->> (keep (fn [page] (:db/id (db/entity [:block/name (util/page-name-sanity-lc page)]))) (get filters true))
+                           (set))]
+      (cond->> ref-blocks
+        (seq exclude-ids)
+        (remove (fn [block]
+                  (let [ids (set (map :db/id (:block/path-refs block)))]
+                    (seq (set/intersection exclude-ids ids)))))
 
-          (seq include-ids)
-          (remove (fn [block]
-                    (let [ids (set (map :db/id (:block/path-refs block)))]
-                      (empty? (set/intersection include-ids ids))))))))))
-
-(defn get-filtered-ref-blocks
-  [ref-blocks filters ref-pages]
-  (try
-    (let [ref-blocks' (doall (mapcat second ref-blocks))
-          filtered-blocks (filter-blocks ref-blocks' filters ref-pages)]
-      (group-by :block/page filtered-blocks))
-    (catch :default e
-      (js/console.error e)
-      (posthog/capture :bad-ref-blocks (bean/->js
-                                        {:ref-blocks ref-blocks
-                                         :filters filters
-                                         :ref-pages ref-pages})))))
+        (seq include-ids)
+        (filter (fn [block]
+                  (let [ids (set (map :db/id (:block/path-refs block)))]
+                    (set/subset? include-ids ids))))))))

+ 2 - 14
src/main/frontend/handler/common.cljs

@@ -2,7 +2,6 @@
   (:require [cljs-bean.core :as bean]
             [cljs.reader :as reader]
             [clojure.string :as string]
-            [dommy.core :as d]
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.db :as db]
@@ -116,19 +115,8 @@
 
 (defn show-custom-context-menu! [e context-menu-content]
   (util/stop e)
-  (let [client-x (gobj/get e "clientX")
-        client-y (gobj/get e "clientY")
-        scroll-y (util/cur-doc-top)]
-    (state/show-custom-context-menu! context-menu-content)
-
-    ;; FIXME: use setTimeout here because rum renders lazily.
-    (js/setTimeout
-     (fn []
-       (when-let [context-menu (d/by-id "custom-context-menu")]
-        (d/set-style! context-menu
-                      :left (str client-x "px")
-                      :top (str (+ scroll-y client-y) "px"))))
-     10)))
+  (let [position [(gobj/get e "clientX") (gobj/get e "clientY")]]
+    (state/show-custom-context-menu! context-menu-content position)))
 
 (defn parse-config
   "Parse configuration from file `content` such as from config.edn."

+ 32 - 82
src/main/frontend/handler/editor.cljs

@@ -204,8 +204,8 @@
 
 (defn clear-selection!
   []
-  (util/select-unhighlight! (dom/by-class "selected"))
-  (state/clear-selection!))
+  (state/clear-selection!)
+  (util/select-unhighlight! (dom/by-class "selected")))
 
 (defn- text-range-by-lst-fst-line [content [direction pos]]
   (case direction
@@ -259,40 +259,6 @@
     (and (not= current-id id)
          (db/entity [:block/uuid id]))))
 
-(defn- attach-page-properties-if-exists!
-  [block]
-  (if (and (:block/pre-block? block)
-           (seq (:block/properties block)))
-    (let [page-properties (:block/properties block)
-          str->page (fn [n] (block/page-name->map n true))
-          refs (->> page-properties
-                    (filter (fn [[_ v]] (coll? v)))
-                    (vals)
-                    (apply concat)
-                    (set)
-                    (map str->page)
-                    (concat (:block/refs block))
-                    (util/distinct-by :block/name))
-          {:keys [tags alias]} page-properties
-          page-tx (let [id (:db/id (:block/page block))
-                        retract-attributes (when id
-                                             (mapv (fn [attribute]
-                                                     [:db/retract id attribute])
-                                                   [:block/properties :block/tags :block/alias]))
-                        tags (->> (map str->page tags) (remove nil?))
-                        alias (->> (map str->page alias) (remove nil?))
-                        tx (cond-> {:db/id id
-                                    :block/properties page-properties}
-                             (seq tags)
-                             (assoc :block/tags tags)
-                             (seq alias)
-                             (assoc :block/alias alias))]
-                    (conj retract-attributes tx))]
-      (assoc block
-             :block/refs refs
-             :db/other-tx page-tx))
-    block))
-
 (defn- remove-non-existed-refs!
   [refs]
   (remove (fn [x] (or
@@ -385,7 +351,6 @@
                 block
                 (dissoc block :block/pre-block?))
         block (update block :block/refs remove-non-existed-refs!)
-        block (attach-page-properties-if-exists! block)
         new-properties (merge
                         (select-keys properties (property/hidden-properties))
                         (:block/properties block))]
@@ -756,7 +721,8 @@
                    (remove nil?))]
       (doseq [id ids]
         (let [block (db/pull [:block/uuid id])]
-          (set-marker block))))))
+          (when (not-empty (:block/content block))
+            (set-marker block)))))))
 
 (defn cycle-todo!
   []
@@ -1073,7 +1039,10 @@
     ;; remove embeds, references and queries
     (let [dom-blocks (remove (fn [block]
                               (or (= "true" (dom/attr block "data-transclude"))
-                                  (= "true" (dom/attr block "data-query")))) blocks)]
+                                  (= "true" (dom/attr block "data-query")))) blocks)
+          dom-blocks (if (seq dom-blocks) dom-blocks
+                         (remove (fn [block]
+                                   (= "true" (dom/attr block "data-transclude"))) blocks))]
       (when (seq dom-blocks)
         (let [repo (state/get-current-repo)
               block-uuids (distinct (map #(uuid (dom/attr % "blockid")) dom-blocks))
@@ -1219,8 +1188,7 @@
 
 (defn clear-last-selected-block!
   []
-  (let [block (state/drop-last-selection-block!)]
-    (util/select-unhighlight! [block])))
+  (state/drop-last-selection-block!))
 
 (defn highlight-selection-area!
   [end-block]
@@ -1235,18 +1203,18 @@
 (defn- select-block-up-down
   [direction]
   (cond
-      ;; when editing, quit editing and select current block
+    ;; when editing, quit editing and select current block
     (state/editing?)
     (state/exit-editing-and-set-selected-blocks! [(gdom/getElement (state/get-editing-block-dom-id))])
 
-      ;; when selection and one block selected, select next block
+    ;; when selection and one block selected, select next block
     (and (state/selection?) (== 1 (count (state/get-selection-blocks))))
     (let [f (if (= :up direction) util/get-prev-block-non-collapsed util/get-next-block-non-collapsed-skip)
           element (f (first (state/get-selection-blocks)))]
       (when element
         (state/conj-selection-block! element direction)))
 
-      ;; if same direction, keep conj on same direction
+    ;; if same direction, keep conj on same direction
     (and (state/selection?) (= direction (state/get-selection-direction)))
     (let [f (if (= :up direction) util/get-prev-block-non-collapsed util/get-next-block-non-collapsed-skip)
           first-last (if (= :up direction) first last)
@@ -1254,7 +1222,7 @@
       (when element
         (state/conj-selection-block! element direction)))
 
-      ;; if different direction, keep clear until one left
+    ;; if different direction, keep clear until one left
     (state/selection?)
     (clear-last-selected-block!))
   nil)
@@ -1729,6 +1697,13 @@
       :markdown (util/format "![%s](%s)" label link)
       :org (util/format "[[%s]]"))))
 
+(defn handle-command-input-close [id]
+  (state/set-editor-show-input! nil)
+  (when-let [saved-cursor (state/get-editor-last-pos)]
+    (when-let [input (gdom/getElement id)]
+      (.focus input)
+      (cursor/move-cursor-to input saved-cursor))))
+
 (defn handle-command-input [command id format m]
   ;; TODO: Add error handling for when user doesn't provide a required field.
   ;; (The current behavior is to just revert back to the editor.)
@@ -1752,41 +1727,16 @@
 
     nil)
 
-  (state/set-editor-show-input! nil)
-
-  (when-let [saved-cursor (state/get-editor-last-pos)]
-    (when-let [input (gdom/getElement id)]
-      (.focus input)
-      (cursor/move-cursor-to input saved-cursor))))
-
-(defn get-search-q
-  []
-  (when-let [id (state/get-edit-input-id)]
-    (when-let [input (gdom/getElement id)]
-      (let [current-pos (cursor/pos input)
-            pos (state/get-editor-last-pos)
-            edit-content (or (state/sub [:editor/content id]) "")]
-        (or
-         @*selected-text
-         (gp-util/safe-subs edit-content pos current-pos))))))
+  (handle-command-input-close id))
 
 (defn close-autocomplete-if-outside
   [input]
   (when (and input
-             (state/get-editor-action)
-             (not (wrapped-by? input page-ref/left-brackets page-ref/right-brackets)))
-    (when (get-search-q)
-      (let [value (gobj/get input "value")
-            pos (state/get-editor-last-pos)
-            current-pos (cursor/pos input)
-            between (gp-util/safe-subs value (min pos current-pos) (max pos current-pos))]
-        (when (and between
-                   (or
-                    (string/includes? between "[")
-                    (string/includes? between "]")
-                    (string/includes? between "(")
-                    (string/includes? between ")")))
-          (state/clear-editor-action!))))))
+             (contains? #{:page-search :page-search-hashtag :block-search} (state/get-editor-action))
+             (not (wrapped-by? input page-ref/left-brackets page-ref/right-brackets))
+             (not (wrapped-by? input block-ref/left-parens block-ref/right-parens))
+             (not (wrapped-by? input "#" "")))
+    (state/clear-editor-action!)))
 
 (defn resize-image!
   [block-id metadata full_text size]
@@ -1816,7 +1766,7 @@
     (reset! *auto-save-timeout
             (js/setTimeout
              (fn []
-               (when (state/input-idle? repo)
+               (when (state/input-idle? repo :diff 500)
                  (state/set-editor-op! :auto-save)
                  ; don't auto-save for page's properties block
                  (save-current-block! {:skip-properties? true})
@@ -2801,7 +2751,7 @@
         nil))))
 
 (defn ^:large-vars/cleanup-todo keyup-handler
-  [_state input input-id search-timeout]
+  [_state input input-id]
   (fn [e key-code]
     (when-not (util/event-is-composing? e)
       (let [current-pos (cursor/pos input)
@@ -2874,7 +2824,7 @@
           (when (and (not editor-action) (not non-enter-processed?))
             (cond
               ;; When you type text inside square brackets
-              (and (not (contains? #{"ArrowDown" "ArrowLeft" "ArrowRight" "ArrowUp"} k))
+              (and (not (contains? #{"ArrowDown" "ArrowLeft" "ArrowRight" "ArrowUp" "Escape"} k))
                    (wrapped-by? input page-ref/left-brackets page-ref/right-brackets))
               (let [orig-pos (cursor/get-caret-pos input)
                     value (gobj/get input "value")
@@ -2922,11 +2872,11 @@
                 (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
                 (state/set-editor-show-block-commands!))
 
-              (nil? @search-timeout)
-              (close-autocomplete-if-outside input)
-
               :else
               nil)))
+
+        (close-autocomplete-if-outside input)
+
         (when-not (or (= k "Shift") is-processed?)
           (state/set-last-key-code! {:key-code key-code
                                      :code code

+ 21 - 11
src/main/frontend/handler/editor/keyboards.cljs

@@ -1,6 +1,5 @@
 (ns frontend.handler.editor.keyboards
-  (:require [dommy.core :as d]
-            [frontend.handler.editor :as editor-handler]
+  (:require [frontend.handler.editor :as editor-handler]
             [frontend.mixins :as mixins]
             [frontend.state :as state]
             [goog.dom :as gdom]))
@@ -12,15 +11,26 @@
     (mixins/hide-when-esc-or-outside
      state
      :on-hide
-     (fn [_state e event]
-       (let [target (.-target e)]
-         (if (d/has-class? target "bottom-action") ;; FIXME: not particular case
-           (.preventDefault e)
-           (let [{:keys [on-hide value]} (editor-handler/get-state)]
-             (when on-hide
-               (on-hide value event))
-             (when (contains? #{:esc :visibilitychange :click} event)
-               (state/clear-edit!))))))
+     (fn [_state _e event]
+       (cond
+         (contains?
+          #{:commands :block-commands
+            :page-search :page-search-hashtag :block-search :template-search
+            :property-search :property-value-search
+            :datepicker}
+          (state/get-editor-action))
+         (state/clear-editor-action!) ;; FIXME: This should probably be handled as a keydown handler in editor, but this handler intercepts Esc first
+
+         ;; editor/input component handles Escape directly, so just prevent handling it here
+         (= :input (state/get-editor-action))
+         nil
+
+         :else
+         (let [{:keys [on-hide value]} (editor-handler/get-state)]
+           (when on-hide
+             (on-hide value event))
+           (when (contains? #{:esc :visibilitychange :click} event)
+             (state/clear-edit!)))))
      :node (gdom/getElement id)
     ;; :visibilitychange? true
 )))

+ 11 - 7
src/main/frontend/handler/events.cljs

@@ -59,6 +59,7 @@
   (db/set-key-value repo :ast/version db-schema/ast-version)
   (search-handler/rebuild-indices!)
   (db/persist! repo)
+  (plugin-handler/hook-plugin-app :graph-after-indexed {:repo repo :empty-graph? empty-graph?})
   (when (state/setups-picker?)
     (if empty-graph?
       (route-handler/redirect! {:to :import :query-params {:from "picker"}})
@@ -134,9 +135,11 @@
                             {:before     #(notification/show!
                                            (ui/loading (t :graph/save))
                                            :warning)
-                             :on-success #(notification/show!
-                                           (ui/loading (t :graph/save-success))
-                                           :warning)
+                             :on-success #(do
+                                            (notification/clear-all!)
+                                            (notification/show!
+                                             (t :graph/save-success)
+                                             :success))
                              :on-error   #(notification/show!
                                            (t :graph/save-error)
                                            :error)}))
@@ -394,7 +397,7 @@
               (state/set-current-repo! current-repo)
               (db/listen-and-persist! current-repo)
               (db/persist-if-idle! current-repo)
-              (file-handler/restore-config! current-repo false)
+              (file-handler/restore-config! current-repo)
               (.watch mobile-util/fs-watcher #js {:path current-repo-dir})
               (when graph-switch-f (graph-switch-f current-repo true)))))))))
 
@@ -451,10 +454,11 @@
 (defmethod handle :file-watcher/changed [[_ ^js event]]
   (let [type (.-event event)
         payload (-> event
-                    (js->clj :keywordize-keys true)
-                    (update :path js/decodeURI))]
+                    (js->clj :keywordize-keys true))
+        ;; TODO: remove this
+        payload' (-> payload (update :path js/decodeURI))]
     (fs-watcher/handle-changed! type payload)
-    (sync/file-watch-handler type payload)))
+    (sync/file-watch-handler type payload')))
 
 (defmethod handle :rebuild-slash-commands-list [[_]]
   (page-handler/rebuild-slash-commands-list!))

+ 22 - 7
src/main/frontend/handler/file.cljs

@@ -8,11 +8,13 @@
             [frontend.db :as db]
             [frontend.fs :as fs]
             [frontend.fs.nfs :as nfs]
+            [frontend.fs.capacitor-fs :as capacitor-fs]
             [frontend.handler.common :as common-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.state :as state]
             [frontend.util :as util]
             [logseq.graph-parser.util :as gp-util]
+            [electron.ipc :as ipc]
             [lambdaisland.glogi :as log]
             [promesa.core :as p]
             [frontend.mobile.util :as mobile]
@@ -53,9 +55,9 @@
   (keep-formats files (gp-config/img-formats)))
 
 (defn restore-config!
-  ([repo-url project-changed-check?]
-   (restore-config! repo-url nil project-changed-check?))
-  ([repo-url config-content _project-changed-check?]
+  ([repo-url]
+   (restore-config! repo-url nil))
+  ([repo-url config-content]
    (let [config-content (if config-content config-content
                             (common-handler/get-config repo-url))]
      (when config-content
@@ -80,13 +82,26 @@
                    (log/error :nfs/load-files-error repo-url)
                    (log/error :exception error))))))
 
+(defn backup-file!
+  "Backup db content to bak directory"
+  [repo-url path db-content content]
+  (cond
+    (util/electron?)
+    (ipc/ipc "backupDbFile" repo-url path db-content content)
+
+    (mobile/native-platform?)
+    (capacitor-fs/backup-file-handle-changed! repo-url path db-content)
+
+    :else
+    nil))
+
 (defn- page-exists-in-another-file
   "Conflict of files towards same page"
   [repo-url page file]
   (when-let [page-name (:block/name page)]
     (let [current-file (:file/path (db/get-page-file repo-url page-name))]
       (when (not= file current-file)
-       current-file))))
+        current-file))))
 
 (defn- get-delete-blocks [repo-url first-page file]
   (let [delete-blocks (->
@@ -178,7 +193,7 @@
     (util/p-handle (write-file!)
                    (fn [_]
                      (when (= path (config/get-config-path repo))
-                       (restore-config! repo true))
+                       (restore-config! repo))
                      (when (= path (config/get-custom-css-path repo))
                        (ui-handler/add-style-if-exists!))
                      (when re-render-root? (ui-handler/re-render-root!)))
@@ -277,7 +292,7 @@
         path (str config/app-name "/" config/metadata-file)
         file-path (str "/" path)
         default-content (if encrypted? "{:db/encrypted? true}" "{}")]
-    (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
+    (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join 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)))))
@@ -288,7 +303,7 @@
         path (str config/app-name "/" config/pages-metadata-file)
         file-path (str "/" path)
         default-content "{}"]
-    (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
+    (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join 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)))))

+ 2 - 0
src/main/frontend/handler/history.cljs

@@ -21,6 +21,7 @@
   [e]
   (util/stop e)
   (state/set-editor-op! :undo)
+  (state/clear-editor-action!)
   (editor/save-current-block!)
   (let [{:keys [editor-cursor]} (undo-redo/undo)]
     (restore-cursor! editor-cursor))
@@ -30,6 +31,7 @@
   [e]
   (util/stop e)
   (state/set-editor-op! :redo)
+  (state/clear-editor-action!)
   (let [{:keys [editor-cursor]} (undo-redo/redo)]
     (restore-cursor! editor-cursor))
   (state/set-editor-op! nil))

+ 85 - 80
src/main/frontend/handler/plugin.cljs

@@ -591,89 +591,94 @@
   (let [el (js/document.createElement "div")]
     (.appendChild js/document.body el)
     (rum/mount
-      (lsp-indicator) el))
+     (lsp-indicator) el))
 
   (state/set-state! :plugin/indicator-text "LOADING")
 
-  (p/then
-    (p/let [root (get-ls-dotdir-root)
-            _ (.setupPluginCore js/LSPlugin (bean/->js {:localUserConfigRoot root :dotConfigRoot root}))
-
-            clear-commands! (fn [pid]
-                              ;; commands
-                              (unregister-plugin-slash-command pid)
-                              (invoke-exported-api "unregister_plugin_simple_command" pid)
-                              (invoke-exported-api "uninstall_plugin_hook" pid)
-                              (unregister-plugin-ui-items pid)
-                              (unregister-plugin-resources pid))
-
-            _ (doto js/LSPluginCore
-                (.on "registered"
-                     (fn [^js pl]
-                       (register-plugin
-                         (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
-
-                (.on "reloaded"
-                     (fn [^js pl]
-                       (register-plugin
-                         (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
-
-                (.on "unregistered" (fn [pid]
-                                      (let [pid (keyword pid)]
-                                        ;; effects
-                                        (unregister-plugin-themes pid)
-                                        ;; plugins
-                                        (swap! state/state medley/dissoc-in [:plugin/installed-plugins pid])
-                                        ;; commands
-                                        (clear-commands! pid))))
-
-                (.on "unlink-plugin" (fn [pid]
-                                       (let [pid (keyword pid)]
-                                         (ipc/ipc "uninstallMarketPlugin" (name pid)))))
-
-                (.on "beforereload" (fn [^js pl]
-                                      (let [pid (.-id pl)]
-                                        (clear-commands! pid)
-                                        (unregister-plugin-themes pid false))))
-
-                (.on "disabled" (fn [pid]
-                                  (clear-commands! pid)
-                                  (unregister-plugin-themes pid)))
-
-                (.on "themes-changed" (fn [^js themes]
-                                       (swap! state/state assoc :plugin/installed-themes
-                                              (vec (mapcat (fn [[pid vs]] (mapv #(assoc % :pid pid) (bean/->clj vs))) (bean/->clj themes))))))
-
-                (.on "theme-selected" (fn [^js theme]
-                                        (let [theme (bean/->clj theme)
-                                              url (:url theme)
-                                              mode (:mode theme)]
-                                          (when mode
-                                            (state/set-custom-theme! mode theme)
-                                            (state/set-theme-mode! mode))
-                                          (hook-plugin-app :theme-changed theme)
-                                          (state/set-state! :plugin/selected-theme url))))
-
-                (.on "reset-custom-theme" (fn [^js themes]
-                                            (let [themes (bean/->clj themes)
-                                                  custom-theme (dissoc themes :mode)
-                                                  mode (:mode themes)]
-                                              (state/set-custom-theme! {:light (if (nil? (:light custom-theme)) {:mode "light"} (:light custom-theme))
-                                                                        :dark (if (nil? (:dark custom-theme)) {:mode "dark"} (:dark custom-theme))})
-                                              (state/set-theme-mode! mode))))
-
-                (.on "settings-changed" (fn [id ^js settings]
-                                          (let [id (keyword id)]
-                                            (when (and settings
-                                                       (contains? (:plugin/installed-plugins @state/state) id))
-                                              (update-plugin-settings-state id (bean/->clj settings)))))))
-
-            default-plugins (get-user-default-plugins)
-
-            _ (.register js/LSPluginCore (bean/->js (if (seq default-plugins) default-plugins [])) true)])
-    #(do
-       (state/set-state! :plugin/indicator-text "END")
-       (callback))))
+  (-> (p/let [root            (get-ls-dotdir-root)
+              _               (.setupPluginCore js/LSPlugin (bean/->js {:localUserConfigRoot root :dotConfigRoot root}))
+
+              clear-commands! (fn [pid]
+                                ;; commands
+                                (unregister-plugin-slash-command pid)
+                                (invoke-exported-api "unregister_plugin_simple_command" pid)
+                                (invoke-exported-api "uninstall_plugin_hook" pid)
+                                (unregister-plugin-ui-items pid)
+                                (unregister-plugin-resources pid))
+
+              _               (doto js/LSPluginCore
+                                (.on "registered"
+                                     (fn [^js pl]
+                                       (register-plugin
+                                        (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
+
+                                (.on "reloaded"
+                                     (fn [^js pl]
+                                       (register-plugin
+                                        (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
+
+                                (.on "unregistered" (fn [pid]
+                                                      (let [pid (keyword pid)]
+                                                        ;; effects
+                                                        (unregister-plugin-themes pid)
+                                                        ;; plugins
+                                                        (swap! state/state medley/dissoc-in [:plugin/installed-plugins pid])
+                                                        ;; commands
+                                                        (clear-commands! pid))))
+
+                                (.on "unlink-plugin" (fn [pid]
+                                                       (let [pid (keyword pid)]
+                                                         (ipc/ipc "uninstallMarketPlugin" (name pid)))))
+
+                                (.on "beforereload" (fn [^js pl]
+                                                      (let [pid (.-id pl)]
+                                                        (clear-commands! pid)
+                                                        (unregister-plugin-themes pid false))))
+
+                                (.on "disabled" (fn [pid]
+                                                  (clear-commands! pid)
+                                                  (unregister-plugin-themes pid)))
+
+                                (.on "themes-changed" (fn [^js themes]
+                                                        (swap! state/state assoc :plugin/installed-themes
+                                                               (vec (mapcat (fn [[pid vs]] (mapv #(assoc % :pid pid) (bean/->clj vs))) (bean/->clj themes))))))
+
+                                (.on "theme-selected" (fn [^js theme]
+                                                        (let [theme (bean/->clj theme)
+                                                              url   (:url theme)
+                                                              mode  (:mode theme)]
+                                                          (when mode
+                                                            (state/set-custom-theme! mode theme)
+                                                            (state/set-theme-mode! mode))
+                                                          (hook-plugin-app :theme-changed theme)
+                                                          (state/set-state! :plugin/selected-theme url))))
+
+                                (.on "reset-custom-theme" (fn [^js themes]
+                                                            (let [themes       (bean/->clj themes)
+                                                                  custom-theme (dissoc themes :mode)
+                                                                  mode         (:mode themes)]
+                                                              (state/set-custom-theme! {:light (if (nil? (:light custom-theme)) {:mode "light"} (:light custom-theme))
+                                                                                        :dark  (if (nil? (:dark custom-theme)) {:mode "dark"} (:dark custom-theme))})
+                                                              (state/set-theme-mode! mode))))
+
+                                (.on "settings-changed" (fn [id ^js settings]
+                                                          (let [id (keyword id)]
+                                                            (when (and settings
+                                                                       (contains? (:plugin/installed-plugins @state/state) id))
+                                                              (update-plugin-settings-state id (bean/->clj settings)))))))
+
+              default-plugins (get-user-default-plugins)
+
+              _               (.register js/LSPluginCore (bean/->js (if (seq default-plugins) default-plugins [])) true)])
+
+      (p/then
+       (fn []
+         (state/set-state! :plugin/indicator-text "END")
+         (callback)))
+      (p/catch
+       (fn [^js e]
+         (log/error :setup-plugin-system-error e)
+         (state/set-state! :plugin/indicator-text (str "Fatal: " e))))))
 
 (defn setup!
   "setup plugin core handler"

+ 15 - 13
src/main/frontend/handler/repo.cljs

@@ -61,7 +61,7 @@
                               "org" (rc/inline "contents.org")
                               "markdown" (rc/inline "contents.md")
                               "")]
-        (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" pages-dir))
+        (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir pages-dir))
                 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)))))))
@@ -73,7 +73,7 @@
         path (str config/app-name "/" config/custom-css-file)
         file-path (str "/" path)
         default-content ""]
-    (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
+    (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join 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)))))
@@ -84,7 +84,7 @@
   (let [repo-dir (config/get-repo-dir repo-url)
         path (str (config/get-pages-directory) "/how_to_make_dummy_notes.md")
         file-path (str "/" path)]
-    (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" (config/get-pages-directory)))
+    (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir (config/get-pages-directory)))
             _file-exists? (fs/create-if-not-exists repo-url repo-dir file-path content)]
       (file-handler/reset-file! repo-url path content))))
 
@@ -110,14 +110,14 @@
 
                     :else
                     default-content)
-          path (str (config/get-journals-directory) "/" file-name "."
-                    (config/get-file-extension format))
+          path (util/safe-path-join (config/get-journals-directory) (str file-name "."
+                                                                         (config/get-file-extension format)))
           file-path (str "/" path)
           page-exists? (db/entity repo-url [:block/name (util/page-name-sanity-lc title)])
           empty-blocks? (db/page-empty? repo-url (util/page-name-sanity-lc 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/get-journals-directory)))
+                _ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir (config/get-journals-directory)))
                 file-exists? (fs/file-exists? repo-dir file-path)]
           (when-not file-exists?
             (p/let [_ (file-handler/reset-file! repo-url path content)]
@@ -133,9 +133,9 @@
   ([repo-url encrypted?]
    (spec/validate :repos/url repo-url)
    (let [repo-dir (config/get-repo-dir repo-url)]
-     (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
-             _ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name "/" config/recycle-dir))
-             _ (fs/mkdir-if-not-exists (str repo-dir "/" (config/get-journals-directory)))
+     (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir config/app-name))
+             _ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir (str config/app-name "/" config/recycle-dir)))
+             _ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir (config/get-journals-directory)))
              _ (file-handler/create-metadata-file repo-url encrypted?)
              _ (create-config-file-if-not-exists repo-url)
              _ (create-contents-file repo-url)
@@ -277,10 +277,12 @@
   (spec/validate :repos/url repo-url)
   (route-handler/redirect-to-home!)
   (state/set-parsing-state! {:graph-loading? true})
-  (let [config (or (state/get-config repo-url)
-                   (when-let [content (some-> (first (filter #(= (config/get-config-path repo-url) (:file/path %)) nfs-files))
+  (let [config (or (when-let [content (some-> (first (filter #(= (config/get-config-path repo-url) (:file/path %)) nfs-files))
                                               :file/content)]
-                     (common-handler/read-config content)))
+                     (common-handler/read-config content))
+                   (state/get-config repo-url))
+        ;; NOTE: Use config while parsing. Make sure it's the corrent journal title format
+        _ (state/set-config! repo-url config)
         relate-path-fn (fn [m k]
                          (some-> (get m k)
                                  (string/replace (js/decodeURI (config/get-local-dir repo-url)) "")))
@@ -386,7 +388,7 @@
   [repo]
   (p/let [_ (state/set-db-restoring! true)
           _ (db/restore-graph! repo)]
-         (file-handler/restore-config! repo false)
+         (file-handler/restore-config! repo)
          ;; Don't have to unlisten the old listerner, as it will be destroyed with the conn
          (db/listen-and-persist! repo)
          (ui-handler/add-style-if-exists!)

+ 2 - 1
src/main/frontend/handler/ui.cljs

@@ -125,7 +125,8 @@
   []
   (when-let [style (or
                     (state/get-custom-css-link)
-                    (db-model/get-custom-css)
+                    (some-> (db-model/get-custom-css)
+                            (config/expand-relative-assets-path))
                     ;; (state/get-custom-css-link)
 )]
     (util/add-style! style)))

+ 10 - 33
src/main/frontend/mixins.cljs

@@ -1,7 +1,7 @@
 (ns frontend.mixins
   (:require [rum.core :as rum]
             [goog.dom :as dom]
-            [frontend.util :refer [profile]]
+            [frontend.util :refer [profile] :as util]
             [frontend.state :as state])
   (:import [goog.events EventHandler]))
 
@@ -26,43 +26,20 @@
      (detach state)
      (dissoc state ::event-handler))})
 
-;; (defn timeout-mixin
-;;   "The setTimeout mixin."
-;;   [name t f]
-;;   {:will-mount
-;;    (fn [state]
-;;      (assoc state name (util/set-timeout t f)))
-;;    :will-unmount
-;;    (fn [state]
-;;      (let [timeout (get state name)]
-;;        (util/clear-timeout timeout)
-;;        (dissoc state name)))})
-
-;; (defn interval-mixin
-;;   "The setInterval mixin."
-;;   [name t f]
-;;   {:will-mount
-;;    (fn [state]
-;;      (assoc state name (util/set-interval t f)))
-;;    :will-unmount
-;;    (fn [state]
-;;      (when-let [interval (get state name)]
-;;        (util/clear-interval interval))
-;;      (dissoc state name))})
-
 (defn hide-when-esc-or-outside
   [state & {:keys [on-hide node visibilitychange? outside?]}]
   (try
     (let [dom-node (rum/dom-node state)]
       (when-let [dom-node (or node dom-node)]
-        (or (false? outside?)
-            (listen state js/window "mousedown"
-                    (fn [e]
-                      (let [target (.. e -target)]
-                        ;; If the click target is outside of current node
-                        (when (and (not (dom/contains dom-node target))
-                                   (not (.contains (.-classList target) "ignore-outside-event")))
-                          (on-hide state e :click))))))
+        (let [click-fn (fn [e]
+                         (let [target (.. e -target)]
+                           ;; If the click target is outside of current node
+                           (when (and
+                                  (not (dom/contains dom-node target))
+                                  (not (.contains (.-classList target) "ignore-outside-event")))
+                             (on-hide state e :click))))]
+          (when-not (false? outside?)
+            (listen state js/window "mousedown" click-fn)))
         (listen state js/window "keydown"
                 (fn [e]
                   (case (.-keyCode e)

+ 3 - 2
src/main/frontend/mobile/core.cljs

@@ -2,6 +2,7 @@
   (:require ["@capacitor/app" :refer [^js App]]
             ["@capacitor/keyboard" :refer [^js Keyboard]]
             [clojure.string :as string]
+            [promesa.core :as p]
             [frontend.fs.capacitor-fs :as mobile-fs]
             [frontend.handler.editor :as editor-handler]
             [frontend.mobile.deeplink :as deeplink]
@@ -20,8 +21,8 @@
 (defn- ios-init
   "Initialize iOS-specified event listeners"
   []
-  (let [path (mobile-fs/iOS-ensure-documents!)]
-    (println "iOS container path: " path))
+  (p/let [path (mobile-fs/iOS-ensure-documents!)]
+    (println "iOS container path: " (js->clj path)))
 
   (state/pub-event! [:validate-appId])
   

+ 5 - 9
src/main/frontend/modules/file/core.cljs

@@ -123,15 +123,11 @@
                        (date/date->file-name journal-page?)
                        (-> (or (:block/original-name page) (:block/name page))
                            (util/file-name-sanity)))
-            path (str
-                  (if journal-page?
-                    (config/get-journals-directory)
-                    (config/get-pages-directory))
-                  "/"
-                  filename
-                  "."
-                  (if (= format "markdown") "md" format))
-            file-path (config/get-file-path repo path)
+            sub-dir (if journal-page?
+                      (config/get-journals-directory)
+                      (config/get-pages-directory))
+            ext (if (= format "markdown") "md" format)
+            file-path (config/get-page-file-path repo sub-dir filename ext)
             file {:file/path file-path}
             tx [{:file/path file-path}
                 {:block/name (:block/name page)

+ 27 - 7
src/main/frontend/modules/outliner/core.cljs

@@ -158,12 +158,24 @@
                                       db-schema/retract-attributes)))))
 
         (when-let [e (:block/page block-entity)]
-          (let [m {:db/id (:db/id e)
+          (let [m' {:db/id (:db/id e)
                    :block/updated-at (util/time-ms)}
-                m (if (:block/created-at e)
-                    m
-                    (assoc m :block/created-at (util/time-ms)))]
-            (swap! txs-state conj m))
+                m' (if (:block/created-at e)
+                    m'
+                    (assoc m' :block/created-at (util/time-ms)))
+                m' (if (or (:block/pre-block? block-entity)
+                           (:block/pre-block? m))
+                     (let [properties (:block/properties m)
+                           alias (set (:alias properties))
+                           tags (set (:tags properties))
+                           alias (map (fn [p] {:block/name (util/page-name-sanity-lc p)}) alias)
+                           tags (map (fn [p] {:block/name (util/page-name-sanity-lc p)}) tags)]
+                       (assoc m'
+                              :block/alias alias
+                              :block/tags tags
+                              :block/properties properties))
+                     m')]
+            (swap! txs-state conj m'))
           (remove-orphaned-page-refs! (:db/id block-entity) txs-state old-refs new-refs)))
 
       (swap! txs-state conj (dissoc m :db/other-tx))
@@ -194,8 +206,16 @@
                                                  (assoc :block/left parent))))
                                            immediate-children)))
                     txs))
-                txs)]
-      (swap! txs-state concat txs)
+                txs)
+          page-tx (let [block (db/entity [:block/uuid block-id])]
+                    (when (:block/pre-block? block)
+                      (let [id (:db/id (:block/page block))]
+                        [[:db/retract id :block/properties]
+                         [:db/retract id :block/properties-order]
+                         [:db/retract id :block/alias]
+                         [:db/retract id :block/tags]])))]
+      (swap! txs-state concat txs page-tx)
+      (util/pprint @txs-state)
       block-id))
 
   (-get-children [this]

+ 12 - 8
src/main/frontend/modules/outliner/file.cljs

@@ -25,14 +25,18 @@
   [repo page-db-id]
   (let [page-block (db/pull repo '[*] page-db-id)
         page-db-id (:db/id page-block)
-        blocks (model/get-page-blocks-no-cache repo (:block/name page-block))]
-    (when-not (and (= 1 (count blocks))
-                   (string/blank? (:block/content (first blocks)))
-                   (nil? (:block/file page-block)))
-      (let [tree (tree/blocks->vec-tree repo blocks (:block/name page-block))]
-        (if page-block
-          (file/save-tree page-block tree)
-          (js/console.error (str "can't find page id: " page-db-id)))))))
+        blocks-count (model/get-page-blocks-count repo page-db-id)]
+    (if (and (> blocks-count 500)
+             (not (state/input-idle? repo :diff 3000)))           ; long page
+      (async/put! write-chan [repo page-db-id])
+      (let [blocks (model/get-page-blocks-no-cache repo (:block/name page-block))]
+        (when-not (and (= 1 (count blocks))
+                       (string/blank? (:block/content (first blocks)))
+                       (nil? (:block/file page-block)))
+          (let [tree (tree/blocks->vec-tree repo blocks (:block/name page-block))]
+            (if page-block
+              (file/save-tree page-block tree)
+              (js/console.error (str "can't find page id: " page-db-id)))))))))
 
 (defn write-files!
   [pages]

+ 11 - 7
src/main/frontend/modules/outliner/pipeline.cljs

@@ -21,7 +21,7 @@
 ;; 1. For each changed block, new-refs = its page + :block/refs + parents :block/refs
 ;; 2. Its children' block/path-refs might need to be updated too.
 (defn compute-block-path-refs
-  [tx-meta blocks]
+  [{:keys [tx-meta]} blocks]
   (let [repo (state/get-current-repo)
         blocks (remove :block/name blocks)]
     (when (:outliner-op tx-meta)
@@ -41,13 +41,15 @@
                             refs-changed? (not= old-refs new-refs)
                             children (db-model/get-block-children-ids repo (:block/uuid block))
                             children-refs (map (fn [id]
-                                                 {:db/id (:db/id (db/entity [:block/uuid id]))
-                                                  :block/path-refs (concat
-                                                                    (map :db/id (:block/path-refs (db/entity id)))
-                                                                    new-refs)}) children)]
+                                                 (let [entity (db/entity [:block/uuid id])]
+                                                   {:db/id (:db/id entity)
+                                                    :block/path-refs (concat
+                                                                      (map :db/id (:block/path-refs entity))
+                                                                      new-refs)})) children)]
                         (swap! *computed-ids set/union (set (cons (:block/uuid block) children)))
                         (util/concat-without-nil
-                         [(when (and refs-changed? (seq new-refs))
+                         [(when (and (seq new-refs)
+                                     refs-changed?)
                             {:db/id (:db/id block)
                              :block/path-refs new-refs})]
                          children-refs))))
@@ -61,7 +63,9 @@
                (not (:compute-new-refs? tx-meta)))
       (let [{:keys [pages blocks]} (ds-report/get-blocks-and-pages tx-report)
             repo (state/get-current-repo)
-            refs-tx (set (compute-block-path-refs (:tx-meta tx-report) blocks))
+            refs-tx (util/profile
+                     "Compute path refs: "
+                     (set (compute-block-path-refs tx-report blocks)))
             truncate-refs-tx (map (fn [m] [:db/retract (:db/id m) :block/path-refs]) refs-tx)
             tx (util/concat-without-nil truncate-refs-tx refs-tx)
             tx-report' (if (seq tx)

+ 31 - 27
src/main/frontend/modules/outliner/tree.cljs

@@ -66,8 +66,8 @@
                  root-block (assoc root-block :block/children result)]
              [root-block])))))))
 
-(defn- tree [flat-nodes root-id]
-  (let [children (group-by :block/parent flat-nodes)
+(defn- tree [parent->children root]
+  (let [root-id (:db/id root)
         nodes (fn nodes [parent-id level]
                 (mapv (fn [b]
                         (let [b' (assoc b :block/level (inc level))
@@ -75,35 +75,39 @@
                           (if (seq children)
                             (assoc b' :block/children children)
                             b')))
-                      (get children {:db/id parent-id})))]
-    (nodes root-id 1)))
+                      (let [parent {:db/id parent-id}]
+                        (-> (get parent->children parent)
+                            (model/try-sort-by-left parent)))))
+        children (nodes root-id 1)
+        root' (assoc root :block/level 1)]
+    (if (seq children)
+      (assoc root' :block/children children)
+      root')))
+
+(defn block-entity->map
+  [e]
+  {:db/id (:db/id e)
+   :block/uuid (:block/uuid e)
+   :block/parent {:db/id (:db/id (:block/parent e))}
+   :block/left {:db/id (:db/id (:block/left e))}
+   :block/page (:block/page e)
+   :block/refs (:block/refs e)})
+
+(defn filter-top-level-blocks
+  [blocks]
+  (let [id->blocks (zipmap (map :db/id blocks) blocks)]
+    (filter #(nil?
+              (id->blocks
+               (:db/id (:block/parent (id->blocks (:db/id %)))))) blocks)))
 
 (defn non-consecutive-blocks->vec-tree
   "`blocks` need to be in the same page."
   [blocks]
-  (let [blocks (map (fn [e] {:db/id (:db/id e)
-                             :block/uuid (:block/uuid e)
-                             :block/parent {:db/id (:db/id (:block/parent e))}
-                             :block/page {:db/id (:db/id (:block/page e))}}) blocks)
-        blocks (model/sort-page-random-blocks blocks)
-        id->parent (zipmap (map :db/id blocks)
-                           (map (comp :db/id :block/parent) blocks))
-        top-level-ids (set (remove #(id->parent (id->parent %)) (map :db/id blocks)))
-        ;; Separate blocks into parent and children groups [parent-children, parent-children]
-        blocks' (loop [blocks blocks
-                       result []]
-                  (if-let [block (first blocks)]
-                    (if (top-level-ids (:db/id block))
-                      (let [block' (assoc block :block/level 1)]
-                        (recur (rest blocks) (conj result [block'])))
-                      (recur (rest blocks) (conj (vec (butlast result))
-                                                 (conj (last result) block))))
-                    result))]
-    (map (fn [[parent & children]]
-           (if (seq children)
-             (assoc parent :block/children
-                    (tree children (:db/id parent)))
-             parent)) blocks')))
+  (let [blocks (map block-entity->map blocks)
+        top-level-blocks (filter-top-level-blocks blocks)
+        top-level-blocks' (model/try-sort-by-left top-level-blocks (:block/parent (first top-level-blocks)))
+        parent->children (group-by :block/parent blocks)]
+    (map #(tree parent->children %) top-level-blocks')))
 
 (defn- sort-blocks-aux
   [parents parent-groups]

+ 10 - 8
src/main/frontend/modules/shortcut/data_helper.cljs

@@ -46,14 +46,16 @@
 
 (defn normalize-user-keyname
   [k]
-  (some-> k
-          (util/safe-lower-case)
-          (str/replace #";+" "semicolon")
-          (str/replace #"=+" "equals")
-          (str/replace #"~+" "dash")
-          (str/replace "[" "open-square-bracket")
-          (str/replace "]" "close-square-bracket")
-          (str/replace "'" "single-quote")))
+  (let [keynames {";" "semicolon"
+                   "=" "equals"
+                   "-" "dash"
+                   "[" "open-square-bracket"
+                   "]" "close-square-bracket"
+                   "'" "single-quote"}]
+    (some-> k
+            (util/safe-lower-case)
+            (str/replace #"[;=-\[\]']" (fn [s]
+                                         (get keynames s))))))
 
 ;; returns a vector to preserve order
 (defn binding-by-category [name]

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

@@ -1331,6 +1331,7 @@
              :command.graph/remove                   "Bir grafiği kaldır"
              :command.graph/add                      "Grafik ekle"
              :command.graph/save                     "Mevcut grafiği diske kaydet"
+             :command.graph/re-index                 "Mevcut grafiği yeniden oluştur"
              :command.command/run                    "Git komutunu çalıştır"
              :command.go/home                        "Ana sayfaya git"
              :command.go/all-pages                   "Bütün sayfalara git"

+ 41 - 15
src/main/frontend/state.cljs

@@ -133,6 +133,7 @@
      :selection/direction                   :down
      :custom-context-menu/show?             false
      :custom-context-menu/links             nil
+     :custom-context-menu/position          nil
 
      ;; pages or blocks in the right sidebar
      ;; It is a list of `[repo db-id block-type block-data]` 4-tuple
@@ -368,9 +369,11 @@
                  (get (sub-config) (get-current-repo))))))
 
 (defn enable-journals?
-  [repo]
-  (not (false? (:feature/enable-journals?
-                (get (sub-config) repo)))))
+  ([]
+   (enable-journals? (get-current-repo)))
+  ([repo]
+   (not (false? (:feature/enable-journals?
+                 (get (sub-config) repo))))))
 
 (defn enable-flashcards?
   ([]
@@ -691,13 +694,25 @@
   []
   (:selection/blocks @state))
 
-(defn get-selection-block-ids
-  []
-  (->> (sub :selection/blocks)
+(defn- get-selected-block-ids
+  [blocks]
+  (->> blocks
        (keep #(when-let [id (dom/attr % "blockid")]
                 (uuid id)))
        (distinct)))
 
+(defn get-selection-block-ids
+  []
+  (get-selected-block-ids (get-selection-blocks)))
+
+(defn sub-block-selected?
+  [block-uuid]
+  (rum/react
+   (rum/derived-atom [state] [::select-block block-uuid]
+     (fn [state]
+       (contains? (set (get-selected-block-ids (:selection/blocks state)))
+                  block-uuid)))))
+
 (defn get-selection-start-block-or-first
   []
   (or (get-selection-start-block)
@@ -715,7 +730,6 @@
 
 (defn conj-selection-block!
   [block direction]
-  (dom/add-class! block "selected noselect")
   (swap! state assoc
          :selection/mode true
          :selection/blocks (-> (conj (vec (:selection/blocks @state)) block)
@@ -724,10 +738,18 @@
 
 (defn drop-last-selection-block!
   []
-  (let [last-block (peek (vec (:selection/blocks @state)))]
+  (let [direction (:selection/direction @state)
+        up? (= direction :up)
+        blocks (:selection/blocks @state)
+        last-block (if up?
+                     (first blocks)
+                     (peek (vec blocks)))
+        blocks' (if up?
+                  (rest blocks)
+                  (pop (vec blocks)))]
     (swap! state assoc
            :selection/mode true
-           :selection/blocks (pop (vec (:selection/blocks @state))))
+           :selection/blocks blocks')
     last-block))
 
 (defn get-selection-direction
@@ -735,16 +757,18 @@
   (:selection/direction @state))
 
 (defn show-custom-context-menu!
-  [links]
+  [links position]
   (swap! state assoc
          :custom-context-menu/show? true
-         :custom-context-menu/links links))
+         :custom-context-menu/links links
+         :custom-context-menu/position position))
 
 (defn hide-custom-context-menu!
   []
   (swap! state assoc
          :custom-context-menu/show? false
-         :custom-context-menu/links nil))
+         :custom-context-menu/links nil
+         :custom-context-menu/position nil))
 
 (defn toggle-navigation-item-collapsed!
   [item]
@@ -1327,12 +1351,13 @@
         (>= (- now last-time) 3000)))))
 
 (defn input-idle?
-  [repo]
+  [repo & {:keys [diff]
+           :or {diff 1000}}]
   (when repo
     (or
       (when-let [last-time (get-in @state [:editor/last-input-time repo])]
         (let [now (util/time-ms)]
-          (>= (- now last-time) 500)))
+          (>= (- now last-time) diff)))
       ;; not in editing mode
       (not (get-edit-input-id)))))
 
@@ -1635,7 +1660,8 @@
 (defn edit-in-query-or-refs-component
   []
   (let [config (last (get-editor-args))]
-    (or (:custom-query? config) (:ref? config))))
+    {:custom-query? (:custom-query? config)
+     :ref? (:ref? config)}))
 
 (defn set-auth-id-token
   [id-token]

+ 34 - 39
src/main/frontend/ui.cljs

@@ -84,7 +84,7 @@
 
 (rum/defc dropdown-content-wrapper [state content class]
   (let [class (or class
-                  (util/hiccup->class "origin-top-right.absolute.right-0.mt-2.rounded-md.shadow-lg"))]
+                  (util/hiccup->class "origin-top-right.absolute.right-0.mt-2"))]
     [:div.dropdown-wrapper
      {:class (str class " "
                   (case state
@@ -109,18 +109,38 @@
         (when @open?
           (dropdown-content-wrapper dropdown-state modal-content modal-class))))]))
 
+;; `sequence` can be a list of symbols, a list of strings, or a string
+(defn render-keyboard-shortcut [sequence]
+  (let [sequence (if (string? sequence)
+                   (-> sequence ;; turn string into sequence
+                       (string/trim)
+                       (string/lower-case)
+                       (string/split  #" |\+"))
+                   sequence)]
+    [:span.keyboard-shortcut
+     (map-indexed (fn [i key]
+                    [:code {:key i}
+                   ;; Display "cmd" rather than "meta" to the user to describe the Mac
+                   ;; mod key, because that's what the Mac keyboards actually say.
+                     (if (or (= :meta key) (= "meta" key))
+                       (util/meta-key-name)
+                       (name key))])
+                  sequence)]))
+
 (rum/defc menu-link
-  [options child]
-  [:a.block.px-4.py-2.text-sm.transition.ease-in-out.duration-150.cursor.menu-link
+  [options child shortcut]
+  [:a.flex.justify-between.px-4.py-2.text-sm.transition.ease-in-out.duration-150.cursor.menu-link
    options
-   child])
+   [:span child]
+   (when shortcut
+     [:span.ml-1 (render-keyboard-shortcut shortcut)])])
 
 (rum/defc dropdown-with-links
   [content-fn links {:keys [links-header links-footer] :as opts}]
   (dropdown
    content-fn
    (fn [{:keys [close-fn]}]
-     [:div.py-1.rounded-md.shadow-xs
+     [:.menu-links-wrapper
       (when links-header links-header)
       (for [{:keys [options title icon hr hover-detail item]} (if (fn? links) (links) links)]
         (let [new-options
@@ -138,16 +158,16 @@
                            [:div {:style {:margin-right "8px"
                                           :margin-left  "4px"}} title]]))]
           (if hr
-            [:hr.my-1 {:key "dropdown-hr"}]
+            [:hr.menu-separator {:key "dropdown-hr"}]
             (rum/with-key
-              (menu-link new-options child)
+              (menu-link new-options child nil)
               title))))
       (when links-footer links-footer)])
    opts))
 
 (defn button
   [text & {:keys [background href class intent on-click small? large?]
-           :or {small? false large? false}
+           :or   {small? false large? false}
            :as   option}]
   (let [klass (when-not intent ".bg-indigo-600.hover:bg-indigo-700.focus:border-indigo-700.active:bg-indigo-700.text-center")
         klass (if background (string/replace klass "indigo" background) klass)
@@ -418,7 +438,7 @@
                                        (if (and (gobj/get e "shiftKey") on-shift-chosen)
                                          (on-shift-chosen item)
                                          (on-chosen item)))}
-                     (if item-render (item-render item chosen?) item)))]]
+                     (if item-render (item-render item chosen?) item) nil))]]
 
              (if get-group-name
                (if-let [group-name (get-group-name item)]
@@ -445,24 +465,6 @@
       {:class       (if on? (if small? "translate-x-4" "translate-x-5") "translate-x-0")
        :aria-hidden "true"}]]]))
 
-;; `sequence` can be a list of symbols, a list of strings, or a string
-(defn render-keyboard-shortcut [sequence]
-  (let [sequence (if (string? sequence)
-                   (-> sequence ;; turn string into sequence
-                       (string/trim)
-                       (string/lower-case)
-                       (string/split  #" |\+"))
-                   sequence)]
-    [:span.keyboard-shortcut
-     (map-indexed (fn [i key]
-                    [:code {:key i}
-                   ;; Display "cmd" rather than "meta" to the user to describe the Mac
-                   ;; mod key, because that's what the Mac keyboards actually say.
-                     (if (or (= :meta key) (= "meta" key))
-                       (util/meta-key-name)
-                       (name key))])
-                  sequence)]))
-
 (defn keyboard-shortcut-from-config [shortcut-name]
   (let [default-binding (:binding (get shortcut-config/all-default-keyboard-shortcuts shortcut-name))
         custom-binding  (when (state/shortcuts) (get (state/shortcuts) shortcut-name))
@@ -936,24 +938,17 @@
   ([content-fn]
    (lazy-visible content-fn nil))
   ([content-fn {:keys [trigger-once? _debug-id]
-                :or {trigger-once? false}}]
+                :or {trigger-once? true}}]
    (if (or (util/mobile?) (mobile-util/native-platform?))
      (content-fn)
      (let [[visible? set-visible!] (rum/use-state false)
-           [last-changed-time set-last-changed-time!] (rum/use-state nil)
            inViewState (useInView #js {:rootMargin "100px"
                                        :triggerOnce trigger-once?
                                        :onChange (fn [in-view? entry]
-                                                   (let [self-top (.-top (.-boundingClientRect entry))
-                                                         time' (util/time-ms)]
-                                                     (when (and
-                                                            (or (and (not visible?) in-view?)
-                                                                ;; hide only the components below the current top for better ux
-                                                                (and visible? (not in-view?) (> self-top 0)))
-                                                            (or (nil? last-changed-time)
-                                                                (and (some? last-changed-time)
-                                                                     (> (- time' last-changed-time) 50))))
-                                                       (set-last-changed-time! time')
+                                                   (let [self-top (.-top (.-boundingClientRect entry))]
+                                                     (when (or (and (not visible?) in-view?)
+                                                               ;; hide only the components below the current top for better ux
+                                                               (and visible? (not in-view?) (> self-top 0)))
                                                        (set-visible! in-view?))))})
            ref (.-ref inViewState)]
        (lazy-visible-inner visible? content-fn ref)))))

+ 0 - 5
src/main/frontend/ui.css

@@ -270,11 +270,6 @@ html.is-mobile {
   }
 }
 
-.dropdown-wrapper {
-  background-color: var(--ls-primary-background-color, #fff);
-  min-width: 12rem;
-}
-
 .dropdown-caret {
   display: inline-block;
   width: 0;

+ 7 - 11
src/main/frontend/util.cljc

@@ -10,6 +10,7 @@
             [cljs-bean.core :as bean]
             [cljs-time.coerce :as tc]
             [cljs-time.core :as t]
+            [clojure.pprint]
             [dommy.core :as d]
             [frontend.mobile.util :refer [native-platform?]]
             [logseq.graph-parser.util :as gp-util]
@@ -337,10 +338,6 @@
    (defn stop-propagation [e]
      (when e (.stopPropagation e))))
 
-#?(:cljs
-   (defn cur-doc-top []
-     (.. js/document -documentElement -scrollTop)))
-
 #?(:cljs
    (defn element-top [elem top]
      (when elem
@@ -487,6 +484,10 @@
   (if (string? s)
     (string/lower-case s) s))
 
+#?(:cljs
+   (defn safe-path-join [prefix & paths]
+     (apply node-path.join (cons prefix paths))))
+
 (defn trim-safe
   [s]
   (when s
@@ -891,11 +892,6 @@
      [string]
      (some-> string str (js/encodeURIComponent) (.replace "+" "%20"))))
 
-#?(:cljs
-   (defn url-decode
-     [string]
-     (some-> string str (js/decodeURIComponent))))
-
 (def windows-reserved-chars #"[:\\*\\?\"<>|]+")
 
 #?(:cljs
@@ -1075,6 +1071,8 @@
   (= (get-relative-path "a/b/c/d/g.org" "a/b/c/e/f.org")
      "../e/f.org"))
 
+(defn keyname [key] (str (namespace key) "/" (name key)))
+
 #?(:cljs
    (defn select-highlight!
      [blocks]
@@ -1087,8 +1085,6 @@
      (doseq [block blocks]
        (d/remove-class! block "selected" "noselect"))))
 
-(defn keyname [key] (str (namespace key) "/" (name key)))
-
 (defn batch [in max-time handler buf-atom]
   (async/go-loop [buf buf-atom t (async/timeout max-time)]
     (let [[v p] (async/alts! [in t])]

+ 15 - 0
src/main/frontend/utils.js

@@ -310,5 +310,20 @@ export const nodePath = Object.assign({}, path, {
   extname (input) {
     input = toPosixPath(input)
     return path.extname(input)
+  },
+
+  join (input, ...paths) {
+    let orURI = null
+
+    try {
+      orURI = new URL(input)
+      input = input.replace(orURI.protocol + '//', '')
+        .replace(orURI.protocol, '')
+        .replace(/^\/+/, '/')
+    } catch (_e) {}
+
+    input = path.join(input, ...paths)
+
+    return (orURI ? (orURI.protocol + '//') : '') + input
   }
 })

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

@@ -1,3 +1,3 @@
 (ns frontend.version)
 
-(defonce version "0.8.0")
+(defonce version "0.8.1")

+ 27 - 6
src/main/logseq/api.cljs

@@ -102,8 +102,16 @@
          :preferred-start-of-week (state/get-start-of-week)
          :current-graph         (state/get-current-repo)
          :show-brackets         (state/show-brackets?)
+         :enabled-journals      (state/enable-journals?)
+         :enabled-flashcards    (state/enable-flashcards?)
          :me                    (state/get-me)}))))
 
+(def ^:export get_current_graph_configs
+  (fn []
+    (some-> (get (:config @state/state) (state/get-current-repo))
+            (normalize-keyword-for-json)
+            (bean/->js))))
+
 (def ^:export get_current_graph
   (fn []
     (when-let [repo (state/get-current-repo)]
@@ -442,6 +450,9 @@
   [block-uuid]
   (editor-handler/open-block-in-sidebar! (uuid block-uuid)))
 
+(defn ^:export new_block_uuid []
+  (str (db/new-block-id)))
+
 (def ^:export select_block
   (fn [block-uuid]
     (when-let [block (db-model/get-block-by-uuid block-uuid)]
@@ -456,8 +467,16 @@
 
 (def ^:export insert_block
   (fn [block-uuid-or-page-name content ^js opts]
-    (let [{:keys [before sibling isPageBlock properties]} (bean/->clj opts)
+    (let [{:keys [before sibling isPageBlock customUUID properties]} (bean/->clj opts)
           page-name (and isPageBlock block-uuid-or-page-name)
+          custom-uuid (or customUUID (:id properties))
+          _ (when (not (string/blank? custom-uuid))
+              (when-not (util/uuid-string? custom-uuid)
+                (throw (js/Error.
+                        (util/format "Illegal custom block UUID pattern (%s)." custom-uuid))))
+              (when (db-model/query-block-by-uuid custom-uuid)
+                (throw (js/Error.
+                        (util/format "Custom block UUID already exists (%s)." custom-uuid)))))
           block-uuid (if isPageBlock nil (uuid block-uuid-or-page-name))
           block-uuid' (if (and (not sibling) before block-uuid)
                         (let [block (db/entity [:block/uuid block-uuid])
@@ -477,11 +496,13 @@
                     before?)
           new-block (editor-handler/api-insert-new-block!
                       content
-                      {:block-uuid block-uuid'
-                       :sibling?   sibling?
-                       :before?    before?
-                       :page       page-name
-                       :properties properties})]
+                      {:block-uuid  block-uuid'
+                       :sibling?    sibling?
+                       :before?     before?
+                       :page        page-name
+                       :custom-uuid custom-uuid
+                       :properties  (merge properties
+                                           (when custom-uuid {:id custom-uuid}))})]
       (bean/->js (normalize-keyword-for-json new-block)))))
 
 (def ^:export insert_batch_block

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