Workflow Send Email Node Multiple Recipients Support (#17458)
This PR adds support sending emails to multiple recipients Figma Reference: https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=88868-88963&t=Ya0csmNlN4xxczvV-11 Demo: https://github.com/user-attachments/assets/ecaeaaec-fe42-4fb5-96d3-a91d08b30148
This commit is contained in:
+98
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
migrateInput,
|
||||
migrateWorkflowSteps,
|
||||
needsMigration,
|
||||
} from 'src/database/commands/upgrade-version-command/1-17/utils/migrate-send-email-step.util';
|
||||
|
||||
const LEGACY_STEP_FROM_PRODUCTION = {
|
||||
id: '3b8934cd-1dda-4acb-a050-785e04f7f40b',
|
||||
name: 'Send Email',
|
||||
type: 'SEND_EMAIL',
|
||||
valid: false,
|
||||
position: { x: 0, y: 150 },
|
||||
settings: {
|
||||
input: {
|
||||
body: '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"sample"}]}]}',
|
||||
email: 'sample@gmail.com',
|
||||
files: [],
|
||||
subject: 'sample',
|
||||
connectedAccountId: '',
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('needsMigration', () => {
|
||||
it('returns true for legacy email field', () => {
|
||||
expect(
|
||||
needsMigration({ connectedAccountId: '', email: 'test@example.com' }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when recipients is already used', () => {
|
||||
expect(
|
||||
needsMigration({
|
||||
connectedAccountId: '',
|
||||
recipients: { to: 'test@example.com' },
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrateInput', () => {
|
||||
it('converts legacy email to recipients.to', () => {
|
||||
const result = migrateInput({
|
||||
connectedAccountId: 'acc-123',
|
||||
email: 'legacy@example.com',
|
||||
subject: 'Test',
|
||||
body: 'Body',
|
||||
});
|
||||
|
||||
expect(result.recipients.to).toBe('legacy@example.com');
|
||||
expect(result).not.toHaveProperty('email');
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrateWorkflowSteps', () => {
|
||||
it('migrates real production workflow with legacy email field', () => {
|
||||
const { migratedSteps, hasChanges } = migrateWorkflowSteps([
|
||||
LEGACY_STEP_FROM_PRODUCTION,
|
||||
]);
|
||||
|
||||
expect(hasChanges).toBe(true);
|
||||
expect(migratedSteps[0].settings.input).toEqual({
|
||||
body: '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"sample"}]}]}',
|
||||
files: [],
|
||||
subject: 'sample',
|
||||
connectedAccountId: '',
|
||||
recipients: {
|
||||
to: 'sample@gmail.com',
|
||||
cc: '',
|
||||
bcc: '',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns hasChanges false when no migration needed', () => {
|
||||
const alreadyMigratedStep = {
|
||||
...LEGACY_STEP_FROM_PRODUCTION,
|
||||
settings: {
|
||||
...LEGACY_STEP_FROM_PRODUCTION.settings,
|
||||
input: {
|
||||
connectedAccountId: '',
|
||||
recipients: { to: 'new@example.com', cc: '', bcc: '' },
|
||||
subject: 'Test',
|
||||
body: 'Body',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { hasChanges } = migrateWorkflowSteps([alreadyMigratedStep]);
|
||||
|
||||
expect(hasChanges).toBe(false);
|
||||
});
|
||||
});
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
type LegacySendEmailInput = {
|
||||
connectedAccountId: string;
|
||||
email?: string;
|
||||
recipients?: {
|
||||
to?: string;
|
||||
cc?: string;
|
||||
bcc?: string;
|
||||
};
|
||||
subject?: string;
|
||||
body?: string;
|
||||
files?: unknown[];
|
||||
};
|
||||
|
||||
type MigratedSendEmailInput = {
|
||||
connectedAccountId: string;
|
||||
recipients: {
|
||||
to: string;
|
||||
cc: string;
|
||||
bcc: string;
|
||||
};
|
||||
subject?: string;
|
||||
body?: string;
|
||||
files?: unknown[];
|
||||
};
|
||||
|
||||
type WorkflowStep = {
|
||||
id: string;
|
||||
type: string;
|
||||
settings: {
|
||||
input: LegacySendEmailInput | MigratedSendEmailInput;
|
||||
};
|
||||
};
|
||||
|
||||
export const needsMigration = (input: LegacySendEmailInput): boolean => {
|
||||
return isDefined(input.email);
|
||||
};
|
||||
|
||||
export const migrateInput = (
|
||||
input: LegacySendEmailInput,
|
||||
): MigratedSendEmailInput => {
|
||||
const { email, recipients, ...rest } = input;
|
||||
|
||||
const toValue = recipients?.to || email || '';
|
||||
const ccValue = recipients?.cc ?? '';
|
||||
const bccValue = recipients?.bcc ?? '';
|
||||
|
||||
return {
|
||||
...rest,
|
||||
recipients: {
|
||||
to: toValue,
|
||||
cc: ccValue,
|
||||
bcc: bccValue,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const migrateWorkflowSteps = (
|
||||
steps: unknown,
|
||||
): { migratedSteps: WorkflowStep[]; hasChanges: boolean } => {
|
||||
if (!isDefined(steps) || !Array.isArray(steps) || steps.length === 0) {
|
||||
return { migratedSteps: [], hasChanges: false };
|
||||
}
|
||||
|
||||
const typedSteps = steps as WorkflowStep[];
|
||||
|
||||
let hasChanges = false;
|
||||
|
||||
const migratedSteps = typedSteps.map((step) => {
|
||||
if (step.type !== WorkflowActionType.SEND_EMAIL) {
|
||||
return step;
|
||||
}
|
||||
|
||||
const input = step.settings.input as LegacySendEmailInput;
|
||||
|
||||
if (!needsMigration(input)) {
|
||||
return step;
|
||||
}
|
||||
|
||||
hasChanges = true;
|
||||
|
||||
return {
|
||||
...step,
|
||||
settings: {
|
||||
...step.settings,
|
||||
input: migrateInput(input),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return { migratedSteps, hasChanges };
|
||||
};
|
||||
Reference in New Issue
Block a user