♻️ (builder) Change to features-centric folder structure
This commit is contained in:
committed by
Baptiste Arnaud
parent
3686465a85
commit
643571fe7d
@@ -0,0 +1,23 @@
|
||||
import { Tag, Text, Wrap, WrapItem } from '@chakra-ui/react'
|
||||
import { SendEmailBlock } from 'models'
|
||||
|
||||
type Props = {
|
||||
block: SendEmailBlock
|
||||
}
|
||||
|
||||
export const SendEmailContent = ({ block }: Props) => {
|
||||
if (block.options.recipients.length === 0)
|
||||
return <Text color="gray.500">Configure...</Text>
|
||||
return (
|
||||
<Wrap noOfLines={2} pr="6">
|
||||
<WrapItem>
|
||||
<Text>Send email to</Text>
|
||||
</WrapItem>
|
||||
{block.options.recipients.map((to) => (
|
||||
<WrapItem key={to}>
|
||||
<Tag>{to}</Tag>
|
||||
</WrapItem>
|
||||
))}
|
||||
</Wrap>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { SendEmailIcon as SendEmailIco } from '@/components/icons'
|
||||
import { IconProps } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
|
||||
export const SendEmailIcon = (props: IconProps) => <SendEmailIco {...props} />
|
||||
@@ -0,0 +1,207 @@
|
||||
import {
|
||||
Stack,
|
||||
useDisclosure,
|
||||
Text,
|
||||
Flex,
|
||||
HStack,
|
||||
Switch,
|
||||
FormLabel,
|
||||
} from '@chakra-ui/react'
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import { CredentialsType, SendEmailOptions, Variable } from 'models'
|
||||
import React, { useState } from 'react'
|
||||
import { env } from 'utils'
|
||||
import { SmtpConfigModal } from './SmtpConfigModal'
|
||||
import { SwitchWithLabel } from '@/components/SwitchWithLabel'
|
||||
import { VariableSearchInput } from '@/components/VariableSearchInput'
|
||||
import { CredentialsDropdown } from '@/features/credentials'
|
||||
import { Input, Textarea } from '@/components/inputs'
|
||||
|
||||
type Props = {
|
||||
options: SendEmailOptions
|
||||
onOptionsChange: (options: SendEmailOptions) => void
|
||||
}
|
||||
|
||||
export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const [refreshCredentialsKey, setRefreshCredentialsKey] = useState(0)
|
||||
|
||||
const handleCredentialsSelect = (credentialsId?: string) => {
|
||||
setRefreshCredentialsKey(refreshCredentialsKey + 1)
|
||||
onOptionsChange({
|
||||
...options,
|
||||
credentialsId: credentialsId === undefined ? 'default' : credentialsId,
|
||||
})
|
||||
}
|
||||
|
||||
const handleToChange = (recipientsStr: string) => {
|
||||
const recipients: string[] = recipientsStr
|
||||
.split(',')
|
||||
.map((str) => str.trim())
|
||||
onOptionsChange({
|
||||
...options,
|
||||
recipients,
|
||||
})
|
||||
}
|
||||
|
||||
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) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
subject,
|
||||
})
|
||||
|
||||
const handleBodyChange = (body: string) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
body,
|
||||
})
|
||||
|
||||
const handleReplyToChange = (replyTo: string) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
replyTo,
|
||||
})
|
||||
|
||||
const handleIsCustomBodyChange = (isCustomBody: boolean) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
isCustomBody,
|
||||
})
|
||||
|
||||
const handleIsBodyCodeChange = () =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
isBodyCode: options.isBodyCode ? !options.isBodyCode : true,
|
||||
})
|
||||
|
||||
const handleChangeAttachmentVariable = (
|
||||
variable: Pick<Variable, 'id' | 'name'> | undefined
|
||||
) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
attachmentsVariableId: variable?.id,
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
<Text>From: </Text>
|
||||
<CredentialsDropdown
|
||||
type={CredentialsType.SMTP}
|
||||
currentCredentialsId={options.credentialsId}
|
||||
onCredentialsSelect={handleCredentialsSelect}
|
||||
onCreateNewClick={onOpen}
|
||||
defaultCredentialLabel={env('SMTP_FROM')
|
||||
?.match(/\<(.*)\>/)
|
||||
?.pop()}
|
||||
refreshDropdownKey={refreshCredentialsKey}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text>Reply to: </Text>
|
||||
<Input
|
||||
onChange={handleReplyToChange}
|
||||
defaultValue={options.replyTo}
|
||||
placeholder={'email@gmail.com'}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text>To: </Text>
|
||||
<Input
|
||||
onChange={handleToChange}
|
||||
defaultValue={options.recipients.join(', ')}
|
||||
placeholder="email1@gmail.com, email2@gmail.com"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text>Cc: </Text>
|
||||
<Input
|
||||
onChange={handleCcChange}
|
||||
defaultValue={options.cc?.join(', ') ?? ''}
|
||||
placeholder="email1@gmail.com, email2@gmail.com"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text>Bcc: </Text>
|
||||
<Input
|
||||
onChange={handleBccChange}
|
||||
defaultValue={options.bcc?.join(', ') ?? ''}
|
||||
placeholder="email1@gmail.com, email2@gmail.com"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text>Subject: </Text>
|
||||
<Input
|
||||
data-testid="subject-input"
|
||||
onChange={handleSubjectChange}
|
||||
defaultValue={options.subject ?? ''}
|
||||
/>
|
||||
</Stack>
|
||||
<SwitchWithLabel
|
||||
label={'Custom content?'}
|
||||
initialValue={options.isCustomBody ?? false}
|
||||
onCheckChange={handleIsCustomBodyChange}
|
||||
/>
|
||||
{options.isCustomBody && (
|
||||
<Stack>
|
||||
<Flex justifyContent="space-between">
|
||||
<Text>Content: </Text>
|
||||
<HStack>
|
||||
<Text fontSize="sm">Text</Text>
|
||||
<Switch
|
||||
size="sm"
|
||||
isChecked={options.isBodyCode ?? false}
|
||||
onChange={handleIsBodyCodeChange}
|
||||
/>
|
||||
<Text fontSize="sm">Code</Text>
|
||||
</HStack>
|
||||
</Flex>
|
||||
{options.isBodyCode ? (
|
||||
<CodeEditor
|
||||
value={options.body ?? ''}
|
||||
onChange={handleBodyChange}
|
||||
lang="html"
|
||||
/>
|
||||
) : (
|
||||
<Textarea
|
||||
data-testid="body-input"
|
||||
minH="300px"
|
||||
onChange={handleBodyChange}
|
||||
defaultValue={options.body ?? ''}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable">
|
||||
Attachments:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options.attachmentsVariableId}
|
||||
onSelectVariable={handleChangeAttachmentVariable}
|
||||
/>
|
||||
</Stack>
|
||||
<SmtpConfigModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
onNewCredentials={handleCredentialsSelect}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Input, SmartNumberInput } from '@/components/inputs'
|
||||
import { SwitchWithLabel } from '@/components/SwitchWithLabel'
|
||||
import { FormControl, FormLabel, HStack, Stack } from '@chakra-ui/react'
|
||||
import { isDefined } from '@udecode/plate-common'
|
||||
import { SmtpCredentialsData } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
config: SmtpCredentialsData
|
||||
onConfigChange: (config: SmtpCredentialsData) => void
|
||||
}
|
||||
|
||||
export const SmtpConfigForm = ({ config, onConfigChange }: Props) => {
|
||||
const handleFromEmailChange = (email: string) =>
|
||||
onConfigChange({ ...config, from: { ...config.from, email } })
|
||||
const handleFromNameChange = (name: string) =>
|
||||
onConfigChange({ ...config, from: { ...config.from, name } })
|
||||
const handleHostChange = (host: string) => onConfigChange({ ...config, host })
|
||||
const handleUsernameChange = (username: string) =>
|
||||
onConfigChange({ ...config, username })
|
||||
const handlePasswordChange = (password: string) =>
|
||||
onConfigChange({ ...config, password })
|
||||
const handleTlsCheck = (isTlsEnabled: boolean) =>
|
||||
onConfigChange({ ...config, isTlsEnabled })
|
||||
const handlePortNumberChange = (port?: number) =>
|
||||
isDefined(port) && onConfigChange({ ...config, port })
|
||||
|
||||
return (
|
||||
<Stack as="form" spacing={4}>
|
||||
<Input
|
||||
isRequired
|
||||
label="From email"
|
||||
defaultValue={config.from.email ?? ''}
|
||||
onChange={handleFromEmailChange}
|
||||
placeholder="notifications@provider.com"
|
||||
withVariableButton={false}
|
||||
/>
|
||||
<Input
|
||||
label="From name"
|
||||
defaultValue={config.from.name ?? ''}
|
||||
onChange={handleFromNameChange}
|
||||
placeholder="John Smith"
|
||||
withVariableButton={false}
|
||||
/>
|
||||
<Input
|
||||
isRequired
|
||||
label="Host"
|
||||
defaultValue={config.host ?? ''}
|
||||
onChange={handleHostChange}
|
||||
placeholder="mail.provider.com"
|
||||
withVariableButton={false}
|
||||
/>
|
||||
<Input
|
||||
isRequired
|
||||
label="Username / Email"
|
||||
type="email"
|
||||
defaultValue={config.username ?? ''}
|
||||
onChange={handleUsernameChange}
|
||||
placeholder="user@provider.com"
|
||||
withVariableButton={false}
|
||||
/>
|
||||
<Input
|
||||
isRequired
|
||||
label="Password"
|
||||
type="password"
|
||||
defaultValue={config.password ?? ''}
|
||||
onChange={handlePasswordChange}
|
||||
withVariableButton={false}
|
||||
/>
|
||||
<SwitchWithLabel
|
||||
label="Secure?"
|
||||
initialValue={config.isTlsEnabled ?? false}
|
||||
onCheckChange={handleTlsCheck}
|
||||
moreInfoContent="If enabled, the connection will use TLS when connecting to server. If disabled then TLS is used if server supports the STARTTLS extension. In most cases enable it if you are connecting to port 465. For port 587 or 25 keep it disabled."
|
||||
/>
|
||||
<FormControl as={HStack} justifyContent="space-between" isRequired>
|
||||
<FormLabel mb="0">Port number:</FormLabel>
|
||||
<SmartNumberInput
|
||||
placeholder="25"
|
||||
value={config.port}
|
||||
onValueChange={handlePortNumberChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Button,
|
||||
} from '@chakra-ui/react'
|
||||
import { useUser } from '@/features/account'
|
||||
import { CredentialsType, SmtpCredentialsData } from 'models'
|
||||
import React, { useState } from 'react'
|
||||
import { isNotDefined } from 'utils'
|
||||
import { SmtpConfigForm } from './SmtpConfigForm'
|
||||
import { useWorkspace } from '@/features/workspace'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { testSmtpConfig } from '../../queries/testSmtpConfigQuery'
|
||||
import { createCredentialsQuery } from '@/features/credentials'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onNewCredentials: (id: string) => void
|
||||
}
|
||||
|
||||
export const SmtpConfigModal = ({
|
||||
isOpen,
|
||||
onNewCredentials,
|
||||
onClose,
|
||||
}: Props) => {
|
||||
const { user } = useUser()
|
||||
const { workspace } = useWorkspace()
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const { showToast } = useToast()
|
||||
const [smtpConfig, setSmtpConfig] = useState<SmtpCredentialsData>({
|
||||
from: {},
|
||||
port: 25,
|
||||
})
|
||||
|
||||
const handleCreateClick = async () => {
|
||||
if (!user?.email || !workspace?.id) return
|
||||
setIsCreating(true)
|
||||
const { error: testSmtpError } = await testSmtpConfig(
|
||||
smtpConfig,
|
||||
user.email
|
||||
)
|
||||
if (testSmtpError) {
|
||||
console.error(testSmtpError)
|
||||
setIsCreating(false)
|
||||
return showToast({
|
||||
title: 'Invalid configuration',
|
||||
description: "We couldn't send the test email with your configuration",
|
||||
})
|
||||
}
|
||||
const { data, error } = await createCredentialsQuery({
|
||||
data: smtpConfig,
|
||||
name: smtpConfig.from.email as string,
|
||||
type: CredentialsType.SMTP,
|
||||
workspaceId: workspace.id,
|
||||
})
|
||||
setIsCreating(false)
|
||||
if (error)
|
||||
return showToast({ title: error.name, description: error.message })
|
||||
if (!data?.credentials)
|
||||
return showToast({ description: "Credentials wasn't created" })
|
||||
onNewCredentials(data.credentials.id)
|
||||
onClose()
|
||||
}
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Create SMTP config</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<SmtpConfigForm config={smtpConfig} onConfigChange={setSmtpConfig} />
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
onClick={handleCreateClick}
|
||||
isDisabled={
|
||||
isNotDefined(smtpConfig.from.email) ||
|
||||
isNotDefined(smtpConfig.host) ||
|
||||
isNotDefined(smtpConfig.username) ||
|
||||
isNotDefined(smtpConfig.password) ||
|
||||
isNotDefined(smtpConfig.port)
|
||||
}
|
||||
isLoading={isCreating}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { SendEmailSettings } from './SendEmailSettings'
|
||||
@@ -0,0 +1,3 @@
|
||||
export { SendEmailSettings } from './components/SendEmailSettings'
|
||||
export { SendEmailContent } from './components/SendEmailContent'
|
||||
export { SendEmailIcon } from './components/SendEmailIcon'
|
||||
@@ -0,0 +1,72 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import cuid from 'cuid'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
|
||||
const typebotId = cuid()
|
||||
|
||||
test.describe('Send email block', () => {
|
||||
test('its configuration should work', async ({ page }) => {
|
||||
if (
|
||||
!process.env.SMTP_USERNAME ||
|
||||
!process.env.SMTP_PORT ||
|
||||
!process.env.SMTP_HOST ||
|
||||
!process.env.SMTP_PASSWORD ||
|
||||
!process.env.NEXT_PUBLIC_SMTP_FROM
|
||||
)
|
||||
throw new Error('SMTP_ env vars are missing')
|
||||
await importTypebotInDatabase(
|
||||
getTestAsset('typebots/integrations/sendEmail.json'),
|
||||
{
|
||||
id: typebotId,
|
||||
}
|
||||
)
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('text=Configure...')
|
||||
await page.click(`text=notifications@typebot.io`)
|
||||
await page.click('text=Connect new')
|
||||
const createButton = page.locator('button >> text=Create')
|
||||
await expect(createButton).toBeDisabled()
|
||||
await page.fill(
|
||||
'[placeholder="notifications@provider.com"]',
|
||||
process.env.SMTP_USERNAME
|
||||
)
|
||||
await page.fill('[placeholder="John Smith"]', 'John Smith')
|
||||
await page.fill('[placeholder="mail.provider.com"]', process.env.SMTP_HOST)
|
||||
await page.fill(
|
||||
'[placeholder="user@provider.com"]',
|
||||
process.env.SMTP_USERNAME
|
||||
)
|
||||
await page.fill('[type="password"]', process.env.SMTP_PASSWORD)
|
||||
await page.fill('input[role="spinbutton"]', process.env.SMTP_PORT)
|
||||
await expect(createButton).toBeEnabled()
|
||||
await createButton.click()
|
||||
|
||||
await expect(
|
||||
page.locator(`button >> text=${process.env.SMTP_USERNAME}`)
|
||||
).toBeVisible()
|
||||
|
||||
await page.fill(
|
||||
'[placeholder="email1@gmail.com, email2@gmail.com"]',
|
||||
'email1@gmail.com, email2@gmail.com'
|
||||
)
|
||||
await expect(page.locator('span >> text=email1@gmail.com')).toBeVisible()
|
||||
await expect(page.locator('span >> text=email2@gmail.com')).toBeVisible()
|
||||
|
||||
await page.fill(
|
||||
'[placeholder="email1@gmail.com, email2@gmail.com"]',
|
||||
'email1@gmail.com, email2@gmail.com'
|
||||
)
|
||||
await page.fill('[data-testid="subject-input"]', 'Email subject')
|
||||
await page.click('text="Custom content?"')
|
||||
await page.fill('[data-testid="body-input"]', 'Here is my email')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await typebotViewer(page).locator('text=Go').click()
|
||||
await expect(
|
||||
page.locator('text=Emails are not sent in preview mode >> nth=0')
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
import { SmtpCredentialsData } from 'models'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const testSmtpConfig = (smtpData: SmtpCredentialsData, to: string) =>
|
||||
sendRequest({
|
||||
method: 'POST',
|
||||
url: '/api/integrations/email/test-config',
|
||||
body: {
|
||||
...smtpData,
|
||||
to,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user