diff --git a/packages/twenty-front/src/modules/blocknote-editor/utils/__tests__/parseInitialBlocknote.test.ts b/packages/twenty-front/src/modules/blocknote-editor/utils/__tests__/parseInitialBlocknote.test.ts index b8894a89565..c890f1c0a6c 100644 --- a/packages/twenty-front/src/modules/blocknote-editor/utils/__tests__/parseInitialBlocknote.test.ts +++ b/packages/twenty-front/src/modules/blocknote-editor/utils/__tests__/parseInitialBlocknote.test.ts @@ -38,6 +38,50 @@ describe('parseInitialBlocknote', () => { expect(parseInitialBlocknote('{"key": "value"}')).toBeUndefined(); }); + it('should filter invalid blocks from the parsed content', () => { + const input = JSON.stringify([ + { type: 'paragraph', content: 'valid block' }, + null, + { content: 'missing type' }, + ]); + + expect(parseInitialBlocknote(input)).toEqual([ + { type: 'paragraph', content: 'valid block' }, + ]); + }); + + it('should return undefined when all parsed blocks are invalid', () => { + const input = JSON.stringify([null, { content: 'missing type' }]); + + expect(parseInitialBlocknote(input)).toBeUndefined(); + }); + + it('should sanitize invalid nested children', () => { + const input = JSON.stringify([ + { + type: 'paragraph', + content: 'parent', + children: [ + { type: 'paragraph', content: 'valid child' }, + { content: 'invalid child' }, + ], + }, + ]); + + expect(parseInitialBlocknote(input)).toEqual([ + { + type: 'paragraph', + content: 'parent', + children: [ + { + type: 'paragraph', + content: 'valid child', + }, + ], + }, + ]); + }); + it('should use custom log context when parsing fails', () => { const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); parseInitialBlocknote('invalid', 'Custom context'); diff --git a/packages/twenty-front/src/modules/blocknote-editor/utils/parseInitialBlocknote.ts b/packages/twenty-front/src/modules/blocknote-editor/utils/parseInitialBlocknote.ts index a3a1ab377a6..6c6b3b22b0b 100644 --- a/packages/twenty-front/src/modules/blocknote-editor/utils/parseInitialBlocknote.ts +++ b/packages/twenty-front/src/modules/blocknote-editor/utils/parseInitialBlocknote.ts @@ -1,12 +1,35 @@ import type { PartialBlock } from '@blocknote/core'; import { isArray, isNonEmptyString } from '@sniptt/guards'; +import { isDefined } from 'twenty-shared/utils'; + +const isObjectRecord = (value: unknown): value is Record => { + return typeof value === 'object' && value !== null; +}; + +const sanitizePartialBlock = (block: unknown): PartialBlock | undefined => { + if (!isObjectRecord(block) || !isNonEmptyString(block.type)) { + return undefined; + } + + const sanitizedBlock = { + ...block, + } as PartialBlock; + + if (isArray(block.children)) { + sanitizedBlock.children = block.children + .map(sanitizePartialBlock) + .filter(isDefined); + } + + return sanitizedBlock; +}; export const parseInitialBlocknote = ( blocknote?: string | null, logContext?: string, ): PartialBlock[] | undefined => { if (isNonEmptyString(blocknote) && blocknote !== '{}') { - let parsedBody: PartialBlock[] | undefined = undefined; + let parsedBody: unknown = undefined; // TODO: Remove this once we have removed the old rich text try { @@ -18,11 +41,19 @@ export const parseInitialBlocknote = ( console.warn(blocknote); } - if (!isArray(parsedBody) || parsedBody.length === 0) { + if (!isArray(parsedBody)) { return undefined; } - return parsedBody; + const sanitizedBody = parsedBody + .map(sanitizePartialBlock) + .filter(isDefined); + + if (sanitizedBody.length === 0) { + return undefined; + } + + return sanitizedBody; } return undefined; diff --git a/packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx b/packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx index 4f4e7c6a7e5..5fe054b19aa 100644 --- a/packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx +++ b/packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx @@ -44,12 +44,14 @@ export const PromiseRejectionEffect = () => { error?.networkError?.name === 'AbortError' || error?.name === 'AbortError'; - if (!isAbortError) { - enqueueErrorSnackBar( - error instanceof Error ? { message: error.message } : {}, - ); + if (isAbortError) { + return; } + enqueueErrorSnackBar( + error instanceof Error ? { message: error.message } : {}, + ); + try { const { captureException } = await import('@sentry/react'); captureException(error, (scope) => {