Explorar o código

Merge branch 'master' into mobile

charlie %!s(int64=3) %!d(string=hai) anos
pai
achega
0d16b12885
Modificáronse 61 ficheiros con 850 adicións e 1264 borrados
  1. 3 0
      .github/workflows/build-desktop-release.yml
  2. 16 0
      .github/workflows/build.yml
  3. 0 5
      cypress/fixtures/example.json
  4. 0 73
      cypress/integration/app/basic.cljs
  5. 0 49
      cypress/integration/app/template.cljs
  6. 0 46
      cypress/integration/app/util.cljs
  7. 0 7
      cypress/plugins/index.js
  8. 0 44
      cypress/support/commands.js
  9. 0 22
      cypress/support/index.js
  10. 302 0
      e2e-tests/basic.spec.ts
  11. 50 0
      e2e-tests/utils.ts
  12. 1 1
      libs/package.json
  13. 2 0
      libs/src/LSPlugin.ts
  14. 4 5
      package.json
  15. 11 0
      playwright.config.ts
  16. 1 0
      resources/forge.config.js
  17. 0 758
      resources/js/lsplugin.core.js
  18. 2 2
      resources/package.json
  19. 10 11
      src/electron/electron/git.cljs
  20. 1 0
      src/electron/electron/handler.cljs
  21. 4 0
      src/electron/electron/state.cljs
  22. 7 1
      src/main/electron/listener.cljs
  23. 3 0
      src/main/frontend/commands.cljs
  24. 17 13
      src/main/frontend/components/block.cljs
  25. 1 1
      src/main/frontend/components/editor.cljs
  26. 1 0
      src/main/frontend/components/journal.cljs
  27. 31 30
      src/main/frontend/components/page.cljs
  28. 2 1
      src/main/frontend/components/plugins.cljs
  29. 4 1
      src/main/frontend/components/repo.cljs
  30. 3 2
      src/main/frontend/components/settings.cljs
  31. 2 0
      src/main/frontend/components/sidebar.cljs
  32. 17 0
      src/main/frontend/components/widgets.cljs
  33. 11 0
      src/main/frontend/config.cljs
  34. 1 1
      src/main/frontend/db/default.cljs
  35. 31 21
      src/main/frontend/db/model.cljs
  36. 1 0
      src/main/frontend/db/query_dsl.cljs
  37. 5 5
      src/main/frontend/extensions/srs.cljs
  38. 29 7
      src/main/frontend/format/block.cljs
  39. 1 1
      src/main/frontend/fs/node.cljs
  40. 2 2
      src/main/frontend/fs/watcher_handler.cljs
  41. 20 1
      src/main/frontend/handler.cljs
  42. 129 105
      src/main/frontend/handler/editor.cljs
  43. 9 2
      src/main/frontend/handler/events.cljs
  44. 6 5
      src/main/frontend/handler/file.cljs
  45. 2 1
      src/main/frontend/handler/graph.cljs
  46. 2 2
      src/main/frontend/handler/notification.cljs
  47. 14 14
      src/main/frontend/handler/page.cljs
  48. 5 3
      src/main/frontend/handler/route.cljs
  49. 0 5
      src/main/frontend/handler/ui.cljs
  50. 1 1
      src/main/frontend/handler/web/nfs.cljs
  51. 26 3
      src/main/frontend/modules/outliner/core.cljs
  52. 13 2
      src/main/frontend/modules/outliner/datascript.cljc
  53. 3 2
      src/main/frontend/modules/outliner/file.cljs
  54. 3 4
      src/main/frontend/modules/outliner/pipeline.cljs
  55. 1 1
      src/main/frontend/modules/shortcut/config.cljs
  56. 8 1
      src/main/frontend/state.cljs
  57. 10 0
      src/main/frontend/util.cljc
  58. 3 0
      src/main/frontend/util/keycode.cljs
  59. 1 1
      src/main/frontend/version.cljs
  60. 16 0
      src/main/logseq/api.cljs
  61. 2 2
      src/test/frontend/db/query_dsl_test.cljs

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

@@ -158,6 +158,9 @@ jobs:
       - name: Build/Release Electron app
         run: yarn electron:make
         working-directory: ./static
+        env:
+          CSC_LINK: ${{ secrets.CSC_LINK }}
+          CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
 
       - name: Change Artifact Name
         run: Get-ChildItem  static\out\make\squirrel.windows\x64\*.exe | Rename-Item -NewName Logseq-win64.exe

+ 16 - 0
.github/workflows/build.yml

@@ -79,3 +79,19 @@ jobs:
         run: |
           yarn cljs:test
           node static/tests.js
+
+      - name: Run Playwright test
+        if: github.event_name == 'pull_request'
+        run: |
+          yarn release
+          (cd static && yarn install && yarn rebuild:better-sqlite3)
+          xvfb-run -- yarn e2e-test
+        env:
+          DEBUG: "pw:test"
+
+      - name: Save test artifacts
+        if: ${{ github.event_name == 'pull_request' && failure() }}
+        uses: actions/upload-artifact@v2
+        with:
+          name: e2e-test-report
+          path: artifacts.zip

+ 0 - 5
cypress/fixtures/example.json

@@ -1,5 +0,0 @@
-{
-  "name": "Using fixtures to represent data",
-  "email": "[email protected]",
-  "body": "Fixtures are a great way to mock data for responses to routes"
-}

+ 0 - 73
cypress/integration/app/basic.cljs

