Explorar o código

Fix: Delete and backspace deleting block ids and breaking references (#8974)

* press delete at the end of a block, if no refs in current block,
delete current block instead of next

* enhance backspace behavior to avoid broke ref

* fix press backspace at the end of the block of the end of the page issue

* add e2e-test for delete and backspace across blocks

---------

Co-authored-by: Junyi Du <[email protected]>
Co-authored-by: Tienson Qin <[email protected]>
Co-authored-by: Gabriel Horner <[email protected]>
megayu %!s(int64=2) %!d(string=hai) anos
pai
achega
c90e266310
Modificáronse 2 ficheiros con 157 adicións e 39 borrados
  1. 88 2
      e2e-tests/basic.spec.ts
  2. 69 37
      src/main/frontend/handler/editor.cljs

+ 88 - 2
e2e-tests/basic.spec.ts

@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
 import fs from 'fs/promises'
 import path from 'path'
 import { test } from './fixtures'
-import { randomString, createRandomPage } from './utils'
+import { randomString, createRandomPage, modKey } from './utils'
 
 
 test('create page and blocks, save to disk', async ({ page, block, graphDir }) => {
@@ -82,7 +82,93 @@ test('delete and backspace', async ({ page, block }) => {
   await page.keyboard.press('Delete', { delay: 50 })
   expect(await page.inputValue('textarea >> nth=0')).toBe('te')
 
-  // TODO: test delete & backspace across blocks
+  // delete & backspace across blocks
+  await block.enterNext()
+  await block.mustFill('test')
+  await page.keyboard.press('ArrowUp', { delay: 50 })
+  await page.keyboard.press('Delete', { delay: 50 })
+  expect(await page.inputValue('textarea >> nth=0')).toBe('tetest')
+  await block.enterNext()
+  expect(await page.inputValue('textarea >> nth=0')).toBe('test')
+  await page.keyboard.press('Backspace', { delay: 50 })
+  expect(await page.inputValue('textarea >> nth=0')).toBe('tetest')
+
+  // delete across blocks, the current block has no refs
+  await block.clickNext()
+  await block.mustFill('no ref')
+  await block.enterNext()
+  await block.mustType('has a ref')
+  await page.keyboard.press(modKey + '+c')
+  await page.waitForTimeout(100)
+  await block.clickNext()
+  await page.keyboard.press(modKey + '+v')
+  await page.waitForTimeout(100)
+  await page.keyboard.press('ArrowUp', { delay: 50 })
+  await page.keyboard.press('ArrowUp', { delay: 50 })
+  await page.keyboard.press('End')
+  await page.waitForTimeout(100)
+  await page.keyboard.press('Delete', { delay: 50 })
+  await page.waitForTimeout(100)
+  expect(await page.inputValue('textarea >> nth=0')).toBe('no refhas a ref')
+  await expect(page.locator('.warning')).toHaveCount(0)
+
+  // delete across blocks, the current block has refs and the next block has no refs
+  await page.keyboard.press('Enter')
+  await page.keyboard.press('ArrowUp', { delay: 50 })
+  await page.keyboard.press('End')
+  await page.waitForTimeout(100)
+  await page.keyboard.press('Delete', { delay: 50 })
+  await page.waitForTimeout(100)
+  expect(await page.inputValue('textarea >> nth=0')).toBe('no refhas a ref')
+  await expect(page.locator('.warning')).toHaveCount(0)
+
+  // delete across blocks, the current block and the next block both have refs
+  await page.keyboard.press('Enter')
+  await page.keyboard.press(modKey + '+c')
+  await page.waitForTimeout(100)
+  await page.keyboard.press('End')
+  await page.keyboard.press('Enter')
+  await page.keyboard.press(modKey + '+v')
+  await page.waitForTimeout(100)
+  await page.keyboard.press('ArrowUp', { delay: 50 })
+  await page.keyboard.press('ArrowUp', { delay: 50 })
+  await page.keyboard.press('End')
+  await page.waitForTimeout(100)
+  await page.keyboard.press('Delete', { delay: 50 })
+  await page.waitForTimeout(100)
+  expect(await page.inputValue('textarea >> nth=0')).not.toBe('no refhas a ref')
+  expect(await page.inputValue('textarea >> nth=0')).toContain('no refhas a ref')
+  await expect(page.locator('.warning')).toHaveCount(1)
+
+  // backspace across blocks, the current block has refs and the prev block has no refs
+  await page.keyboard.press(modKey + '+z')
+  await page.waitForTimeout(100)
+  await page.keyboard.press('Home')
+  await page.waitForTimeout(100)
+  await page.keyboard.press('Backspace', { delay: 50 })
+  await page.waitForTimeout(100)
+  expect(await page.inputValue('textarea >> nth=0')).toBe('tetestno ref')
+  await expect(page.locator('.warning')).toHaveCount(0)
+
+  // backspace across blocks, the current block has no refs
+  await page.keyboard.press('End')
+  await page.keyboard.press('Enter')
+  await block.mustFill('no ref')
+  await page.keyboard.press('Home')
+  await page.waitForTimeout(100)
+  await page.keyboard.press('Backspace', { delay: 50 })
+  await page.waitForTimeout(100)
+  expect(await page.inputValue('textarea >> nth=0')).toBe('tetestno refno ref')
+  await expect(page.locator('.warning')).toHaveCount(0)
+
+  // backspace across blocks, the current block and the prev block both have refs
+  await page.keyboard.press('ArrowDown', { delay: 50 })
+  await page.keyboard.press('Home')
+  await page.waitForTimeout(100)
+  await page.keyboard.press('Backspace', { delay: 50 })
+  await page.waitForTimeout(100)
+  expect(await page.inputValue('textarea >> nth=0')).toBe('tetestno refno refhas a ref')
+  await expect(page.locator('.warning')).toHaveCount(1)
 })
 
 

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

@@ -362,6 +362,7 @@
                 block
                 (dissoc block :block/pre-block?))
         block (update block :block/refs remove-non-existed-refs!)
+        block (if (and left (not= (:block/left block) left)) (assoc block :block/left left) block)
         new-properties (merge
                         (select-keys properties (property/hidden-properties))
                         (:block/properties block))]
@@ -778,29 +779,37 @@
        (outliner-core/delete-blocks! [block] {:children? children?})))))
 
 (defn- move-to-prev-block
-  [repo sibling-block format id value]
-  (when (and repo sibling-block)
-    (when-let [sibling-block-id (dom/attr sibling-block "blockid")]
-      (when-let [block (db/pull repo '[*] [:block/uuid (uuid sibling-block-id)])]
-        (let [original-content (util/trim-safe (:block/content block))
-              value' (-> (property/remove-built-in-properties format original-content)
-                         (drawer/remove-logbook))
-              new-value (str value' value)
-              tail-len (count value)
-              pos (max
-                   (if original-content
-                     (gobj/get (utf8/encode original-content) "length")
-                     0)
-                   0)]
-          (edit-block! block pos id
-                       {:custom-content new-value
-                        :tail-len tail-len
-                        :move-cursor? false})
-          {:prev-block block
-           :new-content new-value})))))
+  ([repo sibling-block format id value]
+   (move-to-prev-block repo sibling-block format id value true))
+  ([repo sibling-block format id value edit?]
+   (when (and repo sibling-block)
+     (when-let [sibling-block-id (dom/attr sibling-block "blockid")]
+       (when-let [block (db/pull repo '[*] [:block/uuid (uuid sibling-block-id)])]
+         (let [original-content (util/trim-safe (:block/content block))
+               value' (-> (property/remove-built-in-properties format original-content)
+                          (drawer/remove-logbook))
+               new-value (str value' value)
+               tail-len (count value)
+               pos (max
+                    (if original-content
+                      (gobj/get (utf8/encode original-content) "length")
+                      0)
+                    0)]
+           (when edit?
+             (edit-block! block pos id
+                          {:custom-content new-value
+                           :tail-len tail-len
+                           :move-cursor? false}))
+           {:prev-block block
+            :new-content new-value
+            :pos pos}))))))
 
 (declare save-block!)
 
+(defn- block-has-no-ref?
+  [eid]
+  (empty? (:block/_refs (db/entity eid))))
+
 (defn delete-block!
   ([repo]
    (delete-block! repo true))
@@ -808,11 +817,11 @@
    (state/set-editor-op! :delete)
    (let [{:keys [id block-id block-parent-id value format]} (get-state)]
      (when block-id
-       (let [page-id (:db/id (:block/page (db/entity [:block/uuid block-id])))
+       (let [block (db/entity [:block/uuid block-id])
+             page-id (:db/id (:block/page block))
              page-blocks-count (and page-id (db/get-page-blocks-count repo page-id))]
          (when (> page-blocks-count 1)
-           (let [block (db/entity [:block/uuid block-id])
-                 has-children? (seq (:block/_parent block))
+           (let [has-children? (seq (:block/_parent block))
                  block (db/pull (:db/id block))
                  left (tree/-get-left (outliner-core/block block))
                  left-has-children? (and left
@@ -822,18 +831,31 @@
              (when-not (and has-children? left-has-children?)
                (when block-parent-id
                  (let [block-parent (gdom/getElement block-parent-id)
+                       ;; it's possible to find a block not belong to the same page of current editing block
                        sibling-block (util/get-prev-block-non-collapsed-non-embed block-parent)
-                       {:keys [prev-block new-content]} (move-to-prev-block repo sibling-block format id value)
+                       delete_prev? (and (not (block-has-no-ref? (:db/id block)))
+                                         (block-has-no-ref? [:block/uuid (uuid (dom/attr sibling-block "blockid"))]))
+                       {:keys [prev-block new-content pos]} (move-to-prev-block repo sibling-block format id value (not delete_prev?))
                        concat-prev-block? (boolean (and prev-block new-content))
+                       save-page? (= (:db/id (:block/page prev-block)) page-id)
                        transact-opts (cond->
                                        {:outliner-op :delete-block}
                                        concat-prev-block?
                                        (assoc :concat-data
                                               {:last-edit-block (:block/uuid block)}))]
-                   (outliner-tx/transact! transact-opts
-                     (when concat-prev-block?
-                       (save-block! repo prev-block new-content))
-                     (delete-block-aux! block delete-children?))))))))))
+                   (if (and delete_prev? save-page?)
+                     (let [block (assoc block :block/left (:block/left prev-block))
+                           input (gdom/getElement id)]
+                       (outliner-tx/transact! transact-opts
+                                              (delete-block-aux! prev-block false)
+                                              (save-block! repo block new-content))
+                       (state/set-edit-content! id new-content)
+                       (cursor/move-cursor-to input pos))
+                     (outliner-tx/transact! transact-opts
+                                            (when concat-prev-block?
+                                              (save-block! repo prev-block new-content))
+                                            (delete-block-aux! block delete-children?)))
+                   ))))))))
    (state/set-editor-op! nil)))
 
 (defn delete-blocks!
@@ -2645,11 +2667,9 @@
   (util/safe-set-range-text! input "" start end)
   (state/set-edit-content! (state/get-edit-input-id) (.-value input)))
 
