diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmailBase.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmailBase.tsx index 6f1796318b..360e6697fa 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmailBase.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmailBase.tsx @@ -121,9 +121,6 @@ export const WorkflowEditActionEmailBase = ({ const { accounts: myAccounts, loading: myAccountsLoading } = useMyConnectedAccounts(); - // Sender variables are only enabled for DRAFT_EMAIL for now; SEND_EMAIL keeps a plain account select. - const isSenderVariableEnabled = action.type === 'DRAFT_EMAIL'; - const configuredAccountId = formData.connectedAccountId; const isSenderVariable = isStandaloneVariableString(configuredAccountId); const isConfiguredAccountMine = myAccounts.some( @@ -205,17 +202,11 @@ export const WorkflowEditActionEmailBase = ({ { diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionEmailBase.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionEmailBase.stories.tsx index f71262e1c7..d6eb3382e9 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionEmailBase.stories.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionEmailBase.stories.tsx @@ -128,6 +128,36 @@ const DEFAULT_DRAFT_EMAIL_ACTION: WorkflowDraftEmailAction = { }, }; +const VARIABLE_SENDER_SEND_EMAIL_ACTION: WorkflowSendEmailAction = { + id: getWorkflowNodeIdMock(), + name: 'Send Email', + type: 'SEND_EMAIL', + valid: true, + settings: { + input: { + connectedAccountId: '{{trigger._metadata.workspaceMemberId}}', + recipients: { + to: 'test@twenty.com', + cc: '', + bcc: '', + }, + subject: 'Welcome to Twenty!', + body: 'Hello', + files: [], + inReplyTo: '', + }, + outputSchema: {}, + errorHandlingOptions: { + retryOnFailure: { + value: false, + }, + continueOnFailure: { + value: false, + }, + }, + }, +}; + const VARIABLE_SENDER_DRAFT_EMAIL_ACTION: WorkflowDraftEmailAction = { id: getWorkflowNodeIdMock(), name: 'Draft Email', @@ -283,11 +313,9 @@ export const VariableSender: Story = { }, }; -// SEND_EMAIL does not expose the sender variable picker yet (DRAFT_EMAIL only), -// so the account field stays a plain select with no variable hint. -export const SendEmailHasNoVariablePicker: Story = { +export const VariableSenderSendEmail: Story = { args: { - action: DEFAULT_SEND_EMAIL_ACTION, + action: VARIABLE_SENDER_SEND_EMAIL_ACTION, actionOptions: { onActionUpdate: fn(), }, @@ -296,10 +324,11 @@ export const SendEmailHasNoVariablePicker: Story = { const canvas = within(canvasElement); expect(await canvas.findByText('Account')).toBeVisible(); + expect(await canvas.findByLabelText('Remove variable')).toBeInTheDocument(); expect( - canvas.queryByText( + await canvas.findByText( 'Pick a connected account or set a workspace member as variable', ), - ).not.toBeInTheDocument(); + ).toBeVisible(); }, }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/__tests__/send-email.workflow-action.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/__tests__/send-email.workflow-action.spec.ts index 073517d205..73ba3696da 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/__tests__/send-email.workflow-action.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/__tests__/send-email.workflow-action.spec.ts @@ -47,12 +47,15 @@ const buildSendEmailStep = (input: Record): WorkflowAction => }) as WorkflowAction; const WORKSPACE_MEMBER_ID = '20202020-2222-4222-8222-222222222222'; +const USER_WORKSPACE_ID = '20202020-3333-4333-8333-333333333333'; +const MEMBER_ACCOUNT_ID = '20202020-5555-4555-8555-555555555555'; describe('SendEmailWorkflowAction', () => { let action: SendEmailWorkflowAction; let mockSendEmailTool: jest.Mocked>; let connectedAccountRepository: { findOne: jest.Mock }; - let getRepository: jest.Mock; + let userWorkspaceRepository: { findOne: jest.Mock }; + let workspaceMemberRepository: { findOne: jest.Mock }; beforeEach(async () => { jest.clearAllMocks(); @@ -64,7 +67,8 @@ describe('SendEmailWorkflowAction', () => { }), }; connectedAccountRepository = { findOne: jest.fn() }; - getRepository = jest.fn(); + userWorkspaceRepository = { findOne: jest.fn() }; + workspaceMemberRepository = { findOne: jest.fn() }; const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -78,7 +82,9 @@ describe('SendEmailWorkflowAction', () => { provide: GlobalWorkspaceOrmManager, useValue: { executeInWorkspaceContext: jest.fn((callback) => callback()), - getRepository, + getRepository: jest + .fn() + .mockResolvedValue(workspaceMemberRepository), }, }, { @@ -87,7 +93,7 @@ describe('SendEmailWorkflowAction', () => { }, { provide: getRepositoryToken(UserWorkspaceEntity), - useValue: { findOne: jest.fn() }, + useValue: userWorkspaceRepository, }, ], }).compile(); @@ -197,14 +203,12 @@ describe('SendEmailWorkflowAction', () => { }); describe('sender resolution', () => { - // Sender-as-variable (workspace member) is draft-only for now, so - // send-email must never resolve the value as a workspace member id. - it('does not resolve a workspace member id and passes the value through', async () => { - await action.execute({ + const executeWithSender = (connectedAccountId: string) => + action.execute({ currentStepId: 'step-1', steps: [ buildSendEmailStep({ - connectedAccountId: WORKSPACE_MEMBER_ID, + connectedAccountId, recipients: { to: 'test@example.com' }, subject: 'Test', body: 'hi', @@ -214,13 +218,47 @@ describe('SendEmailWorkflowAction', () => { runInfo: { workspaceId: 'workspace-1', workflowRunId: 'run-1' }, }); - expect(getRepository).not.toHaveBeenCalled(); + it("resolves a workspace member id to the member's first connected account", async () => { + workspaceMemberRepository.findOne.mockResolvedValue({ userId: 'user-1' }); + userWorkspaceRepository.findOne.mockResolvedValue({ + id: USER_WORKSPACE_ID, + }); + connectedAccountRepository.findOne.mockResolvedValue({ + id: MEMBER_ACCOUNT_ID, + }); + + await executeWithSender(WORKSPACE_MEMBER_ID); + + expect(mockSendEmailTool.execute).toHaveBeenCalledWith( + expect.objectContaining({ connectedAccountId: MEMBER_ACCOUNT_ID }), + expect.any(Object), + ); + }); + + it('passes the id through unchanged when it is not a workspace member', async () => { + workspaceMemberRepository.findOne.mockResolvedValue(null); + + await executeWithSender(WORKSPACE_MEMBER_ID); + expect(connectedAccountRepository.findOne).not.toHaveBeenCalled(); expect(mockSendEmailTool.execute).toHaveBeenCalledWith( expect.objectContaining({ connectedAccountId: WORKSPACE_MEMBER_ID }), expect.any(Object), ); }); + + it('throws when the workspace member has no connected account', async () => { + workspaceMemberRepository.findOne.mockResolvedValue({ userId: 'user-1' }); + userWorkspaceRepository.findOne.mockResolvedValue({ + id: USER_WORKSPACE_ID, + }); + connectedAccountRepository.findOne.mockResolvedValue(null); + + await expect(executeWithSender(WORKSPACE_MEMBER_ID)).rejects.toThrow( + `No connected account found for workspace member '${WORKSPACE_MEMBER_ID}'`, + ); + expect(mockSendEmailTool.execute).not.toHaveBeenCalled(); + }); }); describe('step type guard', () => { diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/email-workflow-action.base.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/email-workflow-action.base.ts index 4cdabf26c3..ecea82553e 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/email-workflow-action.base.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/email-workflow-action.base.ts @@ -57,13 +57,6 @@ export abstract class EmailWorkflowActionBase extends ToolBackedWorkflowAction