Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 12 commits into from
Apr 12, 2023
Merged
90 changes: 88 additions & 2 deletions e2e-tests/basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -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()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

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


Expand Down
105 changes: 68 additions & 37 deletions src/main/frontend/handler/editor.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down Expand Up @@ -778,41 +779,49 @@
(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?
[block-or-uuid]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: It could be nice to keep this fn simpler and have it just take an entity id (eid) like db/pull or https://cljdoc.org/d/datascript/datascript/1.4.2/api/datascript.core#entity. This would mean passing in the db/entity arg

(empty? (:block/_refs (db/entity (if (uuid? block-or-uuid) [:block/uuid block-or-uuid] (:db/id block-or-uuid))))))

(defn delete-block!
([repo]
(delete-block! repo true))
([repo delete-children?]
(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
Expand All @@ -822,18 +831,30 @@
(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? block)) (block-has-no-ref? (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!
Expand Down Expand Up @@ -2645,11 +2666,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)
Expand All @@ -2664,17 +2683,29 @@
(and (not collapsed?) first-child (db/has-children? (:block/uuid first-child)))
nil

:else
(and next-block (block-has-no-ref? 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)))))

Expand All @@ -2696,8 +2727,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))))))

Expand Down