Ver código fonte

Merge remote-tracking branch 'upstream/master' into whiteboards

Peng Xiao 3 anos atrás
pai
commit
d4c0ae900b

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

@@ -180,7 +180,7 @@ test('copy & paste block ref and replace its content', async ({ page, block }) =
     }
 })
 
-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,7 +197,7 @@ 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")
@@ -211,3 +211,303 @@ test('copy and paste block after editing new block', async ({ page, block }) =>
 
   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.mustFill('text1 ')
+  await page.waitForTimeout(550) // Wait for 500ms autosave period to expire
+
+  // Then type more, start an action prompt, and undo
+  await page.keyboard.type('text2 ')
+  for (const char of '[[') {
+    await page.keyboard.type(char)
+  }
+  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 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)
+  }
+})

+ 25 - 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
@@ -217,6 +217,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);
   },

+ 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' })
 

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

@@ -2404,7 +2404,6 @@
     (when (and
            (state/in-selection-mode?)
            (non-dragging? e))
-      (util/stop e)
       (editor-handler/highlight-selection-area! block-id))))
 
 (defn- block-mouse-leave
@@ -2463,7 +2462,9 @@
         *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 (or navigated?
+                      custom-query?
+                      (and ref? (:block/uuid config)))
                 (let [block (db/pull [:block/uuid navigating-block])
                       blocks (db/get-paginated-blocks repo (:db/id block)
                                                       {:scoped-block-id (:db/id block)})
@@ -2508,8 +2509,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
@@ -2848,7 +2848,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))
@@ -3331,7 +3332,7 @@
          {: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

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

@@ -359,7 +359,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))

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

@@ -302,8 +302,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
@@ -335,7 +339,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
@@ -380,6 +384,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))
@@ -387,13 +392,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]
@@ -412,6 +417,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!
@@ -422,11 +428,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))
 
@@ -436,7 +442,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?)
 
@@ -510,10 +516,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"}
@@ -522,6 +528,7 @@
      (fn [_]
        (absolute-modal
         component
+        modal-name
         set-default-width?
         pos)))))
 
@@ -557,7 +564,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)

+ 6 - 6
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,9 +56,9 @@
         (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))
+         (fn [] (blocks-cp repo page))
          {:debug-id (str "journal-blocks " page)}))
 
       {})
@@ -77,9 +77,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/reference.cljs

@@ -147,7 +147,7 @@
       :init-collapsed (fn [collapsed-atom]
                         (reset! *collapsed? collapsed-atom))})))
 
-(rum/defcs references* < rum/reactive
+(rum/defcs references* < rum/reactive db-mixins/query
   {:init (fn [state]
            (let [page-name (first (:rum/args state))
                  filters (when page-name

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

@@ -378,6 +378,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
   ([]

+ 80 - 88
src/main/frontend/db/model.cljs

@@ -356,28 +356,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]
@@ -450,6 +458,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))
@@ -458,11 +471,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]
@@ -477,13 +485,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]
@@ -859,17 +860,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."
@@ -1173,21 +1171,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]
-                   {}
-                   '[:find [(pull ?block ?block-attrs) ...]
-                     :in $ [?ref-page ...] ?block-attrs
-                     :where
-                     [?block :block/path-refs ?ref-page]]
-                   pages
-                   (butlast block-attrs))
-          react
+          (d/q
+            '[:find [(pull ?block ?block-attrs) ...]
+              :in $ [?ref-page ...] ?block-attrs
+              :where
+              [?block :block/path-refs ?ref-page]]
+            db
+            pages
+            (butlast block-attrs))
           (remove (fn [block] (= page-id (:db/id (:block/page block)))))
           db-utils/group-by-page
           (map (fn [[k blocks]]
@@ -1209,17 +1205,16 @@
              aliases (set/difference pages #{page-id})]
          (->>
           (react/q repo
-                   [:frontend.db.react/page<-blocks-or-block<-blocks page-id]
-                   {:use-cache? false
-                    :query-fn (fn []
-                                (let [entities (mapcat (fn [id]
-                                                         (:block/_path-refs (db-utils/entity id))) pages)
-                                      blocks (map (fn [e] {:block/parent (:block/parent e)
-                                                           :block/left (:block/left e)
-                                                           :block/page (:block/page e)}) entities)]
-                                  {:entities entities
-                                   :blocks blocks}))}
-                   nil)
+            [:frontend.db.react/refs page-id]
+            {:query-fn (fn []
+                         (let [entities (mapcat (fn [id]
+                                                  (:block/_path-refs (db-utils/entity id))) pages)
+                               blocks (map (fn [e] {:block/parent (:block/parent e)
+                                                    :block/left (:block/left e)
+                                                    :block/page (:block/page e)}) entities)]
+                           {:entities entities
+                            :blocks blocks}))}
+            nil)
           react
           :entities
           (remove (fn [block] (= page-id (:db/id (:block/page block)))))
@@ -1239,16 +1234,15 @@
           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))
+                     @(react/q repo [:frontend.db.react/refs-count id] {}
+                        '[:find [?block ...]
+                          :in $ [?ref-page ...] ?id
+                          :where
+                          [?block :block/refs ?ref-page]
+                          [?block :block/page ?p]
+                          [(not= ?p ?id)]]
+                        pages
+                        id))
                    (:block/_refs block))]
       (count result))))
 
