7417 workflows i can send emails using the email account (#7431)

- update `send-email.workflow-action.ts` so it send email via the google
sdk
- remove useless `workflow-action.email.ts`
- add `send` authorization to google api scopes
- update the front workflow email step form to provide a
`connectedAccountId` from the available connected accounts
- update the permissions of connected accounts: ask users to reconnect
when selecting missing send permission


![image](https://github.com/user-attachments/assets/fe3c329d-fd67-4d0d-8450-099c35933645)
This commit is contained in:
martmull
2024-10-08 23:29:09 +02:00
committed by GitHub
parent 444cd3f03f
commit f138a1cf6e
30 changed files with 443 additions and 159 deletions

View File

@ -4,10 +4,18 @@ import { WorkflowEditActionFormBase } from '@/workflow/components/WorkflowEditAc
import { WorkflowSendEmailStep } from '@/workflow/types/Workflow';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useEffect } from 'react';
import React, { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { IconMail } from 'twenty-ui';
import { IconMail, IconPlus, isDefined } from 'twenty-ui';
import { useDebouncedCallback } from 'use-debounce';
import { Select, SelectOption } from '@/ui/input/components/Select';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import { useRecoilValue } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
import { workflowIdState } from '@/workflow/states/workflowIdState';
import { GMAIL_SEND_SCOPE } from '@/accounts/constants/GmailSendScope';
const StyledTriggerSettings = styled.div`
padding: ${({ theme }) => theme.spacing(6)};
@ -28,6 +36,7 @@ type WorkflowEditActionFormSendEmailProps =
};
type SendEmailFormData = {
connectedAccountId: string;
subject: string;
body: string;
};
@ -36,35 +45,70 @@ export const WorkflowEditActionFormSendEmail = (
props: WorkflowEditActionFormSendEmailProps,
) => {
const theme = useTheme();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
const workflowId = useRecoilValue(workflowIdState);
const redirectUrl = `/object/workflow/${workflowId}`;
const form = useForm<SendEmailFormData>({
defaultValues: {
connectedAccountId: '',
subject: '',
body: '',
},
disabled: props.readonly,
});
useEffect(() => {
form.setValue('subject', props.action.settings.subject ?? '');
form.setValue('body', props.action.settings.template ?? '');
}, [props.action.settings.subject, props.action.settings.template, form]);
const saveAction = useDebouncedCallback((formData: SendEmailFormData) => {
if (props.readonly === true) {
const checkConnectedAccountScopes = async (
connectedAccountId: string | null,
) => {
const connectedAccount = accounts.find(
(account) => account.id === connectedAccountId,
);
if (!isDefined(connectedAccount)) {
return;
}
const scopes = connectedAccount.scopes;
if (
!isDefined(scopes) ||
!isDefined(scopes.find((scope) => scope === GMAIL_SEND_SCOPE))
) {
await triggerGoogleApisOAuth({
redirectLocation: redirectUrl,
loginHint: connectedAccount.handle,
});
}
};
props.onActionUpdate({
...props.action,
settings: {
...props.action.settings,
title: formData.subject,
subject: formData.subject,
template: formData.body,
},
});
}, 1_000);
useEffect(() => {
form.setValue(
'connectedAccountId',
props.action.settings.connectedAccountId ?? '',
);
form.setValue('subject', props.action.settings.subject ?? '');
form.setValue('body', props.action.settings.body ?? '');
}, [props.action.settings, form]);
const saveAction = useDebouncedCallback(
async (formData: SendEmailFormData, checkScopes = false) => {
if (props.readonly === true) {
return;
}
props.onActionUpdate({
...props.action,
settings: {
...props.action.settings,
connectedAccountId: formData.connectedAccountId,
subject: formData.subject,
body: formData.body,
},
});
if (checkScopes === true) {
await checkConnectedAccountScopes(formData.connectedAccountId);
}
},
1_000,
);
useEffect(() => {
return () => {
@ -72,52 +116,120 @@ export const WorkflowEditActionFormSendEmail = (
};
}, [saveAction]);
const handleSave = form.handleSubmit(saveAction);
const handleSave = (checkScopes = false) =>
form.handleSubmit((formData: SendEmailFormData) =>
saveAction(formData, checkScopes),
)();
const filter: { or: object[] } = {
or: [
{
accountOwnerId: {
eq: currentWorkspaceMember?.id,
},
},
],
};
if (
isDefined(props.action.settings.connectedAccountId) &&
props.action.settings.connectedAccountId !== ''
) {
filter.or.push({
id: {
eq: props.action.settings.connectedAccountId,
},
});
}
const { records: accounts, loading } = useFindManyRecords<ConnectedAccount>({
objectNameSingular: 'connectedAccount',
filter,
});
let emptyOption: SelectOption<string | null> = { label: 'None', value: null };
const connectedAccountOptions: SelectOption<string | null>[] = [];
accounts.forEach((account) => {
const selectOption = {
label: account.handle,
value: account.id,
};
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;
}
});
return (
<WorkflowEditActionFormBase
ActionIcon={<IconMail color={theme.color.blue} />}
actionTitle="Send Email"
actionType="Email"
>
<StyledTriggerSettings>
<Controller
name="subject"
control={form.control}
render={({ field }) => (
<TextInput
label="Subject"
placeholder="Thank you for building such an awesome CRM!"
value={field.value}
disabled={field.disabled}
onChange={(email) => {
field.onChange(email);
!loading && (
<WorkflowEditActionFormBase
ActionIcon={<IconMail color={theme.color.blue} />}
actionTitle="Send Email"
actionType="Email"
>
<StyledTriggerSettings>
<Controller
name="connectedAccountId"
control={form.control}
render={({ field }) => (
<Select
dropdownId="select-connected-account-id"
label="Account"
fullWidth
emptyOption={emptyOption}
value={field.value}
options={connectedAccountOptions}
callToActionButton={{
onClick: () =>
triggerGoogleApisOAuth({ redirectLocation: redirectUrl }),
Icon: IconPlus,
text: 'Add account',
}}
onChange={(connectedAccountId) => {
field.onChange(connectedAccountId);
handleSave(true);
}}
/>
)}
/>
<Controller
name="subject"
control={form.control}
render={({ field }) => (
<TextInput
label="Subject"
placeholder="Enter email subject (use {{variable}} for dynamic content)"
value={field.value}
onChange={(email) => {
field.onChange(email);
handleSave();
}}
/>
)}
/>
handleSave();
}}
/>
)}
/>
<Controller
name="body"
control={form.control}
render={({ field }) => (
<TextArea
label="Body"
placeholder="Thank you so much!"
value={field.value}
minRows={4}
disabled={field.disabled}
onChange={(email) => {
field.onChange(email);
handleSave();
}}
/>
)}
/>
</StyledTriggerSettings>
</WorkflowEditActionFormBase>
<Controller
name="body"
control={form.control}
render={({ field }) => (
<TextArea
label="Body"
placeholder="Enter email body (use {{variable}} for dynamic content)"
value={field.value}
minRows={4}
onChange={(email) => {
field.onChange(email);
handleSave();
}}
/>
)}
/>
</StyledTriggerSettings>
</WorkflowEditActionFormBase>
)
);
};

View File

@ -14,13 +14,9 @@ export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & {
};
export type WorkflowSendEmailStepSettings = BaseWorkflowStepSettings & {
connectedAccountId: string;
subject?: string;
template?: string;
title?: string;
callToAction?: {
value: string;
href: string;
};
body?: string;
};
type BaseWorkflowStep = {

View File

@ -33,9 +33,9 @@ export const getStepDefaultDefinition = (
type: 'SEND_EMAIL',
valid: false,
settings: {
subject: 'hello',
title: 'hello',
template: '{{title}}',
connectedAccountId: '',
subject: '',
body: '',
errorHandlingOptions: {
continueOnFailure: {
value: false,