feat: add draft email workflow action (#17793)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
+4
-3
@@ -9,7 +9,7 @@ import { WorkflowActionCode } from '@/workflow/workflow-steps/workflow-actions/c
|
||||
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
|
||||
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
|
||||
import { WorkflowEditActionEmpty } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmpty';
|
||||
import { WorkflowEditActionSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail';
|
||||
import { WorkflowEditActionEmailBase } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmailBase';
|
||||
import { WorkflowEditActionUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord';
|
||||
import { WorkflowEditActionUpsertRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpsertRecord';
|
||||
import { WorkflowEditActionDelay } from '@/workflow/workflow-steps/workflow-actions/delay-actions/components/WorkflowEditActionDelay';
|
||||
@@ -127,9 +127,10 @@ export const WorkflowRunStepNodeDetail = ({
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'SEND_EMAIL': {
|
||||
case 'SEND_EMAIL':
|
||||
case 'DRAFT_EMAIL': {
|
||||
return (
|
||||
<WorkflowEditActionSendEmail
|
||||
<WorkflowEditActionEmailBase
|
||||
key={stepId}
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={{
|
||||
|
||||
+4
-3
@@ -8,7 +8,7 @@ import { WorkflowActionCode } from '@/workflow/workflow-steps/workflow-actions/c
|
||||
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
|
||||
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
|
||||
import { WorkflowEditActionEmpty } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmpty';
|
||||
import { WorkflowEditActionSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail';
|
||||
import { WorkflowEditActionEmailBase } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmailBase';
|
||||
import { WorkflowEditActionUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord';
|
||||
import { WorkflowEditActionUpsertRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpsertRecord';
|
||||
import { WorkflowEditActionDelay } from '@/workflow/workflow-steps/workflow-actions/delay-actions/components/WorkflowEditActionDelay';
|
||||
@@ -125,9 +125,10 @@ export const WorkflowStepDetail = ({
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'SEND_EMAIL': {
|
||||
case 'SEND_EMAIL':
|
||||
case 'DRAFT_EMAIL': {
|
||||
return (
|
||||
<WorkflowEditActionSendEmail
|
||||
<WorkflowEditActionEmailBase
|
||||
key={stepId}
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={props}
|
||||
|
||||
+65
-127
@@ -1,6 +1,5 @@
|
||||
import { GMAIL_SEND_SCOPE } from '@/accounts/constants/GmailSendScope';
|
||||
import { MICROSOFT_SEND_SCOPE } from '@/accounts/constants/MicrosoftSendScope';
|
||||
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
||||
import { getMissingDraftEmailScopes } from '@/accounts/utils/hasMissingDraftEmailScopes';
|
||||
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
|
||||
import { WorkflowSendEmailAttachments } from '@/advanced-text-editor/components/WorkflowSendEmailAttachments';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
@@ -21,60 +20,42 @@ import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import { type WorkflowSendEmailAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
|
||||
import { type WorkflowEmailAction } from '@/workflow/workflow-steps/workflow-actions/email-action/types/WorkflowEmailAction';
|
||||
import { useEmailForm } from '@/workflow/workflow-steps/workflow-actions/hooks/useEmailForm';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { ConnectedAccountProvider, SettingsPath } from 'twenty-shared/types';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import { type EmailRecipients } from 'twenty-shared/workflow';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Callout, IconPlus } from 'twenty-ui/display';
|
||||
import { Button, type SelectOption } from 'twenty-ui/input';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
|
||||
const EMAIL_EDITOR_MIN_HEIGHT = 340;
|
||||
|
||||
const EMAIL_EDITOR_MAX_WIDTH = 600;
|
||||
|
||||
type WorkflowEditActionSendEmailProps = {
|
||||
action: WorkflowSendEmailAction;
|
||||
type WorkflowEditActionEmailBaseProps = {
|
||||
action: WorkflowEmailAction;
|
||||
actionOptions:
|
||||
| {
|
||||
readonly: true;
|
||||
}
|
||||
| {
|
||||
readonly?: false;
|
||||
onActionUpdate: (action: WorkflowSendEmailAction) => void;
|
||||
onActionUpdate: (action: WorkflowEmailAction) => void;
|
||||
};
|
||||
};
|
||||
|
||||
type WorkflowFile = {
|
||||
id: string;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
type SendEmailFormData = {
|
||||
connectedAccountId: string;
|
||||
recipients: Required<EmailRecipients>;
|
||||
subject: string;
|
||||
body: string;
|
||||
files: WorkflowFile[];
|
||||
};
|
||||
|
||||
export const WorkflowEditActionSendEmail = ({
|
||||
export const WorkflowEditActionEmailBase = ({
|
||||
action,
|
||||
actionOptions,
|
||||
}: WorkflowEditActionSendEmailProps) => {
|
||||
}: WorkflowEditActionEmailBaseProps) => {
|
||||
const theme = useTheme();
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
const { triggerApisOAuth } = useTriggerApisOAuth();
|
||||
@@ -89,20 +70,13 @@ export const WorkflowEditActionSendEmail = ({
|
||||
|
||||
const redirectUrl = `/object/workflow/${workflowVisualizerWorkflowId}`;
|
||||
|
||||
const [formData, setFormData] = useState<SendEmailFormData>(() => {
|
||||
const inputRecipients = action.settings.input.recipients;
|
||||
|
||||
return {
|
||||
connectedAccountId: action.settings.input.connectedAccountId,
|
||||
recipients: {
|
||||
to: inputRecipients?.to ?? '',
|
||||
cc: inputRecipients?.cc ?? '',
|
||||
bcc: inputRecipients?.bcc ?? '',
|
||||
},
|
||||
subject: action.settings.input.subject ?? '',
|
||||
body: action.settings.input.body ?? '',
|
||||
files: action.settings.input.files ?? [],
|
||||
};
|
||||
const { formData, handleFieldChange, saveAction } = useEmailForm({
|
||||
action,
|
||||
onActionUpdate:
|
||||
actionOptions.readonly === true
|
||||
? undefined
|
||||
: actionOptions.onActionUpdate,
|
||||
readonly: actionOptions.readonly === true,
|
||||
});
|
||||
|
||||
const [visibleAdvancedFields, setVisibleAdvancedFields] = useState<{
|
||||
@@ -119,95 +93,24 @@ export const WorkflowEditActionSendEmail = ({
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const advancedOptionsDropdownId = 'send-email-advanced-options';
|
||||
const advancedOptionsDropdownId = `${action.id}-email-advanced-options`;
|
||||
|
||||
const hasAvailableAdvancedOptions =
|
||||
!visibleAdvancedFields.cc || !visibleAdvancedFields.bcc;
|
||||
|
||||
const checkConnectedAccountScopes = async (
|
||||
connectedAccountId: string | null,
|
||||
) => {
|
||||
const connectedAccount = accounts.find(
|
||||
(account) => account.id === connectedAccountId,
|
||||
);
|
||||
if (!isDefined(connectedAccount)) {
|
||||
const handleReauthorize = async () => {
|
||||
if (!isDefined(missingScopes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scopes = connectedAccount.scopes;
|
||||
|
||||
const hasSendScope = (
|
||||
connectedAccount: ConnectedAccount,
|
||||
scopes: string[],
|
||||
): boolean => {
|
||||
switch (connectedAccount.provider) {
|
||||
case ConnectedAccountProvider.GOOGLE:
|
||||
return scopes.some((scope) => scope === GMAIL_SEND_SCOPE);
|
||||
case ConnectedAccountProvider.MICROSOFT:
|
||||
return scopes.some((scope) => scope === MICROSOFT_SEND_SCOPE);
|
||||
case ConnectedAccountProvider.IMAP_SMTP_CALDAV:
|
||||
return isDefined(connectedAccount.connectionParameters?.SMTP);
|
||||
default:
|
||||
assertUnreachable(
|
||||
connectedAccount.provider,
|
||||
'Provider not yet supported for sending emails',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
connectedAccount.provider !== ConnectedAccountProvider.IMAP_SMTP_CALDAV &&
|
||||
(!isDefined(scopes) || !hasSendScope(connectedAccount, scopes))
|
||||
) {
|
||||
await triggerApisOAuth(connectedAccount.provider, {
|
||||
redirectLocation: redirectUrl,
|
||||
loginHint: connectedAccount.handle,
|
||||
});
|
||||
}
|
||||
await triggerApisOAuth(missingScopes.provider, {
|
||||
redirectLocation: redirectUrl,
|
||||
loginHint: missingScopes.loginHint,
|
||||
});
|
||||
};
|
||||
|
||||
const saveAction = useDebouncedCallback(
|
||||
async (formData: SendEmailFormData) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
input: {
|
||||
connectedAccountId: formData.connectedAccountId,
|
||||
recipients: formData.recipients,
|
||||
subject: formData.subject,
|
||||
body: formData.body,
|
||||
files: formData.files,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await checkConnectedAccountScopes(formData.connectedAccountId);
|
||||
},
|
||||
1_000,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
saveAction.flush();
|
||||
};
|
||||
}, [saveAction]);
|
||||
|
||||
const handleFieldChange = (
|
||||
fieldName: keyof SendEmailFormData,
|
||||
updatedValue: JsonValue,
|
||||
) => {
|
||||
const newFormData: SendEmailFormData = {
|
||||
...formData,
|
||||
[fieldName]: updatedValue,
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
|
||||
saveAction(newFormData);
|
||||
const handleConnectedAccountChange = (connectedAccountId: string | null) => {
|
||||
handleFieldChange('connectedAccountId', connectedAccountId);
|
||||
};
|
||||
|
||||
const handleUploadAttachment = async (file: File) => {
|
||||
@@ -263,6 +166,24 @@ export const WorkflowEditActionSendEmail = ({
|
||||
},
|
||||
});
|
||||
|
||||
const selectedAccount = accounts.find(
|
||||
(account) => account.id === formData.connectedAccountId,
|
||||
);
|
||||
|
||||
const missingDraftScopes = isDefined(selectedAccount)
|
||||
? getMissingDraftEmailScopes(selectedAccount)
|
||||
: [];
|
||||
|
||||
const missingScopes =
|
||||
isDefined(selectedAccount) &&
|
||||
selectedAccount.provider !== ConnectedAccountProvider.IMAP_SMTP_CALDAV &&
|
||||
missingDraftScopes.length > 0
|
||||
? {
|
||||
provider: selectedAccount.provider,
|
||||
loginHint: selectedAccount.handle,
|
||||
}
|
||||
: null;
|
||||
|
||||
let emptyOption: SelectOption<string | null> = {
|
||||
label: t`None`,
|
||||
value: null,
|
||||
@@ -284,8 +205,6 @@ export const WorkflowEditActionSendEmail = ({
|
||||
if (account.accountOwnerId === currentWorkspaceMember?.id) {
|
||||
connectedAccountOptions.push(selectOption);
|
||||
} else {
|
||||
// This handle the case when the current connected account does not belong to the currentWorkspaceMember
|
||||
// In that case, current connected account email is displayed, but cannot be selected
|
||||
emptyOption = selectOption;
|
||||
}
|
||||
});
|
||||
@@ -294,6 +213,12 @@ export const WorkflowEditActionSendEmail = ({
|
||||
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
saveAction.flush();
|
||||
};
|
||||
}, [saveAction]);
|
||||
|
||||
return (
|
||||
!loading && (
|
||||
<>
|
||||
@@ -314,12 +239,25 @@ export const WorkflowEditActionSendEmail = ({
|
||||
text: t`Add account`,
|
||||
}}
|
||||
onChange={(connectedAccountId) => {
|
||||
handleFieldChange('connectedAccountId', connectedAccountId);
|
||||
handleConnectedAccountChange(connectedAccountId);
|
||||
}}
|
||||
disabled={actionOptions.readonly}
|
||||
dropdownOffset={{ y: parseInt(theme.spacing(1), 10) }}
|
||||
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
|
||||
/>
|
||||
{isDefined(missingScopes) && (
|
||||
<>
|
||||
<Callout
|
||||
variant={'error'}
|
||||
title={t`Missing email draft permission.`}
|
||||
description={t`This account is connected, but we don't have permission to draft emails on your behalf yet. You'll be redirected to approve this access.`}
|
||||
action={{
|
||||
label: t`Reauthorize`,
|
||||
onClick: handleReauthorize,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<FormMultiTextFieldInput
|
||||
label={t`To`}
|
||||
placeholder={t`Enter emails, comma-separated`}
|
||||
@@ -434,7 +372,7 @@ export const WorkflowEditActionSendEmail = ({
|
||||
href: '#',
|
||||
},
|
||||
{
|
||||
children: isDefined(action.name) ? action.name : t`Send Email`,
|
||||
children: isDefined(action.name) ? action.name : t`Email`,
|
||||
href: '#',
|
||||
},
|
||||
{
|
||||
+60
-10
@@ -1,5 +1,8 @@
|
||||
import { type WorkflowSendEmailAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowEditActionSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail';
|
||||
import {
|
||||
type WorkflowDraftEmailAction,
|
||||
type WorkflowSendEmailAction,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { WorkflowEditActionEmailBase } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmailBase';
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { graphql, HttpResponse } from 'msw';
|
||||
import { expect, fn, within } from 'storybook/test';
|
||||
@@ -16,7 +19,7 @@ import {
|
||||
} from '~/testing/mock-data/connected-accounts';
|
||||
import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow';
|
||||
|
||||
const DEFAULT_ACTION: WorkflowSendEmailAction = {
|
||||
const DEFAULT_SEND_EMAIL_ACTION: WorkflowSendEmailAction = {
|
||||
id: getWorkflowNodeIdMock(),
|
||||
name: 'Send Email',
|
||||
type: 'SEND_EMAIL',
|
||||
@@ -45,7 +48,7 @@ const DEFAULT_ACTION: WorkflowSendEmailAction = {
|
||||
},
|
||||
};
|
||||
|
||||
const CONFIGURED_ACTION: WorkflowSendEmailAction = {
|
||||
const CONFIGURED_SEND_EMAIL_ACTION: WorkflowSendEmailAction = {
|
||||
id: getWorkflowNodeIdMock(),
|
||||
name: 'Send Welcome Email',
|
||||
type: 'SEND_EMAIL',
|
||||
@@ -74,9 +77,38 @@ const CONFIGURED_ACTION: WorkflowSendEmailAction = {
|
||||
},
|
||||
};
|
||||
|
||||
const meta: Meta<typeof WorkflowEditActionSendEmail> = {
|
||||
title: 'Modules/Workflow/Actions/SendEmail/EditAction',
|
||||
component: WorkflowEditActionSendEmail,
|
||||
const DEFAULT_DRAFT_EMAIL_ACTION: WorkflowDraftEmailAction = {
|
||||
id: getWorkflowNodeIdMock(),
|
||||
name: 'Draft Email',
|
||||
type: 'DRAFT_EMAIL',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
connectedAccountId: '',
|
||||
recipients: {
|
||||
to: '',
|
||||
cc: '',
|
||||
bcc: '',
|
||||
},
|
||||
subject: '',
|
||||
body: '',
|
||||
files: [],
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const meta: Meta<typeof WorkflowEditActionEmailBase> = {
|
||||
title: 'Modules/Workflow/Actions/Email/EditAction',
|
||||
component: WorkflowEditActionEmailBase,
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
@@ -92,7 +124,7 @@ const meta: Meta<typeof WorkflowEditActionSendEmail> = {
|
||||
},
|
||||
},
|
||||
args: {
|
||||
action: DEFAULT_ACTION,
|
||||
action: DEFAULT_SEND_EMAIL_ACTION,
|
||||
},
|
||||
decorators: [
|
||||
WorkflowStepActionDrawerDecorator,
|
||||
@@ -107,7 +139,7 @@ const meta: Meta<typeof WorkflowEditActionSendEmail> = {
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof WorkflowEditActionSendEmail>;
|
||||
type Story = StoryObj<typeof WorkflowEditActionEmailBase>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
@@ -128,7 +160,7 @@ export const Default: Story = {
|
||||
|
||||
export const Configured: Story = {
|
||||
args: {
|
||||
action: CONFIGURED_ACTION,
|
||||
action: CONFIGURED_SEND_EMAIL_ACTION,
|
||||
actionOptions: {
|
||||
onActionUpdate: fn(),
|
||||
},
|
||||
@@ -146,3 +178,21 @@ export const Configured: Story = {
|
||||
expect(subjectInput).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const DraftEmail: Story = {
|
||||
args: {
|
||||
action: DEFAULT_DRAFT_EMAIL_ACTION,
|
||||
actionOptions: {
|
||||
onActionUpdate: fn(),
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
expect(await canvas.findByText('Account')).toBeVisible();
|
||||
expect(await canvas.findByText('To')).toBeVisible();
|
||||
expect(await canvas.findByText('Subject')).toBeVisible();
|
||||
expect(await canvas.findByText('Body')).toBeVisible();
|
||||
expect(await canvas.findByText('Advanced options')).toBeVisible();
|
||||
},
|
||||
};
|
||||
+6
-2
@@ -1,10 +1,14 @@
|
||||
import { type WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
import { CODE_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/CodeAction';
|
||||
import { DRAFT_EMAIL_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/DraftEmailAction';
|
||||
import { HTTP_REQUEST_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/HttpRequestAction';
|
||||
import { SEND_EMAIL_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/SendEmailAction';
|
||||
|
||||
export const CORE_ACTIONS: Array<{
|
||||
defaultLabel: string;
|
||||
type: Extract<WorkflowActionType, 'CODE' | 'SEND_EMAIL' | 'HTTP_REQUEST'>;
|
||||
type: Extract<
|
||||
WorkflowActionType,
|
||||
'CODE' | 'SEND_EMAIL' | 'DRAFT_EMAIL' | 'HTTP_REQUEST'
|
||||
>;
|
||||
icon: string;
|
||||
}> = [SEND_EMAIL_ACTION, CODE_ACTION, HTTP_REQUEST_ACTION];
|
||||
}> = [SEND_EMAIL_ACTION, DRAFT_EMAIL_ACTION, CODE_ACTION, HTTP_REQUEST_ACTION];
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { type WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
|
||||
export const DRAFT_EMAIL_ACTION: {
|
||||
defaultLabel: string;
|
||||
type: Extract<WorkflowActionType, 'DRAFT_EMAIL'>;
|
||||
icon: string;
|
||||
} = {
|
||||
defaultLabel: 'Draft Email',
|
||||
type: 'DRAFT_EMAIL',
|
||||
icon: 'IconMailPlus',
|
||||
};
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { type WorkflowAttachmentType } from '@/workflow/workflow-steps/workflow-actions/email-action/types/WorkflowAttachmentType';
|
||||
import { type EmailRecipients } from 'twenty-shared/workflow';
|
||||
|
||||
export type EmailFormData = {
|
||||
connectedAccountId: string;
|
||||
recipients: Required<EmailRecipients>;
|
||||
subject: string;
|
||||
body: string;
|
||||
files: WorkflowAttachmentType[];
|
||||
};
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import {
|
||||
type WorkflowDraftEmailAction,
|
||||
type WorkflowSendEmailAction,
|
||||
} from '@/workflow/types/Workflow';
|
||||
|
||||
export type WorkflowEmailAction =
|
||||
| WorkflowSendEmailAction
|
||||
| WorkflowDraftEmailAction;
|
||||
+9
-4
@@ -222,14 +222,19 @@ export const WorkflowEditActionFormBuilder = ({
|
||||
<StyledWorkflowStepBody>
|
||||
{triggerType && triggerType !== 'MANUAL' && isCalloutVisible && (
|
||||
<Callout
|
||||
learnMoreText={t`Learn more`}
|
||||
variant={'warning'}
|
||||
title={t`This form will appear in workflow runs.`}
|
||||
description={t`Because this workflow is not using a manual trigger, the form will not open on top of the interface. To fill it, open the corresponding workflow run and complete the form there.`}
|
||||
onClose={() => setIsCalloutVisible(false)}
|
||||
learnMoreUrl={
|
||||
'https://docs.twenty.com/user-guide/workflows/capabilities/workflow-actions#form'
|
||||
}
|
||||
action={{
|
||||
label: t`Learn more`,
|
||||
onClick: () =>
|
||||
window.open(
|
||||
'https://docs.twenty.com/user-guide/workflows/capabilities/workflow-actions#form',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{formData.length === 0 && (
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { type EmailFormData } from '@/workflow/workflow-steps/workflow-actions/email-action/types/EmailFormData';
|
||||
import { type WorkflowEmailAction } from '@/workflow/workflow-steps/workflow-actions/email-action/types/WorkflowEmailAction';
|
||||
import { useState } from 'react';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
type UseEmailFormParams = {
|
||||
action: WorkflowEmailAction;
|
||||
onActionUpdate?: (action: WorkflowEmailAction) => void;
|
||||
readonly: boolean;
|
||||
};
|
||||
|
||||
export const useEmailForm = ({
|
||||
action,
|
||||
onActionUpdate,
|
||||
readonly,
|
||||
}: UseEmailFormParams) => {
|
||||
const [formData, setFormData] = useState<EmailFormData>(() => {
|
||||
const inputRecipients = action.settings.input.recipients;
|
||||
|
||||
return {
|
||||
connectedAccountId: action.settings.input.connectedAccountId,
|
||||
recipients: {
|
||||
to: inputRecipients?.to ?? '',
|
||||
cc: inputRecipients?.cc ?? '',
|
||||
bcc: inputRecipients?.bcc ?? '',
|
||||
},
|
||||
subject: action.settings.input.subject ?? '',
|
||||
body: action.settings.input.body ?? '',
|
||||
files: action.settings.input.files ?? [],
|
||||
};
|
||||
});
|
||||
|
||||
const saveAction = useDebouncedCallback((formData: EmailFormData) => {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
onActionUpdate?.({
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
input: {
|
||||
connectedAccountId: formData.connectedAccountId,
|
||||
recipients: formData.recipients,
|
||||
subject: formData.subject,
|
||||
body: formData.body,
|
||||
files: formData.files,
|
||||
},
|
||||
},
|
||||
});
|
||||
}, 1_000);
|
||||
|
||||
const handleFieldChange = (
|
||||
fieldName: keyof EmailFormData,
|
||||
updatedValue: JsonValue,
|
||||
) => {
|
||||
const newFormData: EmailFormData = {
|
||||
...formData,
|
||||
[fieldName]: updatedValue,
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
saveAction(newFormData);
|
||||
};
|
||||
|
||||
return {
|
||||
formData,
|
||||
handleFieldChange,
|
||||
saveAction,
|
||||
};
|
||||
};
|
||||
+1
@@ -18,6 +18,7 @@ export const getActionIcon = (actionType: WorkflowActionType) => {
|
||||
case 'CODE':
|
||||
case 'HTTP_REQUEST':
|
||||
case 'SEND_EMAIL':
|
||||
case 'DRAFT_EMAIL':
|
||||
return CORE_ACTIONS.find((item) => item.type === actionType)?.icon;
|
||||
case 'LOGIC_FUNCTION':
|
||||
return 'IconFunction';
|
||||
|
||||
+1
@@ -14,6 +14,7 @@ export const getActionIconColorOrThrow = ({
|
||||
case 'LOGIC_FUNCTION':
|
||||
case 'HTTP_REQUEST':
|
||||
case 'SEND_EMAIL':
|
||||
case 'DRAFT_EMAIL':
|
||||
return theme.color.red;
|
||||
case 'CREATE_RECORD':
|
||||
case 'UPDATE_RECORD':
|
||||
|
||||
Reference in New Issue
Block a user