@@ -1312,8 +1306,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 {}))
@@ -1321,16 +1313,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))))))

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

@@ -24,38 +24,29 @@
 ;; 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?))
+;; ::refs-count
+;; get refs count
+(s/def ::refs-count 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
+                                :refs-count ::refs-count
                                 :custom ::custom))
 
 (s/def ::affected-keys (s/coll-of ::react-query-keys))
@@ -125,13 +116,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 +133,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 +214,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] (contains? #{:block/refs :block/path-refs} (:a 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' (mapcat (fn [ref]
+                                                     [
+                                                      ;; [::refs-count (:db/id ref)]
+                                                      [::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-count (:db/id entity)]
+                           [::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 +324,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 +343,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]

+ 37 - 45
src/main/frontend/handler/editor.cljs

@@ -205,8 +205,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
@@ -1073,8 +1073,11 @@
   (when-let [blocks (seq (get-selected-blocks))]
     ;; 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)]
+                              (or (= "true" (dom/attr block "data-transclude"))
+                                  (= "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))
@@ -1220,8 +1223,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]
@@ -1236,18 +1238,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)
@@ -1255,7 +1257,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)
@@ -1734,6 +1736,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.)
@@ -1757,41 +1766,24 @@
 
     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!))))))
+    (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!)))))
 
 (defn resize-image!
   [block-id metadata full_text size]
@@ -1821,7 +1813,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})
@@ -2816,7 +2808,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)
@@ -2889,7 +2881,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")
@@ -2937,11 +2929,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
 )))

+ 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))

+ 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)))

+ 11 - 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,21 @@
      (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 (util/input? dom-node))
+                                  (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 "mouseup" click-fn)))
         (listen state js/window "keydown"
                 (fn [e]
                   (case (.-keyCode e)

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

@@ -46,18 +46,21 @@
   [repo page-db-id]
   (let [page-block (db/pull repo '[*] page-db-id)
         page-db-id (:db/id page-block)
-        whiteboard? (:block/whiteboard? page-block)
-        pull-keys (if whiteboard? whiteboard-blocks-pull-keys-with-persisted-ids '[*])
-        blocks (model/get-page-blocks-no-cache repo (:block/name page-block) {:pull-keys pull-keys})
-        blocks (if whiteboard? (map cleanup-whiteboard-block blocks) blocks)]
-
-    (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 (if whiteboard? blocks 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 [whiteboard? (:block/whiteboard? page-block)
+            pull-keys (if whiteboard? whiteboard-blocks-pull-keys-with-persisted-ids '[*])
+            blocks (model/get-page-blocks-no-cache repo (:block/name page-block) {:pull-keys pull-keys})
+            blocks (if whiteboard? (map cleanup-whiteboard-block blocks) blocks)]
+        (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 (if whiteboard? blocks tree))
+              (js/console.error (str "can't find page id: " page-db-id)))))))))
 
 (defn write-files!
   [pages]

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

@@ -23,7 +23,10 @@
 (defn compute-block-path-refs
   [tx-meta blocks]
   (let [repo (state/get-current-repo)
-        blocks (remove :block/name blocks)]
+        blocks (remove :block/name blocks)
+        blocks (if (= (:outliner-op tx-meta) :insert-blocks)
+                 (butlast blocks)
+                 blocks)]
     (when (:outliner-op tx-meta)
       (when (react/path-refs-need-recalculated? tx-meta)
         (let [*computed-ids (atom #{})]
@@ -61,7 +64,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-meta 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)

+ 18 - 23
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,8 +75,14 @@
                           (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 non-consecutive-blocks->vec-tree
   "`blocks` need to be in the same page."
@@ -84,26 +90,15 @@
   (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/left {:db/id (:db/id (:block/left 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')))
+        parent->children (group-by :block/parent blocks)
+        id->blocks (zipmap (map :db/id blocks) blocks)
+        top-level-blocks (filter #(nil?
+                                   (id->blocks
+                                    (:db/id (:block/parent (id->blocks (:db/id %)))))) blocks)
+        top-level-blocks' (model/try-sort-by-left top-level-blocks (:block/parent (first top-level-blocks)))]
+    (map #(tree parent->children %) top-level-blocks')))
 
 (defn- sort-blocks-aux
   [parents parent-groups]

+ 30 - 9
src/main/frontend/state.cljs

@@ -708,13 +708,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)
@@ -732,7 +744,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)
@@ -741,10 +752,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
@@ -1344,12 +1363,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)))))
 
@@ -1652,7 +1672,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]

+ 2 - 2
src/main/frontend/util.cljc

@@ -1075,6 +1075,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 +1089,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])]