feat(editor): ✨ Add cc & bcc + Deletable credentials
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
IconButton,
|
||||||
Menu,
|
Menu,
|
||||||
MenuButton,
|
MenuButton,
|
||||||
MenuButtonProps,
|
MenuButtonProps,
|
||||||
@@ -7,19 +8,22 @@ import {
|
|||||||
MenuList,
|
MenuList,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
|
useToast,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { ChevronLeftIcon, PlusIcon } from 'assets/icons'
|
import { ChevronLeftIcon, PlusIcon, TrashIcon } from 'assets/icons'
|
||||||
import React, { useEffect, useMemo } from 'react'
|
import React, { useEffect, useMemo, useState } from 'react'
|
||||||
import { useUser } from 'contexts/UserContext'
|
import { useUser } from 'contexts/UserContext'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { CredentialsType } from 'models'
|
import { CredentialsType } from 'models'
|
||||||
|
import { deleteCredentials, useCredentials } from 'services/credentials'
|
||||||
|
|
||||||
type Props = Omit<MenuButtonProps, 'type'> & {
|
type Props = Omit<MenuButtonProps, 'type'> & {
|
||||||
type: CredentialsType
|
type: CredentialsType
|
||||||
currentCredentialsId?: string
|
currentCredentialsId?: string
|
||||||
onCredentialsSelect: (credentialId: string) => void
|
onCredentialsSelect: (credentialId?: string) => void
|
||||||
onCreateNewClick: () => void
|
onCreateNewClick: () => void
|
||||||
defaultCredentialLabel?: string
|
defaultCredentialLabel?: string
|
||||||
|
refreshDropdownKey?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CredentialsDropdown = ({
|
export const CredentialsDropdown = ({
|
||||||
@@ -28,10 +32,21 @@ export const CredentialsDropdown = ({
|
|||||||
onCredentialsSelect,
|
onCredentialsSelect,
|
||||||
onCreateNewClick,
|
onCreateNewClick,
|
||||||
defaultCredentialLabel,
|
defaultCredentialLabel,
|
||||||
|
refreshDropdownKey,
|
||||||
...props
|
...props
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { credentials } = useUser()
|
const { user } = useUser()
|
||||||
|
const toast = useToast({
|
||||||
|
position: 'top-right',
|
||||||
|
status: 'error',
|
||||||
|
})
|
||||||
|
const { credentials, mutate } = useCredentials({
|
||||||
|
userId: user?.id,
|
||||||
|
onError: (error) =>
|
||||||
|
toast({ title: error.name, description: error.message }),
|
||||||
|
})
|
||||||
|
const [isDeleting, setIsDeleting] = useState<string>()
|
||||||
|
|
||||||
const defaultCredentialsLabel = defaultCredentialLabel ?? `Select an account`
|
const defaultCredentialsLabel = defaultCredentialLabel ?? `Select an account`
|
||||||
|
|
||||||
@@ -44,10 +59,15 @@ export const CredentialsDropdown = ({
|
|||||||
[currentCredentialsId, credentials]
|
[currentCredentialsId, credentials]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleMenuItemClick = (credentialId: string) => () => {
|
const handleMenuItemClick = (credentialsId: string) => () => {
|
||||||
onCredentialsSelect(credentialId)
|
onCredentialsSelect(credentialsId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ((refreshDropdownKey ?? 0) > 0) mutate({ credentials })
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [refreshDropdownKey])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!router.isReady) return
|
if (!router.isReady) return
|
||||||
if (router.query.credentialsId) {
|
if (router.query.credentialsId) {
|
||||||
@@ -63,6 +83,16 @@ export const CredentialsDropdown = ({
|
|||||||
router.push(router.asPath.split('?')[0], undefined, { shallow: true })
|
router.push(router.asPath.split('?')[0], undefined, { shallow: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDeleteDomainClick = (credentialsId: string) => async () => {
|
||||||
|
if (!user?.id) return
|
||||||
|
setIsDeleting(credentialsId)
|
||||||
|
const { error } = await deleteCredentials(user?.id, credentialsId)
|
||||||
|
setIsDeleting(undefined)
|
||||||
|
if (error) return toast({ title: error.name, description: error.message })
|
||||||
|
onCredentialsSelect(undefined)
|
||||||
|
mutate({ credentials: credentials.filter((c) => c.id !== credentialsId) })
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu isLazy placement="bottom-end" matchWidth>
|
<Menu isLazy placement="bottom-end" matchWidth>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
@@ -91,16 +121,27 @@ export const CredentialsDropdown = ({
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{credentialsList.map((credentials) => (
|
{credentialsList.map((credentials) => (
|
||||||
<MenuItem
|
<Button
|
||||||
|
role="menuitem"
|
||||||
|
minH="40px"
|
||||||
key={credentials.id}
|
key={credentials.id}
|
||||||
maxW="500px"
|
|
||||||
overflow="hidden"
|
|
||||||
whiteSpace="nowrap"
|
|
||||||
textOverflow="ellipsis"
|
|
||||||
onClick={handleMenuItemClick(credentials.id)}
|
onClick={handleMenuItemClick(credentials.id)}
|
||||||
|
fontSize="16px"
|
||||||
|
fontWeight="normal"
|
||||||
|
rounded="none"
|
||||||
|
colorScheme="gray"
|
||||||
|
variant="ghost"
|
||||||
|
justifyContent="space-between"
|
||||||
>
|
>
|
||||||
{credentials.name}
|
{credentials.name}
|
||||||
</MenuItem>
|
<IconButton
|
||||||
|
icon={<TrashIcon />}
|
||||||
|
aria-label="Remove credentials"
|
||||||
|
size="xs"
|
||||||
|
onClick={handleDeleteDomainClick(credentials.id)}
|
||||||
|
isLoading={isDeleting === credentials.id}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
))}
|
))}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
maxW="500px"
|
maxW="500px"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Divider, Stack, Text } from '@chakra-ui/react'
|
import { Divider, Stack, Text } from '@chakra-ui/react'
|
||||||
import { CredentialsDropdown } from 'components/shared/CredentialsDropdown'
|
|
||||||
import { DropdownList } from 'components/shared/DropdownList'
|
import { DropdownList } from 'components/shared/DropdownList'
|
||||||
import { TableList, TableListItemProps } from 'components/shared/TableList'
|
import { TableList, TableListItemProps } from 'components/shared/TableList'
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
@@ -24,6 +23,8 @@ import { SheetsDropdown } from './SheetsDropdown'
|
|||||||
import { SpreadsheetsDropdown } from './SpreadsheetDropdown'
|
import { SpreadsheetsDropdown } from './SpreadsheetDropdown'
|
||||||
import { CellWithValueStack } from './CellWithValueStack'
|
import { CellWithValueStack } from './CellWithValueStack'
|
||||||
import { CellWithVariableIdStack } from './CellWithVariableIdStack'
|
import { CellWithVariableIdStack } from './CellWithVariableIdStack'
|
||||||
|
import { omit } from 'services/utils'
|
||||||
|
import { CredentialsDropdown } from 'components/shared/CredentialsDropdown'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
options: GoogleSheetsOptions
|
options: GoogleSheetsOptions
|
||||||
@@ -45,8 +46,8 @@ export const GoogleSheetsSettingsBody = ({
|
|||||||
() => sheets?.find((s) => s.id === options?.sheetId),
|
() => sheets?.find((s) => s.id === options?.sheetId),
|
||||||
[sheets, options?.sheetId]
|
[sheets, options?.sheetId]
|
||||||
)
|
)
|
||||||
const handleCredentialsIdChange = (credentialsId: string) =>
|
const handleCredentialsIdChange = (credentialsId?: string) =>
|
||||||
onOptionsChange({ ...options, credentialsId })
|
onOptionsChange({ ...omit(options, 'credentialsId'), credentialsId })
|
||||||
const handleSpreadsheetIdChange = (spreadsheetId: string) =>
|
const handleSpreadsheetIdChange = (spreadsheetId: string) =>
|
||||||
onOptionsChange({ ...options, spreadsheetId })
|
onOptionsChange({ ...options, spreadsheetId })
|
||||||
const handleSheetIdChange = (sheetId: string) =>
|
const handleSheetIdChange = (sheetId: string) =>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
TextareaWithVariableButton,
|
TextareaWithVariableButton,
|
||||||
} from 'components/shared/TextboxWithVariableButton'
|
} from 'components/shared/TextboxWithVariableButton'
|
||||||
import { CredentialsType, SendEmailOptions } from 'models'
|
import { CredentialsType, SendEmailOptions } from 'models'
|
||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
import { SmtpConfigModal } from './SmtpConfigModal'
|
import { SmtpConfigModal } from './SmtpConfigModal'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -15,11 +15,15 @@ type Props = {
|
|||||||
|
|
||||||
export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
|
export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
const handleCredentialsSelect = (credentialsId: string) =>
|
const [refreshCredentialsKey, setRefreshCredentialsKey] = useState(0)
|
||||||
|
|
||||||
|
const handleCredentialsSelect = (credentialsId?: string) => {
|
||||||
|
setRefreshCredentialsKey(refreshCredentialsKey + 1)
|
||||||
onOptionsChange({
|
onOptionsChange({
|
||||||
...options,
|
...options,
|
||||||
credentialsId,
|
credentialsId: credentialsId === undefined ? 'default' : credentialsId,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const handleToChange = (recipientsStr: string) => {
|
const handleToChange = (recipientsStr: string) => {
|
||||||
const recipients: string[] = recipientsStr
|
const recipients: string[] = recipientsStr
|
||||||
@@ -31,6 +35,22 @@ export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCcChange = (ccStr: string) => {
|
||||||
|
const cc: string[] = ccStr.split(',').map((str) => str.trim())
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
cc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBccChange = (bccStr: string) => {
|
||||||
|
const bcc: string[] = bccStr.split(',').map((str) => str.trim())
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
bcc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubjectChange = (subject: string) =>
|
const handleSubjectChange = (subject: string) =>
|
||||||
onOptionsChange({
|
onOptionsChange({
|
||||||
...options,
|
...options,
|
||||||
@@ -55,6 +75,7 @@ export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
|
|||||||
defaultCredentialLabel={
|
defaultCredentialLabel={
|
||||||
process.env.NEXT_PUBLIC_EMAIL_NOTIFICATIONS_FROM_EMAIL
|
process.env.NEXT_PUBLIC_EMAIL_NOTIFICATIONS_FROM_EMAIL
|
||||||
}
|
}
|
||||||
|
refreshDropdownKey={refreshCredentialsKey}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
@@ -65,6 +86,22 @@ export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
|
|||||||
placeholder="email1@gmail.com, email2@gmail.com"
|
placeholder="email1@gmail.com, email2@gmail.com"
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Text>Cc: </Text>
|
||||||
|
<InputWithVariableButton
|
||||||
|
onChange={handleCcChange}
|
||||||
|
initialValue={options.cc?.join(', ') ?? ''}
|
||||||
|
placeholder="email1@gmail.com, email2@gmail.com"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Text>Bcc: </Text>
|
||||||
|
<InputWithVariableButton
|
||||||
|
onChange={handleBccChange}
|
||||||
|
initialValue={options.bcc?.join(', ') ?? ''}
|
||||||
|
placeholder="email1@gmail.com, email2@gmail.com"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text>Subject: </Text>
|
<Text>Subject: </Text>
|
||||||
<InputWithVariableButton
|
<InputWithVariableButton
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useUser } from 'contexts/UserContext'
|
|||||||
import { CredentialsType, SmtpCredentialsData } from 'models'
|
import { CredentialsType, SmtpCredentialsData } from 'models'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { createCredentials } from 'services/credentials'
|
import { createCredentials } from 'services/credentials'
|
||||||
|
import { testSmtpConfig } from 'services/integrations'
|
||||||
import { isNotDefined } from 'utils'
|
import { isNotDefined } from 'utils'
|
||||||
import { SmtpConfigForm } from './SmtpConfigForm'
|
import { SmtpConfigForm } from './SmtpConfigForm'
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ export const SmtpConfigModal = ({
|
|||||||
onNewCredentials,
|
onNewCredentials,
|
||||||
onClose,
|
onClose,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { user, mutateCredentials } = useUser()
|
const { user } = useUser()
|
||||||
const [isCreating, setIsCreating] = useState(false)
|
const [isCreating, setIsCreating] = useState(false)
|
||||||
const toast = useToast({
|
const toast = useToast({
|
||||||
position: 'top-right',
|
position: 'top-right',
|
||||||
@@ -39,14 +40,24 @@ export const SmtpConfigModal = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const handleCreateClick = async () => {
|
const handleCreateClick = async () => {
|
||||||
if (!user) return
|
if (!user?.email) return
|
||||||
setIsCreating(true)
|
setIsCreating(true)
|
||||||
|
const { error: testSmtpError } = await testSmtpConfig(
|
||||||
|
smtpConfig,
|
||||||
|
user.email
|
||||||
|
)
|
||||||
|
if (testSmtpError) {
|
||||||
|
setIsCreating(false)
|
||||||
|
return toast({
|
||||||
|
title: 'Invalid configuration',
|
||||||
|
description: "We couldn't send the test email with your configuration",
|
||||||
|
})
|
||||||
|
}
|
||||||
const { data, error } = await createCredentials(user.id, {
|
const { data, error } = await createCredentials(user.id, {
|
||||||
data: smtpConfig,
|
data: smtpConfig,
|
||||||
name: smtpConfig.from.email as string,
|
name: smtpConfig.from.email as string,
|
||||||
type: CredentialsType.SMTP,
|
type: CredentialsType.SMTP,
|
||||||
})
|
})
|
||||||
await mutateCredentials()
|
|
||||||
setIsCreating(false)
|
setIsCreating(false)
|
||||||
if (error) return toast({ title: error.name, description: error.message })
|
if (error) return toast({ title: error.name, description: error.message })
|
||||||
if (!data?.credentials)
|
if (!data?.credentials)
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ import { isDefined, isNotDefined } from 'utils'
|
|||||||
import { updateUser as updateUserInDb } from 'services/user'
|
import { updateUser as updateUserInDb } from 'services/user'
|
||||||
import { useToast } from '@chakra-ui/react'
|
import { useToast } from '@chakra-ui/react'
|
||||||
import { deepEqual } from 'fast-equals'
|
import { deepEqual } from 'fast-equals'
|
||||||
import { useCredentials } from 'services/credentials'
|
|
||||||
import { User } from 'db'
|
import { User } from 'db'
|
||||||
import { KeyedMutator } from 'swr'
|
|
||||||
import { Credentials } from 'models'
|
|
||||||
|
|
||||||
const userContext = createContext<{
|
const userContext = createContext<{
|
||||||
user?: User
|
user?: User
|
||||||
@@ -23,12 +20,8 @@ const userContext = createContext<{
|
|||||||
isSaving: boolean
|
isSaving: boolean
|
||||||
hasUnsavedChanges: boolean
|
hasUnsavedChanges: boolean
|
||||||
isOAuthProvider: boolean
|
isOAuthProvider: boolean
|
||||||
credentials: Credentials[]
|
|
||||||
updateUser: (newUser: Partial<User>) => void
|
updateUser: (newUser: Partial<User>) => void
|
||||||
saveUser: () => void
|
saveUser: () => void
|
||||||
mutateCredentials: KeyedMutator<{
|
|
||||||
credentials: Credentials[]
|
|
||||||
}>
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
}>({})
|
}>({})
|
||||||
@@ -41,11 +34,6 @@ export const UserContext = ({ children }: { children: ReactNode }) => {
|
|||||||
position: 'top-right',
|
position: 'top-right',
|
||||||
status: 'error',
|
status: 'error',
|
||||||
})
|
})
|
||||||
const { credentials, mutate: mutateCredentials } = useCredentials({
|
|
||||||
userId: user?.id,
|
|
||||||
onError: (error) =>
|
|
||||||
toast({ title: error.name, description: error.message }),
|
|
||||||
})
|
|
||||||
|
|
||||||
const [isSaving, setIsSaving] = useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
const isOAuthProvider = useMemo(
|
const isOAuthProvider = useMemo(
|
||||||
@@ -96,8 +84,6 @@ export const UserContext = ({ children }: { children: ReactNode }) => {
|
|||||||
isLoading: status === 'loading',
|
isLoading: status === 'loading',
|
||||||
hasUnsavedChanges,
|
hasUnsavedChanges,
|
||||||
isOAuthProvider,
|
isOAuthProvider,
|
||||||
credentials: credentials ?? [],
|
|
||||||
mutateCredentials,
|
|
||||||
updateUser,
|
updateUser,
|
||||||
saveUser,
|
saveUser,
|
||||||
}}
|
}}
|
||||||
|
|||||||
37
apps/builder/pages/api/integrations/email/test-config.ts
Normal file
37
apps/builder/pages/api/integrations/email/test-config.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { SmtpCredentialsData } from 'models'
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
import { createTransport } from 'nodemailer'
|
||||||
|
|
||||||
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
const { from, port, isTlsEnabled, username, password, host, to } =
|
||||||
|
JSON.parse(req.body) as SmtpCredentialsData & { to: string }
|
||||||
|
console.log({
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
secure: isTlsEnabled ?? undefined,
|
||||||
|
auth: {
|
||||||
|
user: username,
|
||||||
|
pass: password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const transporter = createTransport({
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
secure: isTlsEnabled ?? undefined,
|
||||||
|
auth: {
|
||||||
|
user: username,
|
||||||
|
pass: password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const info = await transporter.sendMail({
|
||||||
|
from: `"${from.name}" <${from.email}>`,
|
||||||
|
to,
|
||||||
|
subject: 'Your SMTP configuration is working 🤩',
|
||||||
|
text: 'This email has been sent to test out your SMTP config.\n\nIf your read this then it has been successful.🚀',
|
||||||
|
})
|
||||||
|
res.status(200).send({ message: 'Email sent!', info })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default handler
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { withSentry } from '@sentry/nextjs'
|
||||||
|
import { User } from 'db'
|
||||||
|
import prisma from 'libs/prisma'
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
import { getSession } from 'next-auth/react'
|
||||||
|
import { methodNotAllowed } from 'utils'
|
||||||
|
|
||||||
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
const session = await getSession({ req })
|
||||||
|
|
||||||
|
if (!session?.user)
|
||||||
|
return res.status(401).json({ message: 'Not authenticated' })
|
||||||
|
|
||||||
|
const user = session.user as User
|
||||||
|
const id = req.query.id.toString()
|
||||||
|
if (user.id !== id) return res.status(401).send({ message: 'Forbidden' })
|
||||||
|
if (req.method === 'DELETE') {
|
||||||
|
const credentialsId = req.query.credentialsId.toString()
|
||||||
|
const credentials = await prisma.credentials.delete({
|
||||||
|
where: { id: credentialsId },
|
||||||
|
})
|
||||||
|
return res.send({ credentials })
|
||||||
|
}
|
||||||
|
return methodNotAllowed(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withSentry(handler)
|
||||||
@@ -16,7 +16,7 @@ export const useCredentials = ({
|
|||||||
)
|
)
|
||||||
if (error) onError(error)
|
if (error) onError(error)
|
||||||
return {
|
return {
|
||||||
credentials: data?.credentials,
|
credentials: data?.credentials ?? [],
|
||||||
isLoading: !error && !data,
|
isLoading: !error && !data,
|
||||||
mutate,
|
mutate,
|
||||||
}
|
}
|
||||||
@@ -33,3 +33,14 @@ export const createCredentials = async (
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: credentials,
|
body: credentials,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const deleteCredentials = async (
|
||||||
|
userId: string,
|
||||||
|
credentialsId: string
|
||||||
|
) =>
|
||||||
|
sendRequest<{
|
||||||
|
credentials: Credentials
|
||||||
|
}>({
|
||||||
|
url: `/api/users/${userId}/credentials/${credentialsId}`,
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import { sendRequest } from 'utils'
|
|||||||
import { stringify } from 'qs'
|
import { stringify } from 'qs'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
import { fetcher } from './utils'
|
import { fetcher } from './utils'
|
||||||
import { Variable, VariableForTest, WebhookResponse } from 'models'
|
import {
|
||||||
|
SmtpCredentialsData,
|
||||||
|
Variable,
|
||||||
|
VariableForTest,
|
||||||
|
WebhookResponse,
|
||||||
|
} from 'models'
|
||||||
|
|
||||||
export const getGoogleSheetsConsentScreenUrl = (
|
export const getGoogleSheetsConsentScreenUrl = (
|
||||||
redirectUrl: string,
|
redirectUrl: string,
|
||||||
@@ -123,3 +128,13 @@ export const getDeepKeys = (obj: any): string[] => {
|
|||||||
}
|
}
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const testSmtpConfig = (smtpData: SmtpCredentialsData, to: string) =>
|
||||||
|
sendRequest({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/integrations/email/test-config',
|
||||||
|
body: {
|
||||||
|
...smtpData,
|
||||||
|
to,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const defaultFrom = {
|
|||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
await cors(req, res)
|
await cors(req, res)
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const { credentialsId, recipients, body, subject } = JSON.parse(
|
const { credentialsId, recipients, body, subject, cc, bcc } = JSON.parse(
|
||||||
req.body
|
req.body
|
||||||
) as SendEmailOptions
|
) as SendEmailOptions
|
||||||
|
|
||||||
@@ -47,6 +47,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
})
|
})
|
||||||
const info = await transporter.sendMail({
|
const info = await transporter.sendMail({
|
||||||
from: `"${from.name}" <${from.email}>`,
|
from: `"${from.name}" <${from.email}>`,
|
||||||
|
cc: cc?.join(''),
|
||||||
|
bcc: bcc?.join(''),
|
||||||
to: recipients.join(', '),
|
to: recipients.join(', '),
|
||||||
subject,
|
subject,
|
||||||
text: body,
|
text: body,
|
||||||
|
|||||||
@@ -192,6 +192,8 @@ const sendEmail = async (
|
|||||||
recipients: options.recipients.map(parseVariables(variables)),
|
recipients: options.recipients.map(parseVariables(variables)),
|
||||||
subject: parseVariables(variables)(options.subject ?? ''),
|
subject: parseVariables(variables)(options.subject ?? ''),
|
||||||
body: parseVariables(variables)(options.body ?? ''),
|
body: parseVariables(variables)(options.body ?? ''),
|
||||||
|
cc: (options.cc ?? []).map(parseVariables(variables)),
|
||||||
|
bcc: (options.bcc ?? []).map(parseVariables(variables)),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ export type SendEmailStep = StepBase & {
|
|||||||
export type SendEmailOptions = {
|
export type SendEmailOptions = {
|
||||||
credentialsId: string | 'default'
|
credentialsId: string | 'default'
|
||||||
recipients: string[]
|
recipients: string[]
|
||||||
|
cc?: string[]
|
||||||
|
bcc?: string[]
|
||||||
subject?: string
|
subject?: string
|
||||||
body?: string
|
body?: string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user