2
0

Add Chatwoot livechat integration

Closes #159
This commit is contained in:
Baptiste Arnaud
2022-11-10 10:53:44 +01:00
parent 92147c315f
commit ea84039024
32 changed files with 464 additions and 62 deletions

View File

@ -24,7 +24,6 @@ export const WorkspaceSettingsForm = ({ onClose }: { onClose: () => void }) => {
const handleChangeIcon = (icon: string) => {
if (!workspace?.id) return
console.log(icon)
updateWorkspace(workspace?.id, { icon })
}

View File

@ -30,6 +30,7 @@ import {
PabblyConnectLogo,
ZapierLogo,
} from 'assets/logos'
import { ChatwootLogo } from 'features/chatwoot'
import {
BubbleBlockType,
InputBlockType,
@ -95,6 +96,8 @@ export const BlockIcon = ({ type, ...props }: BlockIconProps) => {
return <PabblyConnectLogo {...props} />
case IntegrationBlockType.EMAIL:
return <SendEmailIcon {...props} />
case IntegrationBlockType.CHATWOOT:
return <ChatwootLogo {...props} />
case 'start':
return <FlagIcon {...props} />
}

View File

@ -98,5 +98,7 @@ export const BlockTypeLabel = ({ type }: Props): JSX.Element => {
return <Text>Pabbly</Text>
case IntegrationBlockType.EMAIL:
return <Text>Email</Text>
case IntegrationBlockType.CHATWOOT:
return <Text>Chatwoot</Text>
}
}

View File

@ -115,7 +115,6 @@ export const ResultsActionButtons = ({
const data = dataToUnparse.map<{ [key: string]: string }>((data) => {
const newObject: { [key: string]: string } = {}
fields?.forEach((field) => {
console.log(data[field])
newObject[field] = data[field]?.plainText
})
return newObject

View File

@ -1,4 +1,5 @@
import { Text } from '@chakra-ui/react'
import { ChatwootBlockNodeLabel } from 'features/chatwoot'
import {
Block,
StartBlock,
@ -153,6 +154,9 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
case IntegrationBlockType.EMAIL: {
return <SendEmailContent block={block} />
}
case IntegrationBlockType.CHATWOOT: {
return <ChatwootBlockNodeLabel block={block} />
}
case 'start': {
return <Text>Start</Text>
}

View File

@ -7,6 +7,7 @@ import {
IconButton,
} from '@chakra-ui/react'
import { ExpandIcon } from 'assets/icons'
import { ChatwootSettingsForm } from 'features/chatwoot/components'
import {
ConditionItem,
ConditionBlock,
@ -276,5 +277,13 @@ export const BlockSettings = ({
/>
)
}
case IntegrationBlockType.CHATWOOT: {
return (
<ChatwootSettingsForm
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
}
}

View File

@ -8,7 +8,7 @@ type Props = {
export const MoreInfoTooltip = ({ children }: Props) => {
return (
<Tooltip label={children} hasArrow rounded="md" p="3">
<Tooltip label={children} hasArrow rounded="md" p="3" placement="top">
<chakra.span cursor="pointer">
<HelpCircleIcon />
</chakra.span>

View File

@ -1,5 +1,7 @@
import {
ComponentWithAs,
FormControl,
FormLabel,
HStack,
InputProps,
TextareaProps,
@ -9,6 +11,7 @@ import React, { ChangeEvent, useEffect, useRef, useState } from 'react'
import { useDebouncedCallback } from 'use-debounce'
import { env } from 'utils'
import { VariablesButton } from '../buttons/VariablesButton'
import { MoreInfoTooltip } from '../MoreInfoTooltip'
export type TextBoxProps = {
onChange: (value: string) => void
@ -17,6 +20,8 @@ export type TextBoxProps = {
| ComponentWithAs<'input', InputProps>
withVariableButton?: boolean
debounceTimeout?: number
label?: string
moreInfoTooltip?: string
} & Omit<InputProps & TextareaProps, 'onChange'>
export const TextBox = ({
@ -24,6 +29,8 @@ export const TextBox = ({
TextBox,
withVariableButton = true,
debounceTimeout = 1000,
label,
moreInfoTooltip,
...props
}: TextBoxProps) => {
const textBoxRef = useRef<(HTMLInputElement & HTMLTextAreaElement) | null>(
@ -92,29 +99,36 @@ export const TextBox = ({
setCarretPosition(textBoxRef.current.selectionStart)
}
if (!withVariableButton) {
return (
<TextBox
ref={textBoxRef}
onChange={handleChange}
bgColor={'white'}
value={value}
{...props}
/>
)
}
const Input = (
<TextBox
ref={textBoxRef}
value={value}
onKeyUp={handleKeyUp}
onClick={handleKeyUp}
onChange={handleChange}
bgColor={'white'}
{...props}
/>
)
return (
<HStack spacing={0} align={'flex-end'}>
<TextBox
ref={textBoxRef}
value={value}
onKeyUp={handleKeyUp}
onClick={handleKeyUp}
onChange={handleChange}
bgColor={'white'}
{...props}
/>
<VariablesButton onSelectVariable={handleVariableSelected} />
</HStack>
<FormControl isRequired={props.isRequired}>
{label && (
<FormLabel>
{label}{' '}
{moreInfoTooltip && (
<MoreInfoTooltip>{moreInfoTooltip}</MoreInfoTooltip>
)}
</FormLabel>
)}
{withVariableButton ? (
<HStack spacing={0} align={'flex-end'}>
{Input}
<VariablesButton onSelectVariable={handleVariableSelected} />
</HStack>
) : (
Input
)}
</FormControl>
)
}

View File

@ -46,8 +46,6 @@ export const ResultsProvider = ({
typebotId,
})
console.log(data?.flatMap((d) => d.results) ?? [])
const fetchMore = () => setSize((state) => state + 1)
const resultHeader = useMemo(

View File

@ -0,0 +1,43 @@
import test, { expect } from '@playwright/test'
import { createTypebots } from 'utils/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
import cuid from 'cuid'
import { defaultChatwootOptions, IntegrationBlockType } from 'models'
import { typebotViewer } from 'utils/playwright/testHelpers'
const typebotId = cuid()
const chatwootTestWebsiteToken = 'tueXiiqEmrWUCZ4NUyoR7nhE'
test.describe('Chatwoot block', () => {
test('should be configurable', async ({ page }) => {
await createTypebots([
{
id: typebotId,
...parseDefaultGroupWithBlock({
type: IntegrationBlockType.CHATWOOT,
options: defaultChatwootOptions,
}),
},
])
await page.goto(`/typebots/${typebotId}/edit`)
await page.getByText('Configure...').click()
await expect(page.getByLabel('Base URL')).toHaveAttribute(
'value',
defaultChatwootOptions.baseUrl
)
await page.getByLabel('Website token').fill(chatwootTestWebsiteToken)
await expect(page.getByText('Open Chatwoot')).toBeVisible()
await page.getByRole('button', { name: 'Set user details' }).click()
await page.getByLabel('ID').fill('123')
await page.getByLabel('Name').fill('John Doe')
await page.getByLabel('Email').fill('john@email.com')
await page.getByLabel('Avatar URL').fill('https://domain.com/avatar.png')
await page.getByLabel('Phone number').fill('+33654347543')
await page.getByRole('button', { name: 'Preview' }).click()
await expect(
page.getByText("Chatwoot won't open in preview mode").nth(0)
).toBeVisible()
})
})

View File

@ -0,0 +1,13 @@
import { Text } from '@chakra-ui/react'
import { ChatwootBlock } from 'models'
type Props = {
block: ChatwootBlock
}
export const ChatwootBlockNodeLabel = ({ block }: Props) =>
block.options.websiteToken.length === 0 ? (
<Text color="gray.500">Configure...</Text>
) : (
<Text>Open Chatwoot</Text>
)

View File

@ -0,0 +1,29 @@
import { Icon, IconProps } from '@chakra-ui/react'
export const ChatwootLogo = (props: IconProps) => (
<Icon
viewBox="0 0 512 512"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g
id="Square-logo"
stroke="none"
stroke-width="1"
fill="none"
fill-rule="evenodd"
>
<g id="chatwoot_logo" fill-rule="nonzero">
<circle id="Oval" fill="#47A7F6" cx="256" cy="256" r="256"></circle>
<path
d="M362.807947,368.807947 L244.122956,368.807947 C178.699407,368.807947 125.456954,315.561812 125.456954,250.12177 C125.456954,184.703089 178.699407,131.456954 244.124143,131.456954 C309.565494,131.456954 362.807947,184.703089 362.807947,250.12177 L362.807947,368.807947 Z"
id="Fill-1"
stroke="#FFFFFF"
stroke-width="6"
fill="#FFFFFF"
></path>
</g>
</g>
</Icon>
)

View File

@ -0,0 +1,98 @@
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Stack,
} from '@chakra-ui/react'
import { Input } from 'components/shared/Textbox'
import { ChatwootOptions } from 'models'
import React from 'react'
type Props = {
options: ChatwootOptions
onOptionsChange: (options: ChatwootOptions) => void
}
export const ChatwootSettingsForm = ({ options, onOptionsChange }: Props) => {
return (
<Stack spacing={4}>
<Input
isRequired
label="Base URL"
defaultValue={options.baseUrl}
onChange={(baseUrl: string) => {
onOptionsChange({ ...options, baseUrl })
}}
withVariableButton={false}
/>
<Input
isRequired
label="Website token"
defaultValue={options.websiteToken}
onChange={(websiteToken) =>
onOptionsChange({ ...options, websiteToken })
}
moreInfoTooltip="Can be found in Chatwoot under Settings > Inboxes > Settings > Configuration, in the code snippet."
/>
<Accordion allowMultiple>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Set user details
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="4">
<Input
label="ID"
defaultValue={options.user?.id}
onChange={(id: string) => {
onOptionsChange({ ...options, user: { ...options.user, id } })
}}
/>
<Input
label="Name"
defaultValue={options.user?.name}
onChange={(name: string) => {
onOptionsChange({
...options,
user: { ...options.user, name },
})
}}
/>
<Input
label="Email"
defaultValue={options.user?.email}
onChange={(email: string) => {
onOptionsChange({
...options,
user: { ...options.user, email },
})
}}
/>
<Input
label="Avatar URL"
defaultValue={options.user?.avatarUrl}
onChange={(avatarUrl: string) => {
onOptionsChange({
...options,
user: { ...options.user, avatarUrl },
})
}}
/>
<Input
label="Phone number"
defaultValue={options.user?.phoneNumber}
onChange={(phoneNumber: string) => {
onOptionsChange({
...options,
user: { ...options.user, phoneNumber },
})
}}
/>
</AccordionPanel>
</AccordionItem>
</Accordion>
</Stack>
)
}

View File

@ -0,0 +1,3 @@
export { ChatwootLogo } from './ChatwootLogo'
export { ChatwootBlockNodeLabel } from './ChatwootBlockNodeLabel'
export { ChatwootSettingsForm } from './ChatwootSettingsForm'

View File

@ -0,0 +1 @@
export * from './components'

View File

@ -4,7 +4,6 @@ import { playwrightBaseConfig } from 'configs/playwright'
const config: PlaywrightTestConfig = {
...playwrightBaseConfig,
testDir: path.join(__dirname, 'playwright/tests'),
webServer: process.env.CI
? {
...(playwrightBaseConfig.webServer as { command: string }),

View File

@ -36,7 +36,7 @@ test.describe.parallel('Image bubble block', () => {
process.env.S3_ENDPOINT
}${process.env.S3_PORT ? `:${process.env.S3_PORT}` : ''}/${
process.env.S3_BUCKET
}/public/typebots/${typebotId}/blocks/block1`
}/public/typebots/${typebotId}/blocks/block2`
)
})

View File

@ -48,10 +48,10 @@ test.describe.parallel('Buttons input block', () => {
await expect(typebotViewer(page).locator('text=Item 3')).toBeVisible()
await page.click('button[aria-label="Close"]')
await page.click('[data-testid="block1-icon"]')
await page.click('[data-testid="block2-icon"]')
await page.click('text=Multiple choice?')
await page.fill('#button', 'Go')
await page.click('[data-testid="block1-icon"]')
await page.click('[data-testid="block2-icon"]')
await page.locator('text=Item 1').hover()
await page.waitForTimeout(1000)

View File

@ -65,7 +65,8 @@ test.describe('Webhook block', () => {
)
await page.click('text=Headers')
await page.click('text=Add a value')
await page.waitForTimeout(200)
await page.getByRole('button', { name: 'Add a value' }).click()
await page.fill('input[placeholder="e.g. Content-Type"]', 'Custom-Typebot')
await page.fill(
'input[placeholder="e.g. application/json"]',

View File

@ -62,6 +62,7 @@ import cuid from 'cuid'
import { diff } from 'deep-object-diff'
import { duplicateWebhook } from 'services/webhook'
import { Plan } from 'db'
import { defaultChatwootOptions } from 'models'
export type TypebotInDashboard = Pick<
Typebot,
@ -350,6 +351,8 @@ const parseDefaultBlockOptions = (type: BlockWithOptionsType): BlockOptions => {
return defaultWebhookOptions
case IntegrationBlockType.EMAIL:
return defaultSendEmailOptions
case IntegrationBlockType.CHATWOOT:
return defaultChatwootOptions
}
}

View File

@ -0,0 +1,31 @@
import test, { expect } from '@playwright/test'
import cuid from 'cuid'
import { createTypebots } from 'utils/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
import { defaultChatwootOptions, IntegrationBlockType } from 'models'
import { typebotViewer } from 'utils/playwright/testHelpers'
const typebotId = cuid()
const chatwootTestWebsiteToken = 'tueXiiqEmrWUCZ4NUyoR7nhE'
test('should work as expected', async ({ page }) => {
await createTypebots([
{
id: typebotId,
...parseDefaultGroupWithBlock(
{
type: IntegrationBlockType.CHATWOOT,
options: {
...defaultChatwootOptions,
websiteToken: chatwootTestWebsiteToken,
},
},
{ withGoButton: true }
),
},
])
await page.goto(`/${typebotId}-public`)
await typebotViewer(page).getByRole('button', { name: 'Go' }).click()
await expect(page.locator('#chatwoot_live_chat_widget')).toBeVisible()
})