mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
Fix Email composer rich text to HTML conversion (#19872)
This commit is contained in:
parent
a8d4039629
commit
a1de37e424
9 changed files with 24 additions and 96 deletions
|
|
@ -111,7 +111,7 @@ export const EmailComposerFields = ({
|
|||
placeholder={t`Type something or press "/" to see commands`}
|
||||
minHeight={120}
|
||||
maxWidth={600}
|
||||
contentType="json"
|
||||
contentType="html"
|
||||
/>
|
||||
<EmailAttachmentsField
|
||||
label={t`Attachments`}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { marked } from 'marked';
|
|||
import { type DependencyList, useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export type AdvancedTextEditorContentType = 'json' | 'markdown';
|
||||
export type AdvancedTextEditorContentType = 'json' | 'html' | 'markdown';
|
||||
|
||||
type UseAdvancedTextEditorProps = {
|
||||
placeholder: string | undefined;
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ export const FormAdvancedTextFieldInput = ({
|
|||
defaultValue,
|
||||
contentType,
|
||||
onUpdate: (editor) => {
|
||||
if (contentType === 'markdown') {
|
||||
if (contentType === 'markdown' || contentType === 'html') {
|
||||
onChange(editor.getHTML());
|
||||
} else {
|
||||
const jsonContent = editor.getJSON();
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { render, toPlainText } from '@react-email/render';
|
||||
import { toPlainText } from '@react-email/render';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { reactMarkupFromJSON } from 'twenty-emails';
|
||||
import { MAX_EMAIL_RECIPIENTS } from 'twenty-shared/constants';
|
||||
import {
|
||||
ConnectedAccountProvider,
|
||||
|
|
@ -31,7 +30,6 @@ import { MessagingAccountAuthenticationService } from 'src/modules/messaging/mes
|
|||
import { type MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||
import { type MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
|
||||
import { type MessageAttachment } from 'src/modules/messaging/message-import-manager/types/message';
|
||||
import { parseEmailBody } from 'src/utils/parse-email-body';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
@Injectable()
|
||||
export class EmailComposerService {
|
||||
|
|
@ -391,15 +389,12 @@ export class EmailComposerService {
|
|||
options.attachmentsFileFolder,
|
||||
);
|
||||
|
||||
const parsedBody = parseEmailBody(body);
|
||||
const reactMarkup = reactMarkupFromJSON(parsedBody);
|
||||
const htmlBody = await render(reactMarkup);
|
||||
const plainTextBody = toPlainText(htmlBody);
|
||||
|
||||
const { JSDOM } = await import('jsdom');
|
||||
const window = new JSDOM('').window;
|
||||
const purify = DOMPurify(window);
|
||||
const sanitizedHtmlBody = purify.sanitize(htmlBody || '');
|
||||
|
||||
const sanitizedHtmlBody = purify.sanitize(body || '');
|
||||
const plainTextBody = toPlainText(sanitizedHtmlBody);
|
||||
const sanitizedSubject = purify.sanitize(subject || '');
|
||||
|
||||
let threadExternalId: string | undefined;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const EmailToolInputZodSchema = z.object({
|
|||
'Recipients object with to, cc, and bcc fields (comma-separated)',
|
||||
),
|
||||
subject: z.string().describe('The email subject line'),
|
||||
body: z.string().describe('The email body content (HTML or plain text)'),
|
||||
body: z.string().describe('The email body content in HTML format'),
|
||||
connectedAccountId: z
|
||||
.string()
|
||||
.refine((val) => isValidUuid(val))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
import { render } from '@react-email/render';
|
||||
import { type JSONContent, reactMarkupFromJSON } from 'twenty-emails';
|
||||
|
||||
export const renderRichTextToHtml = async (
|
||||
jsonContent: JSONContent,
|
||||
): Promise<string> => {
|
||||
const reactMarkup = reactMarkupFromJSON(jsonContent);
|
||||
|
||||
return render(reactMarkup);
|
||||
};
|
||||
|
|
@ -5,6 +5,7 @@ import { resolveInput, resolveRichTextVariables } from 'twenty-shared/utils';
|
|||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
|
||||
|
||||
import { DraftEmailTool } from 'src/engine/core-modules/tool/tools/email-tool/draft-email-tool';
|
||||
import { renderRichTextToHtml } from 'src/engine/core-modules/tool/tools/email-tool/utils/render-rich-text-to-html.util';
|
||||
import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool';
|
||||
import { SendEmailTool } from 'src/engine/core-modules/tool/tools/email-tool/send-email-tool';
|
||||
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
|
||||
|
|
@ -67,9 +68,13 @@ export class ToolExecutorWorkflowAction implements WorkflowAction {
|
|||
const emailInput = toolInput as WorkflowSendEmailActionInput;
|
||||
|
||||
if (emailInput.body) {
|
||||
const resolvedBody = resolveRichTextVariables(emailInput.body, context);
|
||||
const bodyJson = JSON.parse(resolvedBody!);
|
||||
const htmlBody = await renderRichTextToHtml(bodyJson);
|
||||
|
||||
toolInput = {
|
||||
...emailInput,
|
||||
body: resolveRichTextVariables(emailInput.body, context),
|
||||
body: htmlBody,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
import { parseEmailBody } from 'src/utils/parse-email-body';
|
||||
|
||||
describe('parseEmailBody', () => {
|
||||
it('should parse valid JSON content', () => {
|
||||
const jsonContent = {
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [{ type: 'text', text: 'Hello World' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = parseEmailBody(JSON.stringify(jsonContent));
|
||||
|
||||
expect(result).toEqual(jsonContent);
|
||||
});
|
||||
|
||||
it('should return plain string when JSON parsing fails', () => {
|
||||
const plainText = 'This is plain text, not JSON';
|
||||
|
||||
const result = parseEmailBody(plainText);
|
||||
|
||||
expect(result).toBe(plainText);
|
||||
});
|
||||
|
||||
it('should parse JSON content with hardBreak nodes', () => {
|
||||
const jsonWithHardBreaks = {
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{ type: 'text', text: 'Line 1' },
|
||||
{ type: 'hardBreak' },
|
||||
{ type: 'text', text: 'Line 2' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = parseEmailBody(JSON.stringify(jsonWithHardBreaks));
|
||||
|
||||
expect(result).toEqual(jsonWithHardBreaks);
|
||||
expect(
|
||||
(result as typeof jsonWithHardBreaks).content[0].content,
|
||||
).toContainEqual({
|
||||
type: 'hardBreak',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty string', () => {
|
||||
const result = parseEmailBody('');
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle JSON array format', () => {
|
||||
const arrayContent = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [{ type: 'text', text: 'Content' }],
|
||||
},
|
||||
];
|
||||
|
||||
const result = parseEmailBody(JSON.stringify(arrayContent));
|
||||
|
||||
expect(result).toEqual(arrayContent);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { type JSONContent } from 'twenty-emails';
|
||||
|
||||
export const parseEmailBody = (body: string): JSONContent | string => {
|
||||
try {
|
||||
const json = JSON.parse(body);
|
||||
|
||||
return json;
|
||||
} catch {
|
||||
return body;
|
||||
}
|
||||
};
|
||||
Loading…
Reference in a new issue