mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
Fix: Ensure block IDs exist before passing content to BlockNote editor
https://sonarly.com/issue/24449?type=bug When a user navigates to `/objects/notes` and opens a note whose stored `bodyV2.blocknote` JSON contains blocks without `id` fields, BlockNote 0.47.x throws "Error: Block doesn't have id" during ProseMirror EditorView initialization. The error originates in BlockNote's internal node view factory (`yv` function) which reads `node.attrs.id` from each `blockContainer` ProseMirror node and throws if it's falsy. The `parseInitialBlocknote` utility parses stored JSON into `PartialBlock[]` without ensuring blocks have IDs, and while `useCreateBlockNote` is designed to auto-generate IDs for PartialBlocks, certain block structures (nested children, specific block types, or data written under the previous BlockNote 0.31.x format) fail to receive IDs before the ProseMirror view mounts.
This commit is contained in:
parent
21142d98fe
commit
e71539c0ba
4 changed files with 118 additions and 3 deletions
|
|
@ -0,0 +1,63 @@
|
|||
import type { PartialBlock } from '@blocknote/core';
|
||||
|
||||
import { ensureBlockIds } from '@/blocknote-editor/utils/ensureBlockIds';
|
||||
|
||||
const UUID_REGEX =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
||||
|
||||
describe('ensureBlockIds', () => {
|
||||
it('should add id to a block without one', () => {
|
||||
const blocks: PartialBlock[] = [{ type: 'paragraph', content: 'hello' }];
|
||||
const result = ensureBlockIds(blocks);
|
||||
|
||||
expect(result[0].id).toMatch(UUID_REGEX);
|
||||
});
|
||||
|
||||
it('should preserve existing valid id', () => {
|
||||
const blocks: PartialBlock[] = [
|
||||
{ id: 'existing-id', type: 'paragraph', content: 'hello' },
|
||||
];
|
||||
const result = ensureBlockIds(blocks);
|
||||
|
||||
expect(result[0].id).toBe('existing-id');
|
||||
});
|
||||
|
||||
it('should replace empty string id', () => {
|
||||
const blocks: PartialBlock[] = [
|
||||
{ id: '', type: 'paragraph', content: 'hello' },
|
||||
];
|
||||
const result = ensureBlockIds(blocks);
|
||||
|
||||
expect(result[0].id).toMatch(UUID_REGEX);
|
||||
});
|
||||
|
||||
it('should handle nested children blocks', () => {
|
||||
const blocks: PartialBlock[] = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ type: 'paragraph', content: 'nested' }],
|
||||
},
|
||||
];
|
||||
const result = ensureBlockIds(blocks);
|
||||
|
||||
expect(result[0].id).toMatch(UUID_REGEX);
|
||||
expect(result[0].children?.[0]?.id).toMatch(UUID_REGEX);
|
||||
});
|
||||
|
||||
it('should not mutate the original blocks', () => {
|
||||
const blocks: PartialBlock[] = [{ type: 'paragraph', content: 'hello' }];
|
||||
ensureBlockIds(blocks);
|
||||
|
||||
expect(blocks[0].id).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty children array', () => {
|
||||
const blocks: PartialBlock[] = [
|
||||
{ type: 'paragraph', children: [], content: 'hello' },
|
||||
];
|
||||
const result = ensureBlockIds(blocks);
|
||||
|
||||
expect(result[0].id).toMatch(UUID_REGEX);
|
||||
expect(result[0].children).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,10 +1,20 @@
|
|||
import { parseInitialBlocknote } from '@/blocknote-editor/utils/parseInitialBlocknote';
|
||||
|
||||
describe('parseInitialBlocknote', () => {
|
||||
it('should parse valid JSON array string', () => {
|
||||
it('should parse valid JSON array string and ensure block ids', () => {
|
||||
const input = JSON.stringify([{ type: 'paragraph', content: 'test' }]);
|
||||
const result = parseInitialBlocknote(input);
|
||||
expect(result).toEqual([{ type: 'paragraph', content: 'test' }]);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result?.[0].type).toBe('paragraph');
|
||||
expect(result?.[0].id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should preserve existing block ids', () => {
|
||||
const input = JSON.stringify([
|
||||
{ id: 'my-id', type: 'paragraph', content: 'test' },
|
||||
]);
|
||||
const result = parseInitialBlocknote(input);
|
||||
expect(result?.[0].id).toBe('my-id');
|
||||
});
|
||||
|
||||
it('should return undefined for empty string', () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import type { PartialBlock } from '@blocknote/core';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
// Recursively ensures every block has a valid `id`.
|
||||
// BlockNote 0.47.x throws "Block doesn't have id" during ProseMirror
|
||||
// view creation if any block is missing its id attribute — even though
|
||||
// PartialBlock allows omitting it.
|
||||
export const ensureBlockIds = (blocks: PartialBlock[]): PartialBlock[] => {
|
||||
let patchedCount = 0;
|
||||
|
||||
const result = blocks.map((block) => {
|
||||
const patchedBlock = { ...block };
|
||||
|
||||
if (!isNonEmptyString(patchedBlock.id)) {
|
||||
patchedBlock.id = v4();
|
||||
patchedCount++;
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(patchedBlock.children) &&
|
||||
patchedBlock.children.length > 0
|
||||
) {
|
||||
patchedBlock.children = ensureBlockIds(
|
||||
patchedBlock.children as PartialBlock[],
|
||||
);
|
||||
}
|
||||
|
||||
return patchedBlock;
|
||||
});
|
||||
|
||||
if (patchedCount > 0) {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.warn(
|
||||
`[BlockNote] Patched ${patchedCount} block(s) missing id attribute`,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import type { PartialBlock } from '@blocknote/core';
|
||||
import { isArray, isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { ensureBlockIds } from '@/blocknote-editor/utils/ensureBlockIds';
|
||||
|
||||
export const parseInitialBlocknote = (
|
||||
blocknote?: string | null,
|
||||
logContext?: string,
|
||||
|
|
@ -22,7 +24,7 @@ export const parseInitialBlocknote = (
|
|||
return undefined;
|
||||
}
|
||||
|
||||
return parsedBody;
|
||||
return ensureBlockIds(parsedBody);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
|
|
|||
Loading…
Reference in a new issue