-(defn- delete-concat [current-block]
+(defn- delete-concat
+  [current-block input current-pos value]
   (let [input-id (state/get-edit-input-id)
-        ^js input (state/get-input)
-        current-pos (cursor/pos input)
-        value (gobj/get input "value")
         right (outliner-core/get-right-node (outliner-core/block current-block))
         current-block-has-children? (db/has-children? (:block/uuid current-block))
         collapsed? (util/collapsed? current-block)
@@ -2664,17 +2684,29 @@
       (and (not collapsed?) first-child (db/has-children? (:block/uuid first-child)))
       nil
 
-      :else
+      (and next-block (block-has-no-ref? (:db/id current-block)))
       (let [edit-block (state/get-edit-block)
             transact-opts {:outliner-op :delete-block
                            :concat-data {:last-edit-block (:block/uuid edit-block)
                                          :end? true}}
             new-content (str value "" (:block/content next-block))
+            next-block (assoc next-block :block/left (:block/left current-block))
             repo (state/get-current-repo)]
         (outliner-tx/transact! transact-opts
-          (save-block! repo edit-block new-content)
-          (delete-block-aux! next-block false))
+                               (delete-block-aux! edit-block false)
+                               (save-block! repo next-block new-content))
+        (edit-block! next-block current-pos (:block/uuid next-block)))
 
+      :else
+      (let [edit-block (state/get-edit-block)
+            transact-opts {:outliner-op :delete-block
+                           :concat-data {:last-edit-block (:block/uuid edit-block)
+                                         :end? true}}
+            new-content (str value "" (:block/content next-block))
+            repo (state/get-current-repo)]
+        (outliner-tx/transact! transact-opts
+                               (save-block! repo edit-block new-content)
+                               (delete-block-aux! next-block false))
         (state/set-edit-content! input-id new-content)
         (cursor/move-cursor-to input current-pos)))))
 
@@ -2696,8 +2728,8 @@
         (let [editor-state (get-state)
               custom-query? (get-in editor-state [:config :custom-query?])]
           (when-not custom-query?
-            (delete-concat current-block)))
-
+            (delete-concat current-block input current-pos value)))
+            
         :else
         (delete-and-update input current-pos (inc current-pos))))))