@@ -1,73 +0,0 @@
-(ns app.basic
-  "Basic operations"
-  (:require-macros [latte.core :refer [describe beforeEach before it]])
-  (:require [latte.chai :refer (expect)]
-            [app.util :as util])
-  (:refer-clojure :exclude [first get]))
-
-(def cy js/cy)
-
-(describe "basic"
-  (beforeEach []
-              (.clearIndexedDB cy))
-  (before []
-          (.. cy
-              (visit "http://localhost:3001")
-              (get "#main-content-container" #js {:timeout 10000})
-              (should (fn [result]
-                        (expect result :not.to.contain "Loading")))))
-
-  (it "Search" []
-    (.. cy
-        (get "#search-button")
-        (click)
-        (get "input.cp__palette-input")
-        (type "welcome to Logseq"))
-    (.. cy (get "#ui__ac-inner")
-        (should (fn [result]
-                  (expect result :to.have.length 1))))
-    (util/back-to-home)
-
-    ;; create new page
-    (.. cy
-        (get "#search-button")
-        (click)
-        (get "input.cp__palette-input")
-        (type "new page")
-        (wait 500)
-        (type "{enter}"))
-      
-    ;; edit bullet
-    (util/edit-block "this is my first bullet {enter}")
-    (util/edit-block "this is my second bullet {enter}")
-    (util/edit-block "this is my third bullet")
-    (util/tab)
-    (util/edit-block ", continue editing")
-    (util/shift+tab)
-    (util/edit-block ", continue {enter}")
-
-    ;; Backspace to delete a block
-    (util/edit-block "test")
-
-    ;; delete the previous block
-    (dorun (repeatedly 5 util/backspace))
-
-    (.. cy (get ".ls-block")
-        (should (fn [result]
-                  (expect result :to.have.length 3))))
-
-    (util/edit-block "{enter}")
-
-    ;; Del
-    (util/edit-block "test")
-    (util/edit-block "{leftarrow}{leftarrow}")
-    (util/delete)
-    (util/delete)
-
-    ;; FIXME: not working
-    ;; (match-content "te")
-
-    (util/edit-block "{enter}")
-
-    ;; Selection
-    (dorun (repeatedly 3 util/shift+up))))

+ 0 - 49
cypress/integration/app/template.cljs

@@ -1,49 +0,0 @@
-(ns app.template
-  "Template related operations"
-  (:require-macros [latte.core :refer [describe beforeEach before it]])
-  (:require [latte.chai :refer (expect)]
-            [app.util :as util])
-  (:refer-clojure :exclude [first get]))
-
-(def cy js/cy)
-
-(describe "template"
-          (beforeEach []
-                      (.clearIndexedDB cy)
-                      (cy.wait 1000))
-          (before []
-                  (.. cy
-                      (visit "http://localhost:3001")
-                      (get "#main-content-container" #js {:timeout 10000})
-                      (should (fn [result]
-                                (expect result :not.to.contain "Loading")))))
-          (it "template-basic" []
-              (.. cy
-                  (get "#search-button")
-                  (click)
-                  (get "input.cp__palette-input")
-                  (type "template test page")
-                  (wait 1000)
-                  (type "{enter}"))
-              (util/edit-block "template")
-              (.. cy
-                  (realPress #js ["Shift" "Enter"]))
-              (util/edit-block "template:: template-name{enter}{enter}")
-              (util/tab)
-              (util/edit-block "line1{enter}")
-              (util/edit-block "line2{enter}")
-              (util/tab)
-              (util/edit-block "line3")
-              (.. cy
-                  (get ".ls-block")
-                  (should (fn [result]
-                            (expect result :to.have.length 4))))
-              (dorun (repeatedly 3 #(util/edit-block "{enter}")))
-
-              (util/edit-block "/template{enter}")
-              (util/edit-block "template-name{enter}")
-              (cy.wait 1000)
-              (.. cy
-                  (get ".ls-block")
-                  (should (fn [result]
-                            (expect result :to.have.length 8))))))

+ 0 - 46
cypress/integration/app/util.cljs

@@ -1,46 +0,0 @@
-(ns app.util
-  (:refer-clojure :exclude [first get]))
-
-(def cy js/cy)
-
-(defn back-to-home
-  []
-  (.. cy (get ".ui__modal")
-      (first)
-      (click)))
-
-(defn edit-block
-  [content]
-  (.. cy (get "textarea")
-      (first)
-      (click)
-      (type content)))
-
-(defn tab
-  []
-  (.. cy (realPress "Tab")))
-
-(defn shift+tab
-  []
-  (.. cy (realPress #js ["Shift" "Tab"])))
-
-(defn shift+up
-  []
-  (.. cy (realPress #js ["Shift" "ArrowUp"])))
-
-(defn shift+down
-  []
-  (.. cy (realPress #js ["Shift" "ArrowDown"])))
-
-(defn backspace
-  []
-  (edit-block "{backspace}"))
-
-(defn delete
-  []
-  (.. cy (realPress "Delete")))
-
-(defn match-content
-  [value]
-  (.. cy (get "textarea") (first)
-      (should "have.value" value)))

+ 0 - 7
cypress/plugins/index.js

@@ -1,7 +0,0 @@
-const makeCljsPreprocessor = require('cypress-clojurescript-preprocessor');
-/**
- * @type {Cypress.PluginConfig}
- */
-module.exports = (on, config) => {
-  on('file:preprocessor', makeCljsPreprocessor(config));
-};

+ 0 - 44
cypress/support/commands.js

@@ -1,44 +0,0 @@
-// ***********************************************
-// This example commands.js shows you how to
-// create various custom commands and overwrite
-// existing commands.
-//
-// For more comprehensive examples of custom
-// commands please read more here:
-// https://on.cypress.io/custom-commands
-// ***********************************************
-//
-//
-// -- This is a parent command --
-// Cypress.Commands.add('login', (email, password) => { ... })
-//
-//
-// -- This is a child command --
-// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
-//
-//
-// -- This is a dual command --
-// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
-//
-//
-// -- This will overwrite an existing command --
-// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
-
-Cypress.Commands.add('clearIndexedDB', async () => {
-  const databases = await window.indexedDB.databases();
-
-  await Promise.all(
-    databases.map(
-      ({ name }) =>
-      new Promise((resolve, reject) => {
-        const request = window.indexedDB.deleteDatabase(name);
-
-        request.addEventListener('success', resolve);
-        // Note: we need to also listen to the "blocked" event
-        // (and resolve the promise) due to https://stackoverflow.com/a/35141818
-        request.addEventListener('blocked', resolve);
-        request.addEventListener('error', reject);
-      }),
-    ),
-  );
-});

+ 0 - 22
cypress/support/index.js

@@ -1,22 +0,0 @@
-// ***********************************************************
-// This example support/index.js is processed and
-// loaded automatically before your test files.
-//
-// This is a great place to put global configuration and
-// behavior that modifies Cypress.
-//
-// You can change the location of this file or turn off
-// automatically serving support files with the
-// 'supportFile' configuration option.
-//
-// You can read more here:
-// https://on.cypress.io/configuration
-// ***********************************************************
-
-// Import commands.js using ES2015 syntax:
-import './commands'
-
-// Alternatively you can use CommonJS syntax:
-// require('./commands')
-
-import "cypress-real-events/support";

+ 302 - 0
e2e-tests/basic.spec.ts

@@ -0,0 +1,302 @@
+import { test, expect } from '@playwright/test'
+import { ElectronApplication, Page, BrowserContext, _electron as electron } from 'playwright'
+import { randomString, createRandomPage, openSidebar, newBlock, lastBlock } from './utils'
+
+let electronApp: ElectronApplication
+let context: BrowserContext
+let page: Page
+
+test.beforeAll(async () => {
+  electronApp = await electron.launch({
+    cwd: "./static",
+    args: ["electron.js"],
+    // NOTE: video recording for Electron is not supported yet
+    // recordVideo: {
+    //   dir: "./videos",
+    // }
+  })
+
+  context = electronApp.context()
+  await context.tracing.start({ screenshots: true, snapshots: true });
+
+  // Evaluation expression in the Electron context.
+  const appPath = await electronApp.evaluate(async ({ app }) => {
+    // This runs in the main Electron process, parameter here is always
+    // the result of the require('electron') in the main app script.
+    return app.getAppPath()
+  })
+  console.log("Test start with AppPath:", appPath)
+})
+
+test.beforeEach(async () => {
+  // discard any dialog by ESC
+  if (page) {
+    await page.keyboard.press('Escape')
+    await page.keyboard.press('Escape')
+  } else {
+    page = await electronApp.firstWindow()
+  }
+})
+
+test.afterAll(async () => {
+  // await context.close();
+  await context.tracing.stop({ path: 'artifacts.zip' });
+  await electronApp.close()
+})
+
+test('render app', async () => {
+  // Direct Electron console to Node terminal.
+  // page.on('console', console.log)
+
+  // Wait for the app to load
+  await page.waitForLoadState('domcontentloaded')
+  await page.waitForFunction('window.document.title != "Loading"')
+
+  // Logseq: "A privacy-first platform for knowledge management and collaboration."
+  // or Logseq
+  expect(await page.title()).toMatch(/^Logseq.*?/)
+
+  page.once('load', async () => {
+    console.log('Page loaded!')
+    await page.screenshot({ path: 'startup.png' })
+  })
+})
+
+test('first start', async () => {
+  await page.waitForSelector('text=This is a demo graph, changes will not be saved until you open a local folder')
+})
+
+test('open sidebar', async () => {
+  await openSidebar(page)
+
+  await page.waitForSelector('#sidebar-nav-wrapper a:has-text("New page")', { state: 'visible' })
+  await page.waitForSelector('#sidebar-nav-wrapper >> text=Journals', { state: 'visible' })
+})
+
+test('search', async () => {
+  await page.click('#search-button')
+  await page.waitForSelector('[placeholder="Search or create page"]')
+  await page.fill('[placeholder="Search or create page"]', 'welcome')
+
+  await page.waitForTimeout(500)
+  const results = await page.$$('#ui__ac-inner .block')
+  expect(results.length).toBeGreaterThanOrEqual(1)
+})
+
+test('create page and blocks', async () => {
+  await createRandomPage(page)
+
+  // do editing
+  await page.fill(':nth-match(textarea, 1)', 'this is my first bullet')
+  await page.press(':nth-match(textarea, 1)', 'Enter')
+
+  // first block
+  expect(await page.$$('.block-content')).toHaveLength(1)
+
+  await page.fill(':nth-match(textarea, 1)', 'this is my second bullet')
+  await page.press(':nth-match(textarea, 1)', 'Enter')
+
+  await page.fill(':nth-match(textarea, 1)', 'this is my third bullet')
+  await page.press(':nth-match(textarea, 1)', 'Tab')
+  await page.press(':nth-match(textarea, 1)', 'Enter')
+
+  await page.keyboard.type('continue editing test')
+  await page.keyboard.press('Shift+Enter')
+  await page.keyboard.type('continue')
+
+  await page.keyboard.press('Enter')
+  await page.keyboard.press('Shift+Tab')
+  await page.keyboard.press('Shift+Tab')
+  await page.keyboard.type('test ok')
+  await page.keyboard.press('Escape')
+
+  const blocks = await page.$$('.ls-block')
+  expect(blocks).toHaveLength(5)
+
+  // active edit
+  await page.click('.ls-block >> nth=-1')
+  await page.press('textarea >> nth=0', 'Enter')
+  await page.fill('textarea >> nth=0', 'test')
+  for (let i = 0; i < 5; i++) {
+    await page.keyboard.press('Backspace')
+  }
+
+  await page.keyboard.press('Escape')
+  await page.waitForTimeout(500)
+  expect(await page.$$('.ls-block')).toHaveLength(5)
+})
+
+test('delete and backspace', async () => {
+  await createRandomPage(page)
+
+  await page.fill(':nth-match(textarea, 1)', 'test')
+
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('test')
+
+  // backspace
+  await page.keyboard.press('Backspace')
+  await page.keyboard.press('Backspace')
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('te')
+
+  // refill
+  await page.fill(':nth-match(textarea, 1)', 'test')
+  await page.keyboard.press('ArrowLeft')
+  await page.keyboard.press('ArrowLeft')
+
+  // delete
+  await page.keyboard.press('Delete')
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('tet')
+  await page.keyboard.press('Delete')
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('te')
+  await page.keyboard.press('Delete')
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('te')
+
+  // TODO: test delete & backspace across blocks
+})
+
+
+test('selection', async () => {
+  await createRandomPage(page)
+
+  await page.fill(':nth-match(textarea, 1)', 'line 1')
+  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await page.fill(':nth-match(textarea, 1)', 'line 2')
+  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await page.press(':nth-match(textarea, 1)', 'Tab')
+  await page.fill(':nth-match(textarea, 1)', 'line 3')
+  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await page.fill(':nth-match(textarea, 1)', 'line 4')
+  await page.press(':nth-match(textarea, 1)', 'Tab')
+  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await page.fill(':nth-match(textarea, 1)', 'line 5')
+
+  await page.keyboard.down('Shift')
+  await page.keyboard.press('ArrowUp')
+  await page.keyboard.press('ArrowUp')
+  await page.keyboard.press('ArrowUp')
+  await page.keyboard.up('Shift')
+
+  await page.waitForTimeout(500)
+  await page.keyboard.press('Backspace')
+
+  expect(await page.$$('.ls-block')).toHaveLength(2)
+})
+
+test('template', async () => {
+  const randomTemplate = randomString(10)
+
+  await createRandomPage(page)
+
+  await page.fill(':nth-match(textarea, 1)', 'template')
+  await page.press(':nth-match(textarea, 1)', 'Shift+Enter')
+  await page.type(':nth-match(textarea, 1)', 'template:: ' + randomTemplate)
+  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await page.press(':nth-match(textarea, 1)', 'Enter')
+
+  await page.press(':nth-match(textarea, 1)', 'Tab')
+  await page.fill(':nth-match(textarea, 1)', 'line1')
+  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await page.fill(':nth-match(textarea, 1)', 'line2')
+  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await page.press(':nth-match(textarea, 1)', 'Tab')
+  await page.fill(':nth-match(textarea, 1)', 'line3')
+
+  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await page.press(':nth-match(textarea, 1)', 'Enter')
+
+
+  expect(await page.$$('.ls-block')).toHaveLength(5)
+
+  await page.type(':nth-match(textarea, 1)', '/template')
+
+  await page.click('[title="Insert a created template here"]')
+  // type to search template name
+  await page.keyboard.type(randomTemplate.substring(0, 3))
+  await page.click('.absolute >> text=' + randomTemplate)
+
+  await page.waitForTimeout(500)
+
+  expect(await page.$$('.ls-block')).toHaveLength(8)
+})
+
+test('auto completion square brackets', async () => {
+  await createRandomPage(page)
+
+  await page.fill(':nth-match(textarea, 1)', 'Auto-completion test')
+  await page.press(':nth-match(textarea, 1)', 'Enter')
+
+  // [[]]
+  await page.type(':nth-match(textarea, 1)', 'This is a [')
+  await page.inputValue(':nth-match(textarea, 1)').then(text => {
+    expect(text).toBe('This is a []')
+  })
+  await page.type(':nth-match(textarea, 1)', '[')
+  // wait for search popup
+  await page.waitForSelector('text="Search for a page"')
+
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('This is a [[]]')
+
+  // re-enter edit mode
+  await page.press(':nth-match(textarea, 1)', 'Escape')
+  await page.click('.ls-block >> nth=-1')
+  await page.waitForSelector(':nth-match(textarea, 1)', { state: 'visible' })
+
+  // #3253
+  await page.press(':nth-match(textarea, 1)', 'ArrowLeft')
+  await page.press(':nth-match(textarea, 1)', 'ArrowLeft')
+  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await page.waitForSelector('text="Search for a page"', { state: 'visible' })
+
+  // type more `]`s
+  await page.type(':nth-match(textarea, 1)', ']')
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('This is a [[]]')
+  await page.type(':nth-match(textarea, 1)', ']')
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('This is a [[]]')
+  await page.type(':nth-match(textarea, 1)', ']')
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('This is a [[]]]')
+})
+
+test('auto completion and auto pair', async () => {
+  await createRandomPage(page)
+
+  await page.fill(':nth-match(textarea, 1)', 'Auto-completion test')
+  await page.press(':nth-match(textarea, 1)', 'Enter')
+
+  // {}
+  await page.type(':nth-match(textarea, 1)', 'type {{')
+  await page.press(':nth-match(textarea, 1)', 'Escape')
+
+  // FIXME: keycode seq is wrong
+  // expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('type {{}}')
+
+  // (()
+  await newBlock(page)
+
+  await page.type(':nth-match(textarea, 1)', 'type (')
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('type ()')
+  await page.type(':nth-match(textarea, 1)', '(')
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('type (())')
+
+  // ``
+  await newBlock(page)
+
+  await page.type(':nth-match(textarea, 1)', 'type `')
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('type ``')
+  await page.type(':nth-match(textarea, 1)', 'code here')
+
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('type `code here`')
+})
+
+
+// FIXME: Electron with filechooser is not working
+test.skip('open directory', async () => {
+  await page.click('#sidebar-nav-wrapper >> text=Journals')
+  await page.waitForSelector('h1:has-text("Open a local directory")')
+  await page.click('h1:has-text("Open a local directory")')
+
+  // await page.waitForEvent('filechooser')
+  await page.keyboard.press('Escape')
+
+  await page.click('#sidebar-nav-wrapper >> text=Journals')
+})

+ 50 - 0
e2e-tests/utils.ts

@@ -0,0 +1,50 @@
+import { Page } from 'playwright'
+import { expect } from '@playwright/test'
+
+
+export function randomString(length: number) {
+    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+
+    let result = '';
+    const charactersLength = characters.length;
+    for (let i = 0; i < length; i++) {
+        result += characters.charAt(Math.floor(Math.random() * charactersLength));
+    }
+
+    return result;
+}
+
+export async function openSidebar(page: Page) {
+    let sidebarVisible = await page.isVisible('#sidebar-nav-wrapper .left-sidebar-inner')
+    if (!sidebarVisible) {
+        await page.click('#left-menu.button')
+    }
+    await page.waitForSelector('#sidebar-nav-wrapper .left-sidebar-inner', { state: 'visible' })
+}
+
+export async function createRandomPage(page: Page) {
+    const randomTitle = randomString(20)
+
+    // Click #sidebar-nav-wrapper a:has-text("New page")
+    await page.click('#sidebar-nav-wrapper a:has-text("New page")')
+    // Fill [placeholder="Search or create page"]
+    await page.fill('[placeholder="Search or create page"]', randomTitle)
+    // Click text=/.*New page: "new page".*/
+    await page.click('text=/.*New page: ".*/')
+    // wait for textarea of first block
+    await page.waitForSelector(':nth-match(textarea, 1)', { state: 'visible' })
+}
+
+export async function lastBlock(page: Page) {
+    // discard any popups
+    await page.keyboard.press('Escape')
+    // click last block
+    await page.click('.ls-block >> nth=-1')
+    // wait for textarea
+    await page.waitForSelector(':nth-match(textarea, 1)', { state: 'visible' })
+}
+
+export async function newBlock(page: Page) {
+    await lastBlock(page)
+    await page.press(':nth-match(textarea, 1)', 'Enter')
+}

+ 1 - 1
libs/package.json

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

+ 2 - 0
libs/src/LSPlugin.ts

@@ -230,6 +230,8 @@ export interface IAppProxy {
   showMsg: (content: string, status?: 'success' | 'warning' | 'error' | string) => void
   setZoomFactor: (factor: number) => void
   setFullScreen: (flag: boolean | 'toggle') => void
+  setLeftSidebarVisible: (flag: boolean | 'toggle') => void
+  setRightSidebarVisible: (flag: boolean | 'toggle') => void
 
   registerUIItem: (
     type: 'toolbar' | 'pagebar',

+ 4 - 5
package.json

@@ -5,15 +5,16 @@
     "main": "static/electron.js",
     "devDependencies": {
         "@capacitor/cli": "^3.2.2",
+        "@playwright/test": "^1.16.3",
         "@tailwindcss/ui": "0.7.2",
         "@types/gulp": "^4.0.7",
         "cross-env": "^7.0.3",
         "cssnano": "^4.1.10",
-        "cypress": "^7.5.0",
         "del": "^6.0.0",
         "gulp": "^4.0.2",
         "gulp-clean-css": "^4.3.0",
         "npm-run-all": "^4.1.5",
+        "playwright": "^1.16.3",
         "postcss": "8.2.10",
         "postcss-cli": "8.3.1",
         "postcss-import": "^14.0.0",
@@ -36,7 +37,7 @@
         "dev-electron-app": "gulp electron",
         "release-electron": "run-s gulp:build && gulp electronMaker",
         "debug-electron": "cd static/ && yarn electron:debug",
-        "e2e-test": "./node_modules/.bin/cypress open",
+        "e2e-test": "npx playwright test --reporter github",
         "run-android-release": "yarn clean && yarn release-app && rm -rf ./public/static && mv static ./public && npx cap copy android && npx cap run android",
         "run-ios-release": "yarn clean && yarn release-app && rm -rf ./public/static && mv static ./public && npx cap copy ios && npx cap run ios",
         "clean": "gulp clean",
@@ -77,8 +78,6 @@
         "chokidar": "3.5.1",
         "chrono-node": "2.2.4",
         "codemirror": "5.58.1",
-        "cypress-clojurescript-preprocessor": "0.1.4",
-        "cypress-real-events": "1.5.0",
         "d3-force": "3.0.0",
         "diff": "5.0.0",
         "diff-match-patch": "1.0.5",
@@ -92,7 +91,7 @@
         "ignore": "5.1.8",
         "is-svg": "4.2.2",
         "jszip": "3.5.0",
-        "mldoc": "1.1.8",
+        "mldoc": "1.2.3",
         "path": "0.12.7",
         "pixi-graph-fork": "0.1.6",
         "pixi.js": "6.2.0",

+ 11 - 0
playwright.config.ts

@@ -0,0 +1,11 @@
+import { PlaywrightTestConfig } from '@playwright/test'
+
+const config: PlaywrightTestConfig = {
+  testDir: './e2e-tests',
+  maxFailures: 1,
+  use: {
+    screenshot: 'only-on-failure',
+  }
+}
+
+export default config

+ 1 - 0
resources/forge.config.js

@@ -21,6 +21,7 @@ module.exports = {
       'name': '@electron-forge/maker-squirrel',
       'config': {
         'name': 'Logseq',
+        'setupIcon': './icons/logseq.ico',
         'loadingGif': './icons/installing.gif'
       }
     },

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 758
resources/js/lsplugin.core.js


+ 2 - 2
resources/package.json

@@ -1,6 +1,6 @@
 {
   "name": "Logseq",
-  "version": "0.4.9",
+  "version": "0.5.1",
   "main": "electron.js",
   "author": "Logseq",
   "description": "A privacy-first, open-source platform for knowledge management and collaboration.",
@@ -11,7 +11,7 @@
     "electron:make": "electron-forge make",
     "electron:make-macos-arm64": "electron-forge make --platform=darwin --arch=arm64",
     "electron:publish:github": "electron-forge publish",
-    "rebuild:better-sqlite3": "electron-rebuild -v 15 -f -w better-sqlite3",
+    "rebuild:better-sqlite3": "electron-rebuild -v 15.1.2 -f -w better-sqlite3",
     "postinstall": "install-app-deps"
   },
   "config": {

+ 10 - 11
src/electron/electron/git.cljs

@@ -11,13 +11,9 @@
 
 (def log-error (partial (.-error logger) "[Git]"))
 
-(defn get-graph-path
-  []
-  (:graph/current @state/state))
-
 (defn get-graph-git-dir
   []
-  (when-let [graph-path (some-> (get-graph-path)
+  (when-let [graph-path (some-> (state/get-graph-path)
                                 (string/replace "/" "_")
                                 (string/replace ":" "comma"))]
     (let [dir (.join path (.homedir os) ".logseq" "git" graph-path ".git")]
@@ -26,12 +22,12 @@
 
 (defn dot-git-exists?
   []
-  (let [p (.join path (get-graph-path) ".git")]
+  (let [p (.join path (state/get-graph-path) ".git")]
     (fs/existsSync p)))
 
 (defn run-git!
   [commands]
-  (when-let [path (get-graph-path)]
+  (when-let [path (state/get-graph-path)]
     (when (fs/existsSync path)
       (p/let [result (.exec GitProcess commands path)]
         (if (zero? (gobj/get result "exitCode"))
@@ -45,7 +41,7 @@
 (defn git-dir-exists?
   []
   (try
-    (let [p (.join path (get-graph-path) ".git")]
+    (let [p (.join path (state/get-graph-path) ".git")]
       (.isDirectory (fs/statSync p)))
     (catch js/Error e
       nil)))
@@ -53,8 +49,10 @@
 (defn remove-dot-git-file!
   []
   (try
-    (let [graph-path (get-graph-path)
-          _ (and (string/blank? graph-path) (throw (js/Error. "Empty graph path")))
+    (let [graph-path (state/get-graph-path)
+          _ (when (string/blank? graph-path)
+              (utils/send-to-renderer "getCurrentGraph" {})
+              (throw (js/Error. "Empty graph path")))
           p (.join path graph-path ".git")]
       (when (and (fs/existsSync p)
                  (.isFile (fs/statSync p)))
@@ -81,7 +79,7 @@
                ["init"])]
     (p/let [_ (run-git! (clj->js args))]
       (when utils/win32?
-        (run-git! ["config" "core.safecrlf" "false"])))))
+        (run-git! #js ["config" "core.safecrlf" "false"])))))
 
 (defn add-all!
   []
@@ -173,5 +171,6 @@
     (js/setTimeout add-all-and-commit! 3000)
     (let [seconds (state/get-git-commit-seconds)]
       (when (int? seconds)
+        (js/setTimeout add-all-and-commit! 5000)
         (let [interval (js/setInterval add-all-and-commit! (* seconds 1000))]
           (state/set-git-commit-interval! interval))))))

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

@@ -195,6 +195,7 @@
   (search/ensure-search-dir!))
 
 (defmethod handle :addDirWatcher [window [_ dir]]
+  (watcher/close-watcher!)
   (when dir
     (watcher/watch-dir! window dir)))
 

+ 4 - 0
src/electron/electron/state.cljs

@@ -32,3 +32,7 @@
 (defn git-auto-commit-disabled?
   []
   (get-in @state [:config :git/disable-auto-commit?] true))
+
+(defn get-graph-path
+  []
+  (:graph/current @state))

+ 7 - 1
src/main/electron/listener.cljs

@@ -67,7 +67,13 @@
 
   (js/window.apis.on "setGitUsernameAndEmail"
                      (fn []
-                       (state/pub-event! [:modal/set-git-username-and-email]))))
+                       (state/pub-event! [:modal/set-git-username-and-email])))
+
+
+  (js/window.apis.on "getCurrentGraph"
+                     (fn []
+                       (when-let [graph (state/get-current-repo)]
+                         (ipc/ipc "setCurrentGraph" graph)))))
 
 (defn listen!
   []

+ 3 - 0
src/main/frontend/commands.cljs

@@ -377,6 +377,9 @@
           postfix (subs edit-content current-pos)
           postfix (if postfix-fn (postfix-fn postfix) postfix)
           new-value (cond
+                      (string/blank? postfix)
+                      prefix
+
                       space?
                       (util/concat-without-spaces prefix postfix)
 

+ 17 - 13
src/main/frontend/components/block.cljs

@@ -290,7 +290,7 @@
                     config/publishing?
                     (subs href 1)
 
-                    (= protocol "data")
+                    (= "Embed_data" (first url))
                     href
 
                     :else
@@ -871,6 +871,9 @@
                 [:span (util/format "[[%s]]" page)]
                 (page-reference (:html-export? config) page config label*)))))
 
+        ["Embed_data" src]
+        (image-link config url src nil metadata full_text)
+
         ["Search" s]
         (cond
           (string/blank? s)
@@ -887,7 +890,7 @@
           (not (string/includes? s "."))
           (page-reference (:html-export? config) s config label)
 
-          (util/safe-re-find #"(?i)^http[s]?://" s)
+          (util/url? s)
           (->elem :a {:href s
                       :data-href s
                       :target "_blank"}
@@ -1864,7 +1867,6 @@
 
 (rum/defc block-content-fallback
   [edit-input-id block]
-
   (let [content (:block/content block)]
     [:section.border.mt-1.p-1.cursor-pointer.block-content-fallback-ui
      {:on-click #(state/set-editing! edit-input-id content block "")}
@@ -1882,16 +1884,18 @@
         slide? (:slide? config)]
     (if (and edit? editor-box)
       [:div.editor-wrapper {:id editor-id}
-       (editor-box {:block block
-                    :block-id uuid
-                    :block-parent-id block-id
-                    :format format
-                    :heading-level heading-level
-                    :on-hide (fn [_value event]
-                               (when (= event :esc)
-                                 (editor-handler/escape-editing)))}
-                   edit-input-id
-                   config)]
+       (ui/catch-error
+        [:p.warning "Something wrong in the editor"]
+        (editor-box {:block block
+                     :block-id uuid
+                     :block-parent-id block-id
+                     :format format
+                     :heading-level heading-level
+                     :on-hide (fn [_value event]
+                                (when (= event :esc)
+                                  (editor-handler/escape-editing)))}
+                    edit-input-id
+                    config))]
       [:div.flex.flex-row.block-content-wrapper
        [:div.flex-1.w-full {:style {:display (if (:slide? config) "block" "flex")}}
         (ui/catch-error

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

@@ -63,7 +63,7 @@
             (reset! commands/*current-command command)
             (let [command-steps (get (into {} matched) command)
                   restore-slash? (or
-                                  (contains? #{"Today" "Yesterday" "Tomorrow"} command)
+                                  (contains? #{"Today" "Yesterday" "Tomorrow" "Current time"} command)
                                   (and
                                    (not (fn? command-steps))
                                    (not (contains? (set (map first command-steps)) :editor/input))

+ 1 - 0
src/main/frontend/components/journal.cljs

@@ -97,6 +97,7 @@
                        [:div.journal-item.content {:key journal-name}
                         (journal-cp [journal-name format])])
                      {:has-more (page-handler/has-more-journals?)
+                      :on-top-reached page-handler/create-today-journal!
                       :on-load (fn []
                                  (page-handler/load-more-journals!))})])
 

+ 31 - 30
src/main/frontend/components/page.cljs

@@ -846,7 +846,7 @@
             [:a.ml-1.pr-2.opacity-70.hover:opacity-100
              {:on-click (fn [] (state/set-modal!
                                 (batch-delete-dialog
-                                 (model/get-orphaned-pages (state/get-current-repo)) true
+                                 (model/get-orphaned-pages {}) true
                                  #(do
                                     (reset! *checks nil)
                                     (refresh-pages)))))}
@@ -899,35 +899,36 @@
 
            [:tbody
             (for [{:block/keys [idx name created-at updated-at backlinks] :as page} @*results]
-              [:tr {:key name}
-               [:td.selector
-                (checkbox-opt (str "label-" idx)
-                              (get @*checks idx)
-                              {:on-change (fn []
-                                            (swap! *checks update idx not))})]
-
-               [:td.name [:a {:on-click (fn [e]
-                                          (let [repo (state/get-current-repo)]
-                                            (when (gobj/get e "shiftKey")
-                                              (state/sidebar-add-block!
-                                               repo
-                                               (:db/id page)
-                                               :page
-                                               {:page (:block/name page)}))))
-                              :href     (rfe/href :page {:name (:block/name page)})}
-                          (block/page-cp {} page)]]
-
-               (when-not mobile?
-                 [:td.backlinks [:span backlinks]])
-
-               (when-not mobile?
-                 [:td.created-at [:span (if created-at
-                                          (date/int->local-time-2 created-at)
-                                          "Unknown")]])
-               (when-not mobile?
-                 [:td.updated-at [:span (if updated-at
-                                          (date/int->local-time-2 updated-at)
-                                          "Unknown")]])])]]
+              (when-not (string/blank? name)
+                [:tr {:key name}
+                 [:td.selector
+                  (checkbox-opt (str "label-" idx)
+                                (get @*checks idx)
+                                {:on-change (fn []
+                                              (swap! *checks update idx not))})]
+
+                 [:td.name [:a {:on-click (fn [e]
+                                            (let [repo (state/get-current-repo)]
+                                              (when (gobj/get e "shiftKey")
+                                                (state/sidebar-add-block!
+                                                 repo
+                                                 (:db/id page)
+                                                 :page
+                                                 {:page (:block/name page)}))))
+                                :href     (rfe/href :page {:name (:block/name page)})}
+                            (block/page-cp {} page)]]
+
+                 (when-not mobile?
+                   [:td.backlinks [:span backlinks]])
+
+                 (when-not mobile?
+                   [:td.created-at [:span (if created-at
+                                            (date/int->local-time-2 created-at)
+                                            "Unknown")]])
+                 (when-not mobile?
+                   [:td.updated-at [:span (if updated-at
+                                            (date/int->local-time-2 updated-at)
+                                            "Unknown")]])]))]]
 
           [:div.paginates
            [:span]

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

@@ -402,7 +402,8 @@
          #())
        [id])
      [:div.lsp-hook-ui-slot
-      (merge opts {:id id})])))
+      (merge opts {:id id
+                   :on-mouse-down (fn [e] (util/stop e))})])))
 
 (rum/defc ui-item-renderer
   [pid type {:keys [key template]}]

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

@@ -18,6 +18,7 @@
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
+            [frontend.fs :as fs]
             [frontend.version :as version]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]
@@ -196,7 +197,9 @@
                                                  (common-handler/reset-config! url nil)
                                                  (shortcut/refresh!)
                                                  (when-not (= :draw (state/get-current-route))
-                                                   (route-handler/redirect-to-home!)))}})
+                                                   (route-handler/redirect-to-home!))
+                                                 (when-let [dir-name (config/get-repo-dir url)]
+                                                   (fs/watch-dir! dir-name)))}})
                         switch-repos)
             links (concat repo-links
                           [(when (seq switch-repos)

+ 3 - 2
src/main/frontend/components/settings.cljs

@@ -622,8 +622,7 @@
             (enable-all-pages-public-row t enable-all-pages-public?)
             (encryption-row t enable-encryption?)
             (zotero-settings-row t)
-            (auto-push-row t current-repo enable-git-auto-push?)
-            (graph-config t)]
+            (auto-push-row t current-repo enable-git-auto-push?)]
 
            :shortcuts
            [:div.panel-wrap
@@ -676,6 +675,8 @@
                       :target "_blank"}
                   "https://github.com/isomorphic-git/cors-proxy"]])])
 
+            (graph-config t)
+
             (when logged?
               [:div
                [:hr]

+ 2 - 0
src/main/frontend/components/sidebar.cljs

@@ -353,6 +353,8 @@
          (when-not (mobile-util/is-native-platform?)
            (widgets/demo-graph-alert))
 
+         (widgets/github-integration-soon-deprecated-alert)
+
          (cond
            (not indexeddb-support?)
            nil

+ 17 - 0
src/main/frontend/components/widgets.cljs

@@ -141,3 +141,20 @@
     (ui/admonition
      :warning
      [:p "This is a demo graph, changes will not be saved until you open a local folder."])))
+
+(rum/defc github-integration-soon-deprecated-alert
+  []
+  (when-let [repo (state/get-current-repo)]
+    (when (string/starts-with? repo "https://github.com")
+      [:div.github-alert
+       (ui/admonition
+        :warning
+        [:p "We're going to deprecate the GitHub integration when the mobile app is out, you can switch to the latest "
+         [:a {:href "https://github.com/logseq/logseq/releases"
+              :target "_blank"}
+          "desktop app"]
+         [:span ", see more details at "]
+         [:a {:href "https://discord.com/channels/725182569297215569/735735090784632913/861656585578086400"
+              :target "_blank"}
+          "here"]
+         [:span "."]])])))

+ 11 - 0
src/main/frontend/config.cljs

@@ -213,6 +213,17 @@
      1]
     ["" 0]))
 
+(defn with-label-link
+  [format label link]
+  (case format
+    :org
+    [(util/format "[[%s][label]]" link label)
+     (+ 4 (count link) (count label))]
+    :markdown
+    [(util/format "[%s](%s)" label link)
+     (+ 4 (count link) (count label))]
+    ["" 0]))
+
 (defn with-default-label
   [format label]
   (case format

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

@@ -2,7 +2,7 @@
   (:require [clojure.string :as string]))
 
 (defonce built-in-pages-names
-  #{"NOW" "LATER" "DOING" "DONE" "IN-PROGRESS" "TODO" "WAIT" "WAITING" "A" "B" "C" "Favorites"})
+  #{"NOW" "LATER" "DOING" "DONE" "IN-PROGRESS" "TODO" "WAIT" "WAITING" "A" "B" "C" "Favorites" "Contents" "card"})
 
 (def built-in-pages
   (mapv (fn [p]

+ 31 - 21
src/main/frontend/db/model.cljs

@@ -336,6 +336,13 @@
      (set)
      (set/union #{page-id}))))
 
+(defn get-entities-by-ids
+  ([ids]
+   (get-entities-by-ids (state/get-current-repo) ids))
+  ([repo ids]
+   (when repo
+     (db-utils/pull-many repo '[*] ids))))
+
 (defn get-page-names-by-ids
   ([ids]
    (get-page-names-by-ids (state/get-current-repo) ids))
@@ -1432,32 +1439,35 @@
    (take 200)))
 
 (defn get-orphaned-pages
-  [repo]
-  (let [all-pages (get-pages repo)
+  [{:keys [repo pages empty-ref-f]
+          :or {repo (state/get-current-repo)
+               empty-ref-f (fn [page] (zero? (count (:block/_refs page))))}}]
+  (let [pages (->> (or pages (get-pages repo))
+                   (remove nil?))
         built-in-pages (set (map string/lower-case default-db/built-in-pages-names))
         orphaned-pages (->>
-                         (map
-                           (fn [page]
-                             (let [name (string/lower-case page)]
-                               (when-let [page (db-utils/entity [:block/name name])]
-                                 (and
-                                   (zero? (count (:block/_refs page)))
-                                   (or
-                                     (page-empty? repo (:db/id page))
-                                     (let [first-child (first (:block/_left page))
-                                           children (:block/_page page)]
-                                       (and
-                                         first-child
-                                         (= 1 (count children))
-                                         (contains? #{"" "-" "*"} (string/trim (:block/content first-child))))))
-                                   (not (contains? built-in-pages name))
-                                   page))))
-                           all-pages)
-                         (remove false?))]
+                        (map
+                          (fn [page]
+                            (let [name (string/lower-case page)]
+                              (when-let [page (db-utils/entity [:block/name name])]
+                                (and
+                                 (empty-ref-f page)
+                                 (or
+                                  (page-empty? repo (:db/id page))
+                                  (let [first-child (first (:block/_left page))
+                                        children (:block/_page page)]
+                                    (and
+                                     first-child
+                                     (= 1 (count children))
+                                     (contains? #{"" "-" "*"} (string/trim (:block/content first-child))))))
+                                 (not (contains? built-in-pages name))
+                                 page))))
+                          pages)
+                        (remove false?))]
     orphaned-pages))
 
 (defn remove-orphaned-pages!
-  ([repo] (remove-orphaned-pages! repo (get-orphaned-pages repo)))
+  ([repo] (remove-orphaned-pages! repo (get-orphaned-pages {})))
   ([repo orphaned-pages]
    (let [transaction (mapv (fn [page] [:db/retractEntity (:db/id page)]) orphaned-pages)]
      (db-utils/transact! transaction))))

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

@@ -147,6 +147,7 @@
   ([repo e {:keys [sort-by blocks? sample counter current-filter vars] :as env} level]
    ;; TODO: replace with multi-methods for extensibility.
    (let [fe (first e)
+         fe (when fe (symbol (string/lower-case (name fe))))
          page-ref? (text/page-ref? e)]
      (when (or (and page-ref?
                     (not (contains? #{'page-property 'page-tags} (:current-filter env))))

+ 5 - 5
src/main/frontend/extensions/srs.cljs

@@ -571,7 +571,8 @@
   (let [repo (state/get-current-repo)
         query-string (:query-string state)
         card-index (::card-index state)
-        query-result (:query-result state)]
+        query-result (:query-result state)
+        global? (:global? config)]
     (if (seq query-result)
       (let [{:keys [total result]} (query-scheduled repo query-result (tl/local-now))
             review-cards result
@@ -580,7 +581,7 @@
             filtered-total (count result)
             modal? (:modal? config)]
         [:div.flex-1.cards-review {:style (when modal? {:height "100%"})
-                                   :class (if (:global? config) "" "shadow-xl")}
+                                   :class (if global? "" "shadow-xl")}
          [:div.flex.flex-row.items-center.justify-between.cards-title
           [:div.flex.flex-row.items-center
            (ui/icon "infinity" {:style {:font-size 20}})
@@ -638,10 +639,9 @@
                                  (persist-var/persist-save of-matrix))})
                        card-index))]
            review-finished)])
-
-      (if (zero? @cards-total)
+      (if global?
         [:div.ls-card
-         [:h1.title "Time to create your first card!"]
+         [:h1.title "Time to create a card!"]
 
          [:div
           [:p "You can add \"#card\" to any block to turn it into a card or trigger \"/cloze\" to add some clozes."]

+ 29 - 7
src/main/frontend/format/block.cljs

@@ -55,6 +55,7 @@
 
                   (and
                    (= typ "Search")
+                   (text/page-ref? (second (:url (second block))))
                    (text/page-ref-un-brackets! (second (:url (second block)))))
 
                   (and
@@ -84,10 +85,10 @@
 
                (and (vector? block)
                     (= "Macro" (first block)))
-               (let [{:keys [name arguments]} (second block)]
-                 (let [argument (string/join ", " arguments)]
+               (let [{:keys [name arguments]} (second block)
+                     argument (string/join ", " arguments)]
                    (when (= name "embed")
-                     (text/page-ref-un-brackets! argument))))
+                     (text/page-ref-un-brackets! argument)))
 
                (and (vector? block)
                     (= "Tag" (first block)))
@@ -198,8 +199,11 @@
                                            "id"
                                            k)
                                        v (if (coll? v)
-                                           (remove util/wrapped-by-quotes? v)
-                                           (property/parse-property k v))
+                                           (->> (remove util/wrapped-by-quotes? v)
+                                                (remove string/blank?))
+                                           (if (string/blank? v)
+                                             nil
+                                             (property/parse-property k v)))
                                        k (keyword k)
                                        v (if (and
                                               (string? v)
@@ -207,7 +211,8 @@
                                            (set [v])
                                            v)
                                        v (if (coll? v) (set v) v)]
-                                   [k v]))))]
+                                   [k v])))
+                          (remove #(nil? (second %))))]
       {:properties (into {} properties)
        :properties-order (map first properties)
        :page-refs page-refs})))
@@ -469,7 +474,8 @@
                                    [(text/page-ref-un-brackets! v)]
 
                                    :else
-                                   nil)) (vals properties))]
+                                   nil)) (vals properties))
+        page-refs (remove string/blank? page-refs)]
     (map (fn [page] (page-name->map page true)) page-refs)))
 
 (defn extract-blocks
@@ -713,6 +719,22 @@
                    (assoc :block/warning :multiple-blocks))]
        (if uuid (assoc block :block/uuid uuid) block)))))
 
+(defn parse-title-and-body
+  [format pre-block? content]
+  (def content content)
+  (let [ast (format/to-edn content format nil)
+        content (if pre-block? content
+                    (str (config/get-block-pattern format) " " (string/triml content)))
+        content (property/remove-properties format content)
+        ast (->> (format/to-edn content format nil)
+                 (map first))
+        title (when (heading-block? (first ast))
+                (:title (second (first ast))))]
+    (cond->
+      {:block/body (vec (if title (rest ast) ast))}
+      title
+      (assoc :block/title title))))
+
 (defn macro-subs
   [macro-content arguments]
   (loop [s macro-content

+ 1 - 1
src/main/frontend/fs/node.cljs

@@ -69,7 +69,7 @@
           (when (util/electron?)
             (debug/set-ack-step! path :saved-successfully)
             (debug/ack-file-write! path))
-          (let [disk-content (encrypt/decrypt disk-content)]
+          (p/let [disk-content (encrypt/decrypt disk-content)]
             (state/pub-event! [:file/not-matched-from-disk path disk-content content])))
 
         :else

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

@@ -45,7 +45,7 @@
 
           (and (= "change" type)
                (not (db/file-exists? repo path)))
-          (js/console.warn "Can't get file in the db: " path)
+          (js/console.error "Can't get file in the db: " path)
 
           (and (= "change" type)
                (not= (string/trim content) (string/trim db-content))
@@ -69,7 +69,7 @@
                (db/file-exists? repo path))
           (when-let [page-name (db/get-file-page path)]
             (println "Delete page: " page-name ", file path: " path ".")
-            (page-handler/delete! page-name #()))
+            (page-handler/delete! page-name #() :delete-file? false))
 
           (contains? #{"add" "change" "unlink"} type)
           nil

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

@@ -7,6 +7,7 @@
             [frontend.config :as config]
             [frontend.db :as db]
             [frontend.db-schema :as db-schema]
+            [frontend.db.conn :as conn]
             [frontend.error :as error]
             [frontend.handler.command-palette :as command-palette]
             [frontend.handler.common :as common-handler]
@@ -29,6 +30,7 @@
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util.pool :as pool]
+            [cljs.reader :refer [read-string]]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
             [promesa.core :as p]))
@@ -52,7 +54,7 @@
         f (fn []
             (let [repo (state/get-current-repo)]
               (when-not (state/nfs-refreshing?)
-               ;; Don't create the journal file until user writes something
+                ;; Don't create the journal file until user writes something
                 (page-handler/create-today-journal!))
 
               (when (and (state/input-idle? repo)
@@ -150,6 +152,21 @@
   (js/window.addEventListener "online" handle-connection-change)
   (js/window.addEventListener "offline" handle-connection-change))
 
+(defn enable-datalog-console
+  "Enables datalog console in browser provided by https://github.com/homebaseio/datalog-console"
+  []
+  (js/document.documentElement.setAttribute "__datalog-console-remote-installed__" true)
+  (.addEventListener js/window "message"
+                     (fn [event]
+                       (let [conn (conn/get-conn)]
+                         (when-let [devtool-message (gobj/getValueByKeys event "data" ":datalog-console.client/devtool-message")]
+                           (let [msg-type (:type (read-string devtool-message))]
+                             (case msg-type
+
+                               :datalog-console.client/request-whole-database-as-string
+                               (.postMessage js/window #js {":datalog-console.remote/remote-message" (pr-str conn)} "*")
+
+                               nil)))))))
 (defn- get-repos
   []
   (let [logged? (state/logged?)
@@ -218,6 +235,8 @@
     (db/run-batch-txs!)
     (file-handler/run-writes-chan!)
     (pool/init-parser-pool!)
+    (when config/dev?
+      (enable-datalog-console))
     (when (util/electron?)
       (el/listen!))
     (mobile/init!)))

+ 129 - 105
src/main/frontend/handler/editor.cljs

@@ -123,8 +123,11 @@
 (defn strike-through-format! []
   (format-text! config/get-strike-through))
 
-(defn html-link-format! []
-  (when-let [m (get-selection-and-format)]
+(defn html-link-format!
+  ([]
+   (html-link-format! nil))
+  ([link]
+   (when-let [m (get-selection-and-format)]
     (let [{:keys [selection-start selection-end format value edit-id input]} m
           cur-pos (cursor/pos input)
           empty-selection? (= selection-start selection-end)
@@ -135,6 +138,9 @@
                                   empty-selection?
                                   (config/get-empty-link-and-forward-pos format)
 
+                                  link
+                                  (config/with-label-link format selection link)
+
                                   selection-link?
                                   (config/with-default-link format selection)
 
@@ -146,7 +152,7 @@
                      (subs value selection-end))
           cur-pos (or selection-start cur-pos)]
       (state/set-edit-content! edit-id new-value)
-      (cursor/move-cursor-to input (+ cur-pos forward-pos)))))
+      (cursor/move-cursor-to input (+ cur-pos forward-pos))))))
 
 (defn open-block-in-sidebar!
   [block-id]
@@ -186,13 +192,6 @@
     (doseq [block blocks]
       (gdom-classes/remove block "block-highlight"))))
 
-;; FIXME: children' :block/path-ref-pages
-(defn compute-retract-refs
-  "Computes old references to be retracted."
-  [eid {:block/keys [refs]} old-refs]
-  ;; TODO:
-  )
-
 (defn- get-edit-input-id-with-block-id
   [block-id]
   (when-let [first-block (util/get-first-block-by-id block-id)]
@@ -2434,7 +2433,7 @@
               "admonition-block" (keydown-new-line)
               "source-block" (keydown-new-line)
               "block-ref" (open-block-in-sidebar! (:link thing-at-point))
-              "page-ref" (do
+              "page-ref" (when-not (string/blank? (:link thing-at-point))
                            (insert-first-page-block-if-not-exists! (:link thing-at-point))
                            (route-handler/redirect-to-page! (:link thing-at-point)))
               "list-item"
@@ -2762,7 +2761,9 @@
           ctrlKey (gobj/get e "ctrlKey")
           metaKey (gobj/get e "metaKey")
           is-composing? (gobj/getValueByKeys e "event_" "isComposing")
-          pos (cursor/pos input)]
+          pos (cursor/pos input)
+          shift? (.-shiftKey e)
+          code (gobj/getValueByKeys e "event_" "code")]
       (cond
         (or is-composing? (= key-code 229))
         nil
@@ -2788,12 +2789,18 @@
         (and (autopair-when-selected key) (string/blank? (util/get-selected-text)))
         nil
 
-        (and (not (string/blank? (util/get-selected-text))) (= key-code keycode/left-square-bracket))
+        (and (not (string/blank? (util/get-selected-text)))
+             (or (= key-code keycode/left-square-bracket)
+                 (= keycode/left-square-bracket-code code))
+             (not shift?))
         (do
-          (util/stop e)
-          (autopair input-id "[" format nil))
+          (autopair input-id "[" format nil)
+          (util/stop e))
 
-        (and (not (string/blank? (util/get-selected-text))) (= key-code keycode/left-paren))
+        (and (not (string/blank? (util/get-selected-text)))
+             (or (= key-code keycode/left-paren)
+                 (= keycode/left-paren-code code))
+             shift?)
         (do
           (util/stop e)
           (autopair input-id "(" format nil))
@@ -2839,97 +2846,108 @@
 (defn keyup-handler
   [state input input-id search-timeout]
   (fn [e key-code]
-    (let [k (gobj/get e "key")
-          format (:format (get-state))
-          current-pos (cursor/pos input)
-          value (gobj/get input "value")
-          c (util/nth-safe value (dec current-pos))
-          last-key-code (state/get-last-key-code)
-          blank-selected? (string/blank? (util/get-selected-text))]
-      (when-not (state/get-editor-show-input)
-        (cond
-          (and (not (contains? #{"ArrowDown" "ArrowLeft" "ArrowRight" "ArrowUp"} k))
-               (not (:editor/show-page-search? @state/state))
-               (not (:editor/show-page-search-hashtag? @state/state))
-               (wrapped-by? input "[[" "]]"))
-          (let [orig-pos (cursor/get-caret-pos input)
-                value (gobj/get input "value")
-                square-pos (string/last-index-of (subs value 0 (:pos orig-pos)) "[[")
-                pos (+ square-pos 2)
-                _ (state/set-last-pos! pos)
-                pos (assoc orig-pos :pos pos)
-                command-step (if (= \# (util/nth-safe value (dec square-pos)))
-                               :editor/search-page-hashtag
-                               :editor/search-page)]
-            (commands/handle-step [command-step])
-            (reset! commands/*slash-caret-pos pos))
-
-          (and blank-selected?
-               (= keycode/left-square-bracket key-code last-key-code)
-               (not= k "[")
-               (> current-pos 0))
-          (do
-            (commands/handle-step [:editor/input "[[]]" {:backward-truncate-number 2
-                                                         :backward-pos 2}])
-            (commands/handle-step [:editor/search-page])
-            (reset! commands/*slash-caret-pos (cursor/get-caret-pos input)))
-
-          (and blank-selected?
-               (= keycode/left-paren key-code last-key-code)
-               (not= k "(")
-               (> current-pos 0))
-          (do
-            (commands/handle-step [:editor/input "(())" {:backward-truncate-number 2
-                                                         :backward-pos 2}])
-            (commands/handle-step [:editor/search-block :reference])
-            (reset! commands/*slash-caret-pos (cursor/get-caret-pos input)))
-
-          (and (= "〈" c)
-               (= "《" (util/nth-safe value (dec (dec current-pos))))
-               (> current-pos 0))
-          (do
-            (commands/handle-step [:editor/input commands/angle-bracket {:last-pattern "《〈"
-                                                                         :backward-pos 0}])
-            (reset! commands/*angle-bracket-caret-pos (cursor/get-caret-pos input))
-            (reset! commands/*show-block-commands true))
-
-          (and (= c " ")
-               (or (= (util/nth-safe value (dec (dec current-pos))) "#")
-                   (not (state/get-editor-show-page-search?))
-                   (and (state/get-editor-show-page-search?)
-                        (not= (util/nth-safe value current-pos) "]"))))
-          (state/set-editor-show-page-search-hashtag! false)
-
-          (and @*show-commands (not= k (state/get-editor-command-trigger)))
-          (let [matched-commands (get-matched-commands input)]
-            (if (seq matched-commands)
+    (let [k (gobj/get e "key")]
+      (when-not (= k "Process")
+        (let [code (gobj/getValueByKeys e "event_" "code")
+              format (:format (get-state))
+              current-pos (cursor/pos input)
+              value (gobj/get input "value")
+              c (util/nth-safe value (dec current-pos))
+              last-key-code (state/get-last-key-code)
+              blank-selected? (string/blank? (util/get-selected-text))
+              shift? (.-shiftKey e)]
+          (when-not (state/get-editor-show-input)
+            (cond
+              (and (not (contains? #{"ArrowDown" "ArrowLeft" "ArrowRight" "ArrowUp"} k))
+                   (not (:editor/show-page-search? @state/state))
+                   (not (:editor/show-page-search-hashtag? @state/state))
+                   (wrapped-by? input "[[" "]]"))
+              (let [orig-pos (cursor/get-caret-pos input)
+                    value (gobj/get input "value")
+                    square-pos (string/last-index-of (subs value 0 (:pos orig-pos)) "[[")
+                    pos (+ square-pos 2)
+                    _ (state/set-last-pos! pos)
+                    pos (assoc orig-pos :pos pos)
+                    command-step (if (= \# (util/nth-safe value (dec square-pos)))
+                                   :editor/search-page-hashtag
+                                   :editor/search-page)]
+                (commands/handle-step [command-step])
+                (reset! commands/*slash-caret-pos pos))
+
+              (and blank-selected?
+                   (or (= keycode/left-square-bracket key-code (:key-code last-key-code))
+                       (= keycode/left-square-bracket-code code (:code last-key-code)))
+                   (not shift?)
+                   (> current-pos 0)
+                   (not (wrapped-by? input "[[" "]]")))
+              (do
+                (commands/handle-step [:editor/input "[[]]" {:backward-truncate-number 2
+                                                             :backward-pos 2}])
+                (commands/handle-step [:editor/search-page])
+                (reset! commands/*slash-caret-pos (cursor/get-caret-pos input)))
+
+              (and blank-selected?
+                   (or (= keycode/left-paren key-code (:key-code last-key-code))
+                       (= keycode/left-paren-code code (:code last-key-code)))
+                   (:shift? last-key-code)
+                   shift?
+                   (> current-pos 0)
+                   (not (wrapped-by? input "((" "))")))
               (do
-                (reset! *show-commands true)
-                (reset! commands/*matched-commands matched-commands))
-              (reset! *show-commands false)))
-
-          (and @*show-block-commands (not= key-code 188)) ; not <
-          (let [matched-block-commands (get-matched-block-commands input)]
-            (if (seq matched-block-commands)
-              (cond
-                (= key-code 9)       ;tab
-                (when @*show-block-commands
-                  (util/stop e)
-                  (insert-command! input-id
-                                   (last (first matched-block-commands))
-                                   format
-                                   {:last-pattern commands/angle-bracket}))
-
-                :else
-                (reset! commands/*matched-block-commands matched-block-commands))
-              (reset! *show-block-commands false)))
-
-          (nil? @search-timeout)
-          (close-autocomplete-if-outside input)
+                (commands/handle-step [:editor/input "(())" {:backward-truncate-number 2
+                                                             :backward-pos 2}])
+                (commands/handle-step [:editor/search-block :reference])
+                (reset! commands/*slash-caret-pos (cursor/get-caret-pos input)))
+
+              (and (= "〈" c)
+                   (= "《" (util/nth-safe value (dec (dec current-pos))))
+                   (> current-pos 0))
+              (do
+                (commands/handle-step [:editor/input commands/angle-bracket {:last-pattern "《〈"
+                                                                             :backward-pos 0}])
+                (reset! commands/*angle-bracket-caret-pos (cursor/get-caret-pos input))
+                (reset! commands/*show-block-commands true))
+
+              (and (= c " ")
+                   (or (= (util/nth-safe value (dec (dec current-pos))) "#")
+                       (not (state/get-editor-show-page-search?))
+                       (and (state/get-editor-show-page-search?)
+                            (not= (util/nth-safe value current-pos) "]"))))
+              (state/set-editor-show-page-search-hashtag! false)
+
+              (and @*show-commands (not= k (state/get-editor-command-trigger)))
+              (let [matched-commands (get-matched-commands input)]
+                (if (seq matched-commands)
+                  (do
+                    (reset! *show-commands true)
+                    (reset! commands/*matched-commands matched-commands))
+                  (reset! *show-commands false)))
+
+              (and @*show-block-commands (not= key-code 188)) ; not <
+              (let [matched-block-commands (get-matched-block-commands input)]
+                (if (seq matched-block-commands)
+                  (cond
+                    (= key-code 9)       ;tab
+                    (when @*show-block-commands
+                      (util/stop e)
+                      (insert-command! input-id
+                                       (last (first matched-block-commands))
+                                       format
+                                       {:last-pattern commands/angle-bracket}))
+
+                    :else
+                    (reset! commands/*matched-block-commands matched-block-commands))
+                  (reset! *show-block-commands false)))
+
+              (nil? @search-timeout)
+              (close-autocomplete-if-outside input)
 
-          :else
-          nil)))
-    (state/set-last-key-code! key-code)))
+              :else
+              nil))
+          (when-not (= k "Shift")
+            (state/set-last-key-code! {:key-code key-code
+                                       :code code
+                                       :shift? (.-shiftKey e)})))))))
 
 (defn editor-on-click!
   [id]
@@ -3007,6 +3025,12 @@
         (paste-block-vec-tree-at-target copied-block-tree [] nil)
         (util/stop e))
 
+      (and (util/url? text)
+           (not (string/blank? (util/get-selected-text))))
+      (do
+        (util/stop e)
+        (html-link-format! text))
+
       (and (text/block-ref? text)
            (wrapped-by? input "((" "))"))
       (do

+ 9 - 2
src/main/frontend/handler/events.cljs

@@ -173,7 +173,7 @@
   (state/clear-edit!)
   (when-let [repo (state/get-current-repo)]
     (when (and disk-content db-content
-               (not= (string/trim disk-content) (string/trim db-content)))
+               (not= (util/trim-safe disk-content) (util/trim-safe db-content)))
       (state/set-modal! #(diff/local-file repo path disk-content db-content)))))
 
 (defmethod handle :modal/display-file-version [[_ path content hash]]
@@ -226,6 +226,13 @@
   (let [chan (state/get-events-chan)]
     (async/go-loop []
       (let [payload (async/<! chan)]
-        (handle payload))
+        (try
+          (handle payload)
+          (catch js/Error error
+            (let [type :handle-system-events/failed]
+              (js/console.error (str type) (clj->js payload) "\n" error)
+              (state/pub-event! [:instrument {:type    type
+                                              :payload payload
+                                              :error error}])))))
       (recur))
     chan))

+ 6 - 5
src/main/frontend/handler/file.cljs

@@ -157,11 +157,12 @@
                    (p/let [delete-blocks (db/delete-file-blocks! repo-url file)
                            [pages blocks] (extract-handler/extract-blocks-pages repo-url file content utf8-content)
                            _ (when-let [current-file (page-exists-in-another-file (first pages) file)]
-                               (let [error (str "Page already exists with another file: " current-file)]
-                                 (state/pub-event! [:notification/show
-                                                    {:content error
-                                                     :status :error
-                                                     :clear? false}])))
+                               (when (not= file current-file)
+                                 (let [error (str "Page already exists with another file: " current-file ", current file: " file)]
+                                   (state/pub-event! [:notification/show
+                                                      {:content error
+                                                       :status :error
+                                                       :clear? false}]))))
                            blocks (remove-non-exists-refs! blocks)
                            block-ids (map (fn [block] {:block/uuid (:block/uuid block)}) blocks)
                            pages (extract-handler/with-ref-pages pages blocks)]

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

@@ -115,9 +115,10 @@
                           (seq tagged-pages)
                           (seq namespaces))
             linked (set (flatten links))
+            build-in-pages (set (map string/lower-case default-db/built-in-pages-names))
             nodes (cond->> (map :block/name pages-after-journal-filter)
                     (not builtin-pages?)
-                    (remove (fn [p] (default-db/built-in-pages-names (string/upper-case p))))
+                    (remove (fn [p] (contains? build-in-pages (string/lower-case p))))
                     (not orphan-pages?)
                     (filter #(contains? linked (string/lower-case %))))
             page-links (reduce (fn [m [k v]] (-> (update m k inc)

+ 2 - 2
src/main/frontend/handler/notification.cljs

@@ -23,7 +23,7 @@
                                                      uid {:content content
                                                           :status status}))
 
-     (when clear?
-       (js/setTimeout #(clear! uid) 3000))
+     (when (and clear? (not= status :error))
+       (js/setTimeout #(clear! uid) 1500))
 
      uid)))

+ 14 - 14
src/main/frontend/handler/page.cljs

@@ -294,7 +294,8 @@
       (config-handler/set-config! :favorites favorites))))
 
 (defn delete!
-  [page-name ok-handler]
+  [page-name ok-handler & {:keys [delete-file?]
+                           :or {delete-file? true}}]
   (when page-name
     (when-let [repo (state/get-current-repo)]
       (let [page-name (string/lower-case page-name)
@@ -305,7 +306,7 @@
                      blocks)]
         (db/transact! tx-data)
 
-        (delete-file! repo page-name)
+        (when delete-file? (delete-file! repo page-name))
 
         ;; if other page alias this pagename,
         ;; then just remove some attrs of this entity instead of retractEntity
@@ -339,11 +340,8 @@
         page-ids (->> (map :block/page blocks)
                       (remove nil?)
                       (set))
-        tx       (->> (map (fn [{:block/keys [uuid title content properties] :as block}]
-                             (let [title      (let [title' (walk-replace-old-page! title old-original-name new-name)]
-                                                (when-not (= title' title)
-                                                  title'))
-                                   content    (let [content' (replace-old-page! content old-original-name new-name)]
+        tx       (->> (map (fn [{:block/keys [uuid title content properties format pre-block?] :as block}]
+                             (let [content    (let [content' (replace-old-page! content old-original-name new-name)]
                                                 (when-not (= content' content)
                                                   content'))
                                    properties (let [properties' (walk-replace-old-page! properties old-original-name new-name)]
@@ -351,12 +349,13 @@
                                                   properties'))]
                                (when (or title content properties)
                                  (util/remove-nils-non-nested
-                                  {:block/uuid       uuid
-                                   :block/title      title
-                                   :block/content    content
-                                   :block/properties properties
-                                   :block/refs (rename-update-block-refs! (:block/refs block) (:db/id page) (:db/id to-page))
-                                   :block/path-refs (rename-update-block-refs! (:block/path-refs block) (:db/id page) (:db/id to-page))})))) blocks)
+                                  (merge
+                                   {:block/uuid       uuid
+                                    :block/content    content
+                                    :block/properties properties
+                                    :block/refs (rename-update-block-refs! (:block/refs block) (:db/id page) (:db/id to-page))
+                                    :block/path-refs (rename-update-block-refs! (:block/path-refs block) (:db/id page) (:db/id to-page))}
+                                   (block/parse-title-and-body format pre-block? content)))))) blocks)
                       (remove nil?))]
     (db/transact! repo tx)
     (doseq [page-id page-ids]
@@ -697,7 +696,8 @@
 (defn create-today-journal!
   []
   (when-let [repo (state/get-current-repo)]
-    (when (state/enable-journals? repo)
+    (when (and (state/enable-journals? repo)
+               (not (:repo/loading-files? @state/state)))
       (state/set-today! (date/today))
       (when (or (db/cloned? repo)
                 (or (config/local-db? repo)

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

@@ -20,9 +20,11 @@
     (route-fn to path-params query-params)))
 
 (defn redirect-to-home!
-  []
-  (state/pub-event! [:redirect-to-home])
-  (redirect! {:to :home}))
+  ([]
+   (redirect-to-home! true))
+  ([pub-event?]
+   (when pub-event? (state/pub-event! [:redirect-to-home]))
+   (redirect! {:to :home})))
 
 (defn redirect-to-page!
   ([page-name]

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

@@ -21,11 +21,6 @@
   (when-let [elem (gdom/getElement "close-left-bar")]
     (.click elem)))
 
-(defn toggle-left-sidebar!
-  []
-  (state/set-left-sidebar-open!
-    (not (@state/state :ui/left-sidebar-open?))))
-
 (defn hide-right-sidebar
   []
   (state/hide-right-sidebar!))

+ 1 - 1
src/main/frontend/handler/web/nfs.cljs

@@ -129,7 +129,7 @@
                                      (swap! path-handles assoc path handle))))
              _ (state/set-loading-files! true)
              _ (when-not (state/home?)
-                 (route-handler/redirect-to-home!))
+                 (route-handler/redirect-to-home! false))
              root-handle (first result)
              dir-name (if nfs?
                         (gobj/get root-handle "name")

+ 26 - 3
src/main/frontend/modules/outliner/core.cljs

@@ -2,6 +2,7 @@
   (:require [clojure.set :as set]
             [clojure.zip :as zip]
             [frontend.db :as db]
+            [frontend.db.model :as db-model]
             [frontend.db-schema :as db-schema]
             [frontend.db.conn :as conn]
             [frontend.db.outliner :as db-outliner]
@@ -82,6 +83,24 @@
         ]
     block))
 
+(defn- remove-orphaned-page-refs!
+  [db-id txs-state old-refs new-refs]
+  (when (not= old-refs new-refs)
+    (let [new-refs (set (map :block/name new-refs))
+          old-pages (->> (map :db/id old-refs)
+                         (db-model/get-entities-by-ids)
+                         (remove (fn [e] (contains? new-refs (:block/name e))))
+                         (map :block/name)
+                         (remove nil?))
+          orphaned-pages (db-model/get-orphaned-pages {:pages old-pages
+                                                       :empty-ref-f (fn [page]
+                                                                      (let [refs (:block/_refs page)]
+                                                                        (or (zero? (count refs))
+                                                                            (= #{db-id} (set (map :db/id refs))))))})]
+      (when (seq orphaned-pages)
+        (let [tx (mapv (fn [page] [:db/retractEntity (:db/id page)]) orphaned-pages)]
+          (swap! txs-state (fn [state] (vec (concat state tx)))))))))
+
 ;; -get-id, -get-parent-id, -get-left-id return block-id
 ;; the :block/parent, :block/left should be datascript lookup ref
 
@@ -142,7 +161,10 @@
                 (util/remove-nils))
           m (if (state/enable-block-timestamps?) (block-with-timestamps m) m)
           other-tx (:db/other-tx m)
-          id (:db/id (:data this))]
+          id (:db/id (:data this))
+          block-entity (db/entity id)
+          old-refs (:block/refs block-entity)
+          new-refs (:block/refs m)]
       (when (seq other-tx)
         (swap! txs-state (fn [txs]
                            (vec (concat txs other-tx)))))
@@ -155,13 +177,14 @@
                                            [:db/retract id attribute])
                                       db-schema/retract-attributes)))))
 
-        (when-let [e (:block/page (db/entity id))]
+        (when-let [e (:block/page block-entity)]
           (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))))
+            (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))
 

+ 13 - 2
src/main/frontend/modules/outliner/datascript.cljc

@@ -44,9 +44,20 @@
                editor-cursor (state/get-current-edit-block-and-position)
                meta (merge opts {:editor-cursor editor-cursor})
                rs (d/transact! conn txs meta)]
+           (when true                 ; TODO: add debug flag
+             (let [eids (distinct (mapv first (:tx-data rs)))
+                   left&parent-list (->>
+                                     (d/q '[:find ?e ?l ?p
+                                            :in $ [?e ...]
+                                            :where
+                                            [?e :block/left ?l]
+                                            [?e :block/parent ?p]] @conn eids)
+                                     (vec)
+                                     (map next))]
+               (assert (= (count left&parent-list) (count (distinct left&parent-list))) eids)))
            (when-not config/test?
-            (after-transact-pipelines rs))
-          rs)
+             (after-transact-pipelines rs))
+           rs)
          (catch js/Error e
            (log/error :exception e)
            (throw e))))))

+ 3 - 2
src/main/frontend/modules/outliner/file.cljs

@@ -37,8 +37,9 @@
         (try (do-write-file! page-db-id)
              (catch js/Error e
                (notification/show!
-                "Write file failed, please copy the changes to other editors in case of losing data."
-                [:div "Error: " (str (gobj/get e "stack"))]
+                [:div
+                 [:p "Write file failed, please copy the changes to other editors in case of losing data."]
+                 "Error: " (str (gobj/get e "stack"))]
                 :error)
                (log/error :file/write-file-error {:error e})))))))
 

+ 3 - 4
src/main/frontend/modules/outliner/pipeline.cljs

@@ -15,10 +15,9 @@
         path (:file/path (:block/file page))
         page-title (or (:block/original-name page)
                        (:block/name page))]
-    ;; (when (util/electron?)
-    ;;   (debug/set-ack-step! path :start-writing)
-    ;;   (debug/wait-for-write-ack! page-title path))
-    )
+    (when (util/electron?)
+      (debug/set-ack-step! path :start-writing)
+      (debug/wait-for-write-ack! page-title path)))
   (file/sync-to-file page))
 
 (defn invoke-hooks

+ 1 - 1
src/main/frontend/modules/shortcut/config.cljs

@@ -352,7 +352,7 @@
 
    :ui/toggle-left-sidebar          {:desc    "Toggle left sidebar"
                                      :binding "t l"
-                                     :fn      ui-handler/toggle-left-sidebar!}
+                                     :fn      state/toggle-left-sidebar!}
 
    :ui/toggle-help                  {:desc    "Toggle help"
                                      :binding "shift+/"

+ 8 - 1
src/main/frontend/state.cljs

@@ -77,7 +77,9 @@
                               false)
       ;; remember scroll positions of visited paths
       :ui/paths-scroll-positions {}
-      :ui/shortcut-tooltip? (or (storage/get :ui/shortcut-tooltip?) true)
+      :ui/shortcut-tooltip? (if (false? (storage/get :ui/shortcut-tooltip?))
+                              false
+                              true)
 
       :document/mode? document-mode?
 
@@ -1148,6 +1150,11 @@
   (storage/set "ls-left-sidebar-open?" (boolean value))
   (set-state! :ui/left-sidebar-open? value))
 
+(defn toggle-left-sidebar!
+  []
+  (set-left-sidebar-open!
+    (not (get-left-sidebar-open?))))
+
 (defn set-developer-mode!
   [value]
   (set-state! :ui/developer-mode? value)

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

@@ -1453,3 +1453,13 @@
            button (gobj/get e "button")]
        (or (= which 3)
            (= button 2)))))
+
+#?(:cljs
+   (defn url?
+     [s]
+     (and (string? s)
+          (try
+            (js/URL. s)
+            true
+            (catch js/Error _e
+              false)))))

+ 3 - 0
src/main/frontend/util/keycode.cljs

@@ -2,3 +2,6 @@
 
 (def left-square-bracket 219)
 (def left-paren 57)
+
+(def left-square-bracket-code "BracketLeft")
+(def left-paren-code "Digit9")

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

@@ -1,3 +1,3 @@
 (ns frontend.version)
 
-(defonce version "0.4.9")
+(defonce version "0.5.1")

+ 16 - 0
src/main/logseq/api.cljs

@@ -288,6 +288,22 @@
     (when (re-find #"https?://" url)
       (js/apis.openExternal url))))
 
+;; flag - boolean | 'toggle'
+(def ^:export set_left_sidebar_visible
+  (fn [flag]
+    (if (= flag "toggle")
+      (state/toggle-left-sidebar!)
+      (state/set-state! :ui/left-sidebar-open? (boolean flag)))
+    nil))
+
+;; flag - boolean | 'toggle'
+(def ^:export set_right_sidebar_visible
+  (fn [flag]
+    (if (= flag "toggle")
+      (state/toggle-sidebar-open?!)
+      (state/set-state! :ui/sidebar-open? (boolean flag)))
+    nil))
+
 (def ^:export push_state
   (fn [^js k ^js params ^js query]
     (rfe/push-state

+ 2 - 2
src/test/frontend/db/query_dsl_test.cljs

@@ -319,7 +319,7 @@ last-modified-at:: 1609084800002"}]]
       "(not [[page 1]])"
       {:query '([?b :block/uuid]
                 (not [?b :block/path-refs [:block/name "page 1"]]))
-       :count 35}))
+       :count 36}))
 
   (testing "Between query"
     (are [x y] (= (count-only x) y)
@@ -367,7 +367,7 @@ last-modified-at:: 1609084800002"}]]
                   (and [?b :block/path-refs [:block/name "page 1"]])
                   (and [?b :block/path-refs [:block/name "page 2"]])
                   [?b])))
-       :count 38})
+       :count 39})
 
     ;; FIXME: not working
     ;; (are [x y] (= (q-count x) y)

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio