Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 8f7a10fb0e fix(twenty-front): guard BlockNote backspace handler against top-level node position
https://sonarly.com/issue/33776?type=bug

Pressing Backspace at the beginning of the first block in a Dashboard rich text widget throws an unhandled RangeError from ProseMirror, crashing the editor.

Fix: Added a tiptap Extension (`backspaceTopLevelGuard`) that intercepts Backspace keypresses at the beginning of the first top-level block in BlockNote editors, preventing BlockNote 0.47.x's internal parent-block traversal from calling `resolvedPos.before(0)` which throws `RangeError: "There is no position before the top-level node"`.

The extension:
1. Only activates on Backspace with an empty (collapsed) selection
2. Checks if the cursor is at offset 0 within a node at depth ≤ 2 (BlockNote's doc → blockGroup → blockContainer structure)
3. Confirms the position is at the document start (`$from.before($from.depth) === 1`)
4. Returns `true` (handled) to swallow the keypress — there is nothing to delete at this position anyway

Applied to both `useCreateBlockNote` call sites:
- `StandaloneRichTextEditorContent.tsx` (Dashboard rich text widget — the crash site from the Sentry event)
- `RichTextFieldEditor.tsx` (Record field rich text editor — same BlockNote version, same potential crash)

Uses `_tiptapOptions.extensions` to inject the guard extension into BlockNote's underlying tiptap editor.
2026-05-03 22:38:54 +00:00
3 changed files with 46 additions and 0 deletions
@@ -0,0 +1,38 @@
import { Extension } from '@tiptap/core';
// Guards against a BlockNote 0.47.x bug where pressing Backspace at the
// beginning of the first top-level block throws:
// RangeError: "There is no position before the top-level node"
// BlockNote's internal parent-block traversal calls
// resolvedPos.before(depth - 1) without checking that depth > 1.
export const getBackspaceTopLevelGuardExtension = () =>
Extension.create({
name: 'backspaceTopLevelGuard',
priority: 1000,
addKeyboardShortcuts() {
return {
Backspace: ({ editor }) => {
const { $from, empty } = editor.state.selection;
if (!empty) {
return false;
}
// When the cursor is at the very beginning of the first
// top-level block (offset 0 and depth ≤ 2 in BlockNote's
// doc → blockGroup → blockContainer structure), swallow the
// Backspace to prevent the crash. There is nothing to delete
// in this position anyway.
if ($from.parentOffset === 0 && $from.depth <= 2) {
const atDocStart = $from.before($from.depth) === 1;
if (atDocStart) {
return true;
}
}
return false;
},
};
},
});
@@ -5,6 +5,7 @@ import { BLOCK_SCHEMA } from '@/blocknote-editor/blocks/Schema';
import { BlockEditor } from '@/blocknote-editor/components/BlockEditor';
import { BLOCK_EDITOR_GLOBAL_HOTKEYS_CONFIG } from '@/blocknote-editor/constants/BlockEditorGlobalHotkeysConfig';
import { useAttachmentSync } from '@/blocknote-editor/hooks/useAttachmentSync';
import { getBackspaceTopLevelGuardExtension } from '@/blocknote-editor/utils/getBackspaceTopLevelGuardExtension';
import { useReplaceBlockEditorContent } from '@/blocknote-editor/hooks/useReplaceBlockEditorContent';
import { parseInitialBlocknote } from '@/blocknote-editor/utils/parseInitialBlocknote';
import { prepareBodyWithSignedUrls } from '@/blocknote-editor/utils/prepareBodyWithSignedUrls';
@@ -213,6 +214,9 @@ export const RichTextFieldEditor = ({
placeholders: {
default: t`Type '/' for commands, '@' for mentions`,
},
_tiptapOptions: {
extensions: [getBackspaceTopLevelGuardExtension()],
},
});
if (editorRef) {
@@ -12,6 +12,7 @@ import { filterSupportedBlocks } from '@/page-layout/widgets/standalone-rich-tex
import { BLOCK_EDITOR_GLOBAL_HOTKEYS_CONFIG } from '@/blocknote-editor/constants/BlockEditorGlobalHotkeysConfig';
import { useAttachmentSync } from '@/blocknote-editor/hooks/useAttachmentSync';
import { getBackspaceTopLevelGuardExtension } from '@/blocknote-editor/utils/getBackspaceTopLevelGuardExtension';
import { parseInitialBlocknote } from '@/blocknote-editor/utils/parseInitialBlocknote';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
@@ -79,6 +80,9 @@ export const StandaloneRichTextEditorContent = ({
placeholders: {
default: t`Enter text or type '/' for commands`,
},
_tiptapOptions: {
extensions: [getBackspaceTopLevelGuardExtension()],
},
});
const handlePersistBody = useDebouncedCallback((blocknote: string) => {