Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 396565654a | |||
| ea630d25a9 |
-4
@@ -370,7 +370,6 @@ describe('SyncMessageFoldersService', () => {
|
||||
id: 'folder-1',
|
||||
externalId: 'inbox-ext',
|
||||
name: 'INBOX',
|
||||
isSynced: true,
|
||||
isSentFolder: false,
|
||||
parentFolderId: null,
|
||||
});
|
||||
@@ -378,7 +377,6 @@ describe('SyncMessageFoldersService', () => {
|
||||
createMockDiscoveredFolder({
|
||||
externalId: 'inbox-ext',
|
||||
name: 'INBOX',
|
||||
isSynced: true,
|
||||
isSentFolder: false,
|
||||
parentFolderId: null,
|
||||
}),
|
||||
@@ -476,7 +474,6 @@ describe('SyncMessageFoldersService', () => {
|
||||
createMockDiscoveredFolder({
|
||||
externalId: 'unchanged-ext',
|
||||
name: 'Unchanged',
|
||||
isSynced: true,
|
||||
}),
|
||||
createMockDiscoveredFolder({
|
||||
externalId: 'new-ext',
|
||||
@@ -537,7 +534,6 @@ describe('SyncMessageFoldersService', () => {
|
||||
createMockDiscoveredFolder({
|
||||
externalId: 'inbox-ext',
|
||||
name: 'INBOX',
|
||||
isSynced: true,
|
||||
}),
|
||||
];
|
||||
const messageChannel = createMockMessageChannel({
|
||||
|
||||
-66
@@ -93,72 +93,6 @@ describe('computeFoldersToUpdate', () => {
|
||||
expect(result.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should detect isSynced change from false to true', () => {
|
||||
const discoveredFolders = [
|
||||
{
|
||||
name: 'Inbox',
|
||||
externalId: 'INBOX',
|
||||
isSynced: true,
|
||||
isSentFolder: false,
|
||||
parentFolderId: null,
|
||||
},
|
||||
];
|
||||
|
||||
const existingFolders = [
|
||||
{
|
||||
id: 'folder-id',
|
||||
name: 'Inbox',
|
||||
externalId: 'INBOX',
|
||||
isSynced: false,
|
||||
isSentFolder: false,
|
||||
parentFolderId: null,
|
||||
syncCursor: 'cursor',
|
||||
pendingSyncAction: MessageFolderPendingSyncAction.NONE,
|
||||
},
|
||||
];
|
||||
|
||||
const result = computeFoldersToUpdate({
|
||||
discoveredFolders,
|
||||
existingFolders,
|
||||
});
|
||||
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.get('folder-id')?.isSynced).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect isSynced change from true to false', () => {
|
||||
const discoveredFolders = [
|
||||
{
|
||||
name: 'Promotions',
|
||||
externalId: 'promo-1',
|
||||
isSynced: false,
|
||||
isSentFolder: false,
|
||||
parentFolderId: null,
|
||||
},
|
||||
];
|
||||
|
||||
const existingFolders = [
|
||||
{
|
||||
id: 'folder-id',
|
||||
name: 'Promotions',
|
||||
externalId: 'promo-1',
|
||||
isSynced: true,
|
||||
isSentFolder: false,
|
||||
parentFolderId: null,
|
||||
syncCursor: 'cursor',
|
||||
pendingSyncAction: MessageFolderPendingSyncAction.NONE,
|
||||
},
|
||||
];
|
||||
|
||||
const result = computeFoldersToUpdate({
|
||||
discoveredFolders,
|
||||
existingFolders,
|
||||
});
|
||||
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.get('folder-id')?.isSynced).toBe(false);
|
||||
});
|
||||
|
||||
it('should treat empty string parentFolderId same as null', () => {
|
||||
const discoveredFolders = [
|
||||
{
|
||||
|
||||
+2
-16
@@ -14,24 +14,12 @@ export const computeFoldersToUpdate = ({
|
||||
}: {
|
||||
discoveredFolders: DiscoveredMessageFolder[];
|
||||
existingFolders: MessageFolder[];
|
||||
}): Map<
|
||||
string,
|
||||
Pick<
|
||||
MessageFolderEntity,
|
||||
'name' | 'isSynced' | 'isSentFolder' | 'parentFolderId'
|
||||
>
|
||||
> => {
|
||||
}): Map<string, Partial<MessageFolderEntity>> => {
|
||||
const existingFoldersByExternalId = new Map(
|
||||
existingFolders.map((folder) => [folder.externalId, folder]),
|
||||
);
|
||||
|
||||
const foldersToUpdate = new Map<
|
||||
string,
|
||||
Pick<
|
||||
MessageFolderEntity,
|
||||
'name' | 'isSynced' | 'isSentFolder' | 'parentFolderId'
|
||||
>
|
||||
>();
|
||||
const foldersToUpdate = new Map<string, Partial<MessageFolderEntity>>();
|
||||
|
||||
for (const discoveredFolder of discoveredFolders) {
|
||||
const existingFolder = existingFoldersByExternalId.get(
|
||||
@@ -48,14 +36,12 @@ export const computeFoldersToUpdate = ({
|
||||
|
||||
const discoveredFolderData = {
|
||||
name: discoveredFolder.name,
|
||||
isSynced: discoveredFolder.isSynced,
|
||||
isSentFolder: discoveredFolder.isSentFolder,
|
||||
parentFolderId,
|
||||
};
|
||||
|
||||
const existingFolderData = {
|
||||
name: existingFolder.name,
|
||||
isSynced: existingFolder.isSynced,
|
||||
isSentFolder: existingFolder.isSentFolder,
|
||||
parentFolderId: isNonEmptyString(existingFolder.parentFolderId)
|
||||
? existingFolder.parentFolderId
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { DraftEmailTool } from 'src/engine/core-modules/tool/tools/email-tool/draft-email-tool';
|
||||
import { SendEmailTool } from 'src/engine/core-modules/tool/tools/email-tool/send-email-tool';
|
||||
import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool';
|
||||
import { ToolExecutorWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/tool-executor-workflow-action';
|
||||
import { type WorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
|
||||
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
jest.mock(
|
||||
'src/engine/core-modules/tool/tools/email-tool/utils/render-rich-text-to-html.util',
|
||||
() => ({
|
||||
renderRichTextToHtml: jest
|
||||
.fn()
|
||||
.mockResolvedValue('<p>rendered html</p>'),
|
||||
}),
|
||||
);
|
||||
|
||||
const { renderRichTextToHtml } = jest.requireMock(
|
||||
'src/engine/core-modules/tool/tools/email-tool/utils/render-rich-text-to-html.util',
|
||||
);
|
||||
|
||||
const baseSettings: WorkflowActionSettings = {
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
input: {},
|
||||
};
|
||||
|
||||
const buildSendEmailStep = (input: Record<string, unknown>) => ({
|
||||
id: 'step-1',
|
||||
type: WorkflowActionType.SEND_EMAIL as const,
|
||||
name: 'Send Email',
|
||||
valid: true,
|
||||
settings: { ...baseSettings, input },
|
||||
});
|
||||
|
||||
describe('ToolExecutorWorkflowAction', () => {
|
||||
let action: ToolExecutorWorkflowAction;
|
||||
let mockSendEmailTool: jest.Mocked<Pick<SendEmailTool, 'execute'>>;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockSendEmailTool = {
|
||||
execute: jest.fn().mockResolvedValue({
|
||||
result: { success: true },
|
||||
error: undefined,
|
||||
}),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ToolExecutorWorkflowAction,
|
||||
{ provide: HttpTool, useValue: { execute: jest.fn() } },
|
||||
{ provide: SendEmailTool, useValue: mockSendEmailTool },
|
||||
{ provide: DraftEmailTool, useValue: { execute: jest.fn() } },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
action = module.get(ToolExecutorWorkflowAction);
|
||||
});
|
||||
|
||||
const executeWithBody = (body: string | undefined) =>
|
||||
action.execute({
|
||||
currentStepId: 'step-1',
|
||||
steps: [
|
||||
buildSendEmailStep({
|
||||
connectedAccountId: 'account-1',
|
||||
recipients: { to: 'test@example.com' },
|
||||
subject: 'Test',
|
||||
body,
|
||||
}),
|
||||
],
|
||||
context: {
|
||||
trigger: {
|
||||
name: 'John',
|
||||
email: 'john@example.com',
|
||||
},
|
||||
},
|
||||
runInfo: {
|
||||
workspaceId: 'workspace-1',
|
||||
workflowRunId: 'run-1',
|
||||
attemptCount: 1,
|
||||
},
|
||||
});
|
||||
|
||||
describe('email body handling', () => {
|
||||
it('should render TipTap JSON body to HTML', async () => {
|
||||
const tipTapBody = JSON.stringify({
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [{ type: 'text', text: 'Hello world' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await executeWithBody(tipTapBody);
|
||||
|
||||
expect(renderRichTextToHtml).toHaveBeenCalledWith(
|
||||
JSON.parse(tipTapBody),
|
||||
);
|
||||
expect(mockSendEmailTool.execute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ body: '<p>rendered html</p>' }),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass plain text body through without rendering', async () => {
|
||||
const plainTextBody =
|
||||
'{{trigger.name}}\n{{trigger.email}}';
|
||||
|
||||
await executeWithBody(plainTextBody);
|
||||
|
||||
expect(renderRichTextToHtml).not.toHaveBeenCalled();
|
||||
expect(mockSendEmailTool.execute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ body: 'John\njohn@example.com' }),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not crash when body is undefined', async () => {
|
||||
await executeWithBody(undefined);
|
||||
|
||||
expect(renderRichTextToHtml).not.toHaveBeenCalled();
|
||||
expect(mockSendEmailTool.execute).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
+26
-7
@@ -1,6 +1,12 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { JSONContent } from '@tiptap/core';
|
||||
|
||||
import { resolveInput, resolveRichTextVariables } from 'twenty-shared/utils';
|
||||
import {
|
||||
isDefined,
|
||||
parseJson,
|
||||
resolveInput,
|
||||
resolveRichTextVariables,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
|
||||
|
||||
@@ -35,6 +41,22 @@ export class ToolExecutorWorkflowAction implements WorkflowAction {
|
||||
]);
|
||||
}
|
||||
|
||||
private async renderEmailBody(
|
||||
body: string,
|
||||
context: Record<string, unknown>,
|
||||
): Promise<string> {
|
||||
const bodyWithResolvedVariables = resolveRichTextVariables(body, context);
|
||||
const tipTapDocument = isDefined(bodyWithResolvedVariables)
|
||||
? parseJson<JSONContent>(bodyWithResolvedVariables)
|
||||
: null;
|
||||
|
||||
if (isDefined(tipTapDocument) && isDefined(tipTapDocument.type)) {
|
||||
return renderRichTextToHtml(tipTapDocument);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
async execute({
|
||||
currentStepId,
|
||||
steps,
|
||||
@@ -67,14 +89,11 @@ 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);
|
||||
|
||||
if (isDefined(emailInput.body)) {
|
||||
const emailBody = await this.renderEmailBody(emailInput.body, context);
|
||||
toolInput = {
|
||||
...emailInput,
|
||||
body: htmlBody,
|
||||
body: emailBody,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user