Fix blocknote.map crash with generic field-level RICH_TEXT_V2 handler (#17834)
## Summary Fixes #17667 - **Root cause**: `ActivityQueryResultGetterHandler` called `JSON.parse()` on the `blocknote` field and assumed the result was always an array. When the stored value was valid JSON but not an array (e.g., `"{}"`), `blocknote.map()` crashed with `blocknote.map is not a function`, breaking the entire notes page. - **Fix**: Replaced the object-level `ActivityQueryResultGetterHandler` (hardcoded for `note`/`task` only) with a generic field-level `RichTextV2FieldQueryResultGetterHandler` that safely parses blocknote JSON with `Array.isArray` validation and gracefully skips malformed values instead of crashing. - **Bonus**: The new handler works for **all** objects with `RICH_TEXT_V2` fields (not just `note`/`task`), following the same pattern as the existing `FilesFieldQueryResultGetterHandler`. ## Changes | File | Change | |------|--------| | `rich-text-v2-field-query-result-getter.handler.ts` | New field-level handler with safe blocknote parsing | | `common-result-getters.service.ts` | Register new handler, remove `note`/`task` object handlers | | `activity-query-result-getter.handler.ts` | Deleted (replaced by field-level handler) | | `rich-text-v2-field-query-result-getter.handler.spec.ts` | 9 tests covering all edge cases | ## Test plan - [x] Unit tests pass (9 tests covering: null blocknote, non-string blocknote, invalid JSON, non-array JSON like `"{}"`, no images, external URLs, internal URLs, multiple fields) - [x] Lint passes (`lint:diff-with-main`) - [x] Typecheck passes Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+5
-3
@@ -11,7 +11,7 @@ import { type QueryResultFieldValue } from 'src/engine/api/graphql/workspace-que
|
||||
import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface';
|
||||
|
||||
import { FilesFieldQueryResultGetterHandler } from 'src/engine/api/common/common-result-getters/handlers/field-handlers/files-field-query-result-getter.handler';
|
||||
import { ActivityQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler';
|
||||
import { RichTextV2FieldQueryResultGetterHandler } from 'src/engine/api/common/common-result-getters/handlers/field-handlers/rich-text-v2-field-query-result-getter.handler';
|
||||
import { AttachmentQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler';
|
||||
import { PersonQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler';
|
||||
import { WorkspaceMemberQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler';
|
||||
@@ -55,8 +55,6 @@ export class CommonResultGettersService {
|
||||
'workspaceMember',
|
||||
new WorkspaceMemberQueryResultGetterHandler(this.fileService),
|
||||
],
|
||||
['note', new ActivityQueryResultGetterHandler(this.fileService)],
|
||||
['task', new ActivityQueryResultGetterHandler(this.fileService)],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -69,6 +67,10 @@ export class CommonResultGettersService {
|
||||
FieldMetadataType.FILES,
|
||||
new FilesFieldQueryResultGetterHandler(this.filesFieldService),
|
||||
],
|
||||
[
|
||||
FieldMetadataType.RICH_TEXT_V2,
|
||||
new RichTextV2FieldQueryResultGetterHandler(this.fileService),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
import { FieldMetadataType, type ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
import { RichTextV2FieldQueryResultGetterHandler } from 'src/engine/api/common/common-result-getters/handlers/field-handlers/rich-text-v2-field-query-result-getter.handler';
|
||||
import { type FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
|
||||
const baseRecord: ObjectRecord = {
|
||||
id: '1',
|
||||
createdAt: '2021-01-01',
|
||||
updatedAt: '2021-01-01',
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
const richTextFieldMetadata = [
|
||||
{
|
||||
type: FieldMetadataType.RICH_TEXT_V2,
|
||||
name: 'bodyV2',
|
||||
},
|
||||
] as FlatFieldMetadata[];
|
||||
|
||||
const mockFileService = {
|
||||
signFileUrl: jest.fn().mockReturnValue('signed-path'),
|
||||
} as unknown as FileService;
|
||||
|
||||
describe('RichTextV2FieldQueryResultGetterHandler', () => {
|
||||
let handler: RichTextV2FieldQueryResultGetterHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.SERVER_URL = 'https://my-domain.twenty.com';
|
||||
handler = new RichTextV2FieldQueryResultGetterHandler(mockFileService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
delete process.env.SERVER_URL;
|
||||
});
|
||||
|
||||
describe('should return record unchanged', () => {
|
||||
it('when no RICH_TEXT_V2 field metadata is present', async () => {
|
||||
const record = {
|
||||
...baseRecord,
|
||||
bodyV2: { blocknote: '[]', markdown: null },
|
||||
};
|
||||
|
||||
const result = await handler.handle(record, 'ws-1', []);
|
||||
|
||||
expect(result).toEqual(record);
|
||||
});
|
||||
|
||||
it('when blocknote is null', async () => {
|
||||
const record = {
|
||||
...baseRecord,
|
||||
bodyV2: { blocknote: null, markdown: null },
|
||||
};
|
||||
|
||||
const result = await handler.handle(
|
||||
record,
|
||||
'ws-1',
|
||||
richTextFieldMetadata,
|
||||
);
|
||||
|
||||
expect(result).toEqual(record);
|
||||
});
|
||||
|
||||
it('when blocknote is not a string', async () => {
|
||||
const record = {
|
||||
...baseRecord,
|
||||
bodyV2: { blocknote: 123, markdown: null },
|
||||
};
|
||||
|
||||
const result = await handler.handle(
|
||||
record,
|
||||
'ws-1',
|
||||
richTextFieldMetadata,
|
||||
);
|
||||
|
||||
expect(result).toEqual(record);
|
||||
});
|
||||
|
||||
it('when blocknote is an invalid JSON string', async () => {
|
||||
const record = {
|
||||
...baseRecord,
|
||||
bodyV2: { blocknote: 'not-json', markdown: null },
|
||||
};
|
||||
|
||||
const result = await handler.handle(
|
||||
record,
|
||||
'ws-1',
|
||||
richTextFieldMetadata,
|
||||
);
|
||||
|
||||
expect(result).toEqual(record);
|
||||
});
|
||||
|
||||
it('when blocknote parses to a non-array value', async () => {
|
||||
const record = {
|
||||
...baseRecord,
|
||||
bodyV2: { blocknote: '{}', markdown: null },
|
||||
};
|
||||
|
||||
const result = await handler.handle(
|
||||
record,
|
||||
'ws-1',
|
||||
richTextFieldMetadata,
|
||||
);
|
||||
|
||||
expect(result).toEqual(record);
|
||||
});
|
||||
|
||||
it('when blocknote has no image blocks', async () => {
|
||||
const record = {
|
||||
...baseRecord,
|
||||
bodyV2: {
|
||||
markdown: null,
|
||||
blocknote: JSON.stringify([
|
||||
{ type: 'paragraph', text: 'Hello, world!' },
|
||||
]),
|
||||
},
|
||||
};
|
||||
|
||||
const result = await handler.handle(
|
||||
record,
|
||||
'ws-1',
|
||||
richTextFieldMetadata,
|
||||
);
|
||||
|
||||
expect(result).toEqual(record);
|
||||
});
|
||||
|
||||
it('when image block has external URL', async () => {
|
||||
const record = {
|
||||
...baseRecord,
|
||||
bodyV2: {
|
||||
markdown: null,
|
||||
blocknote: JSON.stringify([
|
||||
{
|
||||
type: 'image',
|
||||
props: { url: 'https://external.com/image.jpg' },
|
||||
},
|
||||
]),
|
||||
},
|
||||
};
|
||||
|
||||
const result = await handler.handle(
|
||||
record,
|
||||
'ws-1',
|
||||
richTextFieldMetadata,
|
||||
);
|
||||
|
||||
expect(result).toEqual(record);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should sign internal image URLs', () => {
|
||||
it('when image block has an internal attachment URL', async () => {
|
||||
const imageBlock = {
|
||||
type: 'image',
|
||||
props: {
|
||||
name: 'photo.jpg',
|
||||
url: 'https://my-domain.twenty.com/files/attachment/some-token/photo.jpg',
|
||||
caption: '',
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
|
||||
const record = {
|
||||
...baseRecord,
|
||||
bodyV2: {
|
||||
markdown: null,
|
||||
blocknote: JSON.stringify([imageBlock]),
|
||||
},
|
||||
};
|
||||
|
||||
const result = await handler.handle(
|
||||
record,
|
||||
'ws-1',
|
||||
richTextFieldMetadata,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
...baseRecord,
|
||||
bodyV2: {
|
||||
markdown: null,
|
||||
blocknote: JSON.stringify([
|
||||
{
|
||||
...imageBlock,
|
||||
props: {
|
||||
...imageBlock.props,
|
||||
url: 'https://my-domain.twenty.com/files/signed-path',
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle multiple RICH_TEXT_V2 fields', () => {
|
||||
it('when record has multiple rich text fields', async () => {
|
||||
const multiFieldMetadata = [
|
||||
{ type: FieldMetadataType.RICH_TEXT_V2, name: 'bodyV2' },
|
||||
{ type: FieldMetadataType.RICH_TEXT_V2, name: 'description' },
|
||||
] as FlatFieldMetadata[];
|
||||
|
||||
const record = {
|
||||
...baseRecord,
|
||||
bodyV2: {
|
||||
markdown: null,
|
||||
blocknote: JSON.stringify([{ type: 'paragraph', text: 'Hello' }]),
|
||||
},
|
||||
description: {
|
||||
markdown: null,
|
||||
blocknote: '{}',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await handler.handle(record, 'ws-1', multiFieldMetadata);
|
||||
|
||||
// bodyV2 should be unchanged (no images), description should be
|
||||
// unchanged (non-array blocknote)
|
||||
expect(result).toEqual(record);
|
||||
});
|
||||
});
|
||||
});
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import { FieldMetadataType, type ObjectRecord } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface';
|
||||
|
||||
import { type FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type RichTextBlock = Record<string, any>;
|
||||
|
||||
const signBlocknoteImageUrls = (
|
||||
blocknoteBlocks: RichTextBlock[],
|
||||
workspaceId: string,
|
||||
fileService: FileService,
|
||||
): RichTextBlock[] => {
|
||||
return blocknoteBlocks.map((block: RichTextBlock) => {
|
||||
if (block.type !== 'image' || !block.props?.url) {
|
||||
return block;
|
||||
}
|
||||
|
||||
let url: URL;
|
||||
|
||||
try {
|
||||
url = new URL(block.props.url);
|
||||
} catch {
|
||||
return block;
|
||||
}
|
||||
|
||||
const pathname = url.pathname;
|
||||
const isLinkExternal = !pathname.startsWith('/files/attachment/');
|
||||
|
||||
if (isLinkExternal) {
|
||||
return block;
|
||||
}
|
||||
|
||||
const fileName = pathname.match(/files\/attachment\/(?:.+)\/(.+)$/)?.[1];
|
||||
|
||||
if (!isDefined(fileName)) {
|
||||
return block;
|
||||
}
|
||||
|
||||
const signedPath = fileService.signFileUrl({
|
||||
url: `attachment/${fileName}`,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
...block,
|
||||
props: {
|
||||
...block.props,
|
||||
url: `${process.env.SERVER_URL}/files/${signedPath}`,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const parseBlocknoteJsonSafely = (
|
||||
blocknoteJson: string,
|
||||
): RichTextBlock[] | null => {
|
||||
try {
|
||||
const parsed = JSON.parse(blocknoteJson);
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export class RichTextV2FieldQueryResultGetterHandler
|
||||
implements QueryResultGetterHandlerInterface
|
||||
{
|
||||
constructor(private readonly fileService: FileService) {}
|
||||
|
||||
async handle(
|
||||
record: ObjectRecord,
|
||||
workspaceId: string,
|
||||
flatFieldMetadata: FlatFieldMetadata[],
|
||||
): Promise<ObjectRecord> {
|
||||
const richTextV2Fields = flatFieldMetadata.filter(
|
||||
(field) => field.type === FieldMetadataType.RICH_TEXT_V2,
|
||||
);
|
||||
|
||||
if (richTextV2Fields.length === 0) {
|
||||
return record;
|
||||
}
|
||||
|
||||
for (const field of richTextV2Fields) {
|
||||
const fieldValue = record[field.name];
|
||||
const blocknoteJson = fieldValue?.blocknote;
|
||||
|
||||
if (!blocknoteJson || typeof blocknoteJson !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const blocknoteBlocks = parseBlocknoteJsonSafely(blocknoteJson);
|
||||
|
||||
if (!isDefined(blocknoteBlocks)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const signedBlocks = signBlocknoteImageUrls(
|
||||
blocknoteBlocks,
|
||||
workspaceId,
|
||||
this.fileService,
|
||||
);
|
||||
|
||||
record[field.name] = {
|
||||
...fieldValue,
|
||||
blocknote: JSON.stringify(signedBlocks),
|
||||
};
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
}
|
||||
-185
@@ -1,185 +0,0 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { FieldActorSource } from 'twenty-shared/types';
|
||||
|
||||
import { ActivityQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler';
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { type NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity';
|
||||
|
||||
const baseNote = {
|
||||
id: '1',
|
||||
bodyV2: {
|
||||
markdown: null,
|
||||
blocknote: null,
|
||||
},
|
||||
position: 1,
|
||||
title: 'Test',
|
||||
createdBy: {
|
||||
name: 'Test',
|
||||
source: FieldActorSource.MANUAL,
|
||||
workspaceMemberId: '1',
|
||||
context: {},
|
||||
},
|
||||
updatedBy: {
|
||||
name: 'Test',
|
||||
source: FieldActorSource.MANUAL,
|
||||
workspaceMemberId: '1',
|
||||
context: {},
|
||||
},
|
||||
createdAt: '2021-01-01',
|
||||
updatedAt: '2021-01-01',
|
||||
noteTargets: [],
|
||||
attachments: [],
|
||||
timelineActivities: [],
|
||||
favorites: [],
|
||||
searchVector: '',
|
||||
deletedAt: null,
|
||||
} satisfies NoteWorkspaceEntity;
|
||||
|
||||
const baseTask = {
|
||||
...baseNote,
|
||||
type: 'task',
|
||||
};
|
||||
|
||||
describe('ActivityQueryResultGetterHandler', () => {
|
||||
let handler: ActivityQueryResultGetterHandler;
|
||||
|
||||
beforeEach(async () => {
|
||||
process.env.SERVER_URL = 'https://my-domain.twenty.com';
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ActivityQueryResultGetterHandler,
|
||||
{
|
||||
provide: FileService,
|
||||
useValue: {
|
||||
signFileUrl: jest.fn().mockReturnValue('signed-path'),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
handler = module.get<ActivityQueryResultGetterHandler>(
|
||||
ActivityQueryResultGetterHandler,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
delete process.env.SERVER_URL;
|
||||
});
|
||||
|
||||
describe('should do nothing', () => {
|
||||
it('when activity is a note and no image is found', async () => {
|
||||
const note = {
|
||||
...baseNote,
|
||||
bodyV2: {
|
||||
markdown: null,
|
||||
blocknote: JSON.stringify([
|
||||
{ type: 'paragraph', text: 'Hello, world!' },
|
||||
]),
|
||||
},
|
||||
};
|
||||
|
||||
const result = await handler.handle(note, '1');
|
||||
|
||||
expect(result).toEqual(note);
|
||||
});
|
||||
|
||||
it('when activity is a note and link is external', async () => {
|
||||
const note = {
|
||||
...baseNote,
|
||||
bodyV2: {
|
||||
markdown: null,
|
||||
blocknote: JSON.stringify([
|
||||
{
|
||||
id: 'c6a5f700-5e56-480d-90a9-7f295216370e',
|
||||
type: 'image',
|
||||
props: {
|
||||
backgroundColor: 'default',
|
||||
textAlignment: 'left',
|
||||
name: '20240529_123208.jpg',
|
||||
url: 'http://external-content.com/image.jpg',
|
||||
caption: '',
|
||||
showPreview: true,
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 'e2454736-51c1-4e61-a02d-71f0890bdda7',
|
||||
type: 'paragraph',
|
||||
props: {
|
||||
textColor: 'default',
|
||||
backgroundColor: 'default',
|
||||
textAlignment: 'left',
|
||||
},
|
||||
content: [],
|
||||
children: [],
|
||||
},
|
||||
]),
|
||||
},
|
||||
};
|
||||
|
||||
const result = await handler.handle(note, '1');
|
||||
|
||||
expect(result).toEqual(note);
|
||||
});
|
||||
|
||||
it('when activity is a task and no image is found', async () => {
|
||||
const task = {
|
||||
...baseTask,
|
||||
bodyV2: {
|
||||
markdown: null,
|
||||
blocknote: null,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await handler.handle(task, '1');
|
||||
|
||||
expect(result).toEqual(task);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should update token in file link', () => {
|
||||
it('when file link is in the body', async () => {
|
||||
const imageBlock = {
|
||||
id: 'c6a5f700-5e56-480d-90a9-7f295216370e',
|
||||
type: 'image',
|
||||
props: {
|
||||
backgroundColor: 'default',
|
||||
textAlignment: 'left',
|
||||
name: '20240529_123208.jpg',
|
||||
url: 'https://my-domain.twenty.com/files/attachment/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaWxlbmFtZSI6ImU0NWNiNDhhLTM2MmYtNGU4Zi1iOTEzLWM5MmI1ZTNlMGFhNi5qcGciLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsInN1YiI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsInR5cGUiOiJGSUxFIiwiaWF0IjoxNzUwNDI4NDQ1LCJleHAiOjE3NTA1MTQ4NDV9.qTN1b9IcmZvfVAqt1UlfJ_nn3GwIAEp7G9IoPtRJDxk/e45cb48a-362f-4e8f-b913-c92b5e3e0aa6.jpg',
|
||||
caption: '',
|
||||
showPreview: true,
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
|
||||
const note = {
|
||||
...baseNote,
|
||||
bodyV2: {
|
||||
markdown: null,
|
||||
blocknote: JSON.stringify([imageBlock]),
|
||||
},
|
||||
};
|
||||
|
||||
const result = await handler.handle(note, '1');
|
||||
|
||||
expect(result).toEqual({
|
||||
...note,
|
||||
bodyV2: {
|
||||
markdown: null,
|
||||
blocknote: JSON.stringify([
|
||||
{
|
||||
...imageBlock,
|
||||
props: {
|
||||
...imageBlock.props,
|
||||
url: 'https://my-domain.twenty.com/files/signed-path',
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface';
|
||||
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { type NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity';
|
||||
import { type TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type RichTextBlock = Record<string, any>;
|
||||
|
||||
type RichTextBody = RichTextBlock[];
|
||||
|
||||
@Injectable()
|
||||
export class ActivityQueryResultGetterHandler
|
||||
implements QueryResultGetterHandlerInterface
|
||||
{
|
||||
constructor(private readonly fileService: FileService) {}
|
||||
|
||||
async handle(
|
||||
activity: TaskWorkspaceEntity | NoteWorkspaceEntity,
|
||||
workspaceId: string,
|
||||
): Promise<TaskWorkspaceEntity | NoteWorkspaceEntity> {
|
||||
const blocknoteJson = activity.bodyV2?.blocknote;
|
||||
|
||||
if (!activity.id || !blocknoteJson) {
|
||||
return activity;
|
||||
}
|
||||
|
||||
let blocknote: RichTextBody = [];
|
||||
|
||||
try {
|
||||
blocknote = JSON.parse(blocknoteJson);
|
||||
} catch {
|
||||
blocknote = [];
|
||||
// TODO: Remove this once we have removed the old rich text
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`Failed to parse body for activity ${activity.id} in workspace ${workspaceId}, for rich text version 'v2'`,
|
||||
);
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(blocknoteJson);
|
||||
}
|
||||
|
||||
const blocknoteWithSignedPayload = await Promise.all(
|
||||
blocknote.map(async (block: RichTextBlock) => {
|
||||
if (block.type !== 'image' || !block.props.url) {
|
||||
return block;
|
||||
}
|
||||
|
||||
const imageProps = block.props;
|
||||
const url = new URL(imageProps.url);
|
||||
|
||||
const pathname = url.pathname;
|
||||
|
||||
const isLinkExternal = !pathname.startsWith('/files/attachment/');
|
||||
|
||||
if (isLinkExternal) {
|
||||
return block;
|
||||
}
|
||||
|
||||
const fileName = pathname.match(
|
||||
/files\/attachment\/(?:.+)\/(.+)$/,
|
||||
)?.[1];
|
||||
|
||||
const signedPath = this.fileService.signFileUrl({
|
||||
url: `attachment/${fileName}`,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
...block,
|
||||
props: {
|
||||
...imageProps,
|
||||
url: `${process.env.SERVER_URL}/files/${signedPath}`,
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
...activity,
|
||||
bodyV2: {
|
||||
blocknote: JSON.stringify(blocknoteWithSignedPayload),
|
||||
markdown: activity.bodyV2?.markdown ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user