♻️ (builder) Change to features-centric folder structure

This commit is contained in:
Baptiste Arnaud
2022-11-15 09:35:48 +01:00
committed by Baptiste Arnaud
parent 3686465a85
commit 643571fe7d
683 changed files with 3907 additions and 3643 deletions

View File

@@ -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>
)
}

View File

@@ -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} />

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -0,0 +1 @@
export { SendEmailSettings } from './SendEmailSettings'

View File

@@ -0,0 +1,3 @@
export { SendEmailSettings } from './components/SendEmailSettings'
export { SendEmailContent } from './components/SendEmailContent'
export { SendEmailIcon } from './components/SendEmailIcon'

View File

@@ -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()
})
})

View File

@@ -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,
},
})