2
0

(credentials) Add credentials management menu in workspace settings

Closes #1567
This commit is contained in:
Baptiste Arnaud
2024-07-16 15:11:48 +02:00
parent db628cd051
commit c6005c49a2
81 changed files with 3582 additions and 1704 deletions

View File

@ -1 +1 @@
node_modules node_modules

View File

@ -1,3 +1,4 @@
emojiList.json emojiList.json
iconNames.ts iconNames.ts
reporters reporters
.last-run.json

View File

@ -0,0 +1 @@
src/test/reporters

View File

@ -100,7 +100,7 @@
}, },
"devDependencies": { "devDependencies": {
"@chakra-ui/styled-system": "2.9.2", "@chakra-ui/styled-system": "2.9.2",
"@playwright/test": "1.43.1", "@playwright/test": "1.45.2",
"@typebot.io/billing": "workspace:*", "@typebot.io/billing": "workspace:*",
"@typebot.io/forge": "workspace:*", "@typebot.io/forge": "workspace:*",
"@typebot.io/forge-repository": "workspace:*", "@typebot.io/forge-repository": "workspace:*",

View File

@ -10,13 +10,13 @@ export default defineConfig({
timeout: process.env.CI ? 10 * 1000 : 5 * 1000, timeout: process.env.CI ? 10 * 1000 : 5 * 1000,
}, },
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
workers: process.env.CI ? 1 : 3, workers: process.env.CI ? 1 : 4,
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 1,
reporter: [ reporter: [
[process.env.CI ? 'github' : 'list'], [process.env.CI ? 'github' : 'list'],
['html', { outputFolder: 'src/test/reporters' }], ['html', { outputFolder: 'src/test/reporters' }],
], ],
maxFailures: process.env.CI ? 10 : undefined, maxFailures: 10,
webServer: process.env.CI webServer: process.env.CI
? { ? {
command: 'pnpm run start', command: 'pnpm run start',

View File

@ -695,3 +695,11 @@ export const VideoPopoverIcon = (props: IconProps) => (
/> />
</Icon> </Icon>
) )
export const WalletIcon = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M3 9a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2" />
<path d="M3 11h3c.8 0 1.6.3 2.1.9l1.1.9c1.6 1.6 4.1 1.6 5.7 0l1.1-.9c.5-.5 1.3-.9 2.1-.9H21" />
</Icon>
)

View File

@ -49,6 +49,7 @@ export const NumberInput = <HasVariable extends boolean>({
helperText, helperText,
...props ...props
}: Props<HasVariable>) => { }: Props<HasVariable>) => {
const [isTouched, setIsTouched] = useState(false)
const [value, setValue] = useState(defaultValue?.toString() ?? '') const [value, setValue] = useState(defaultValue?.toString() ?? '')
const onValueChangeDebounced = useDebouncedCallback( const onValueChangeDebounced = useDebouncedCallback(
@ -56,6 +57,11 @@ export const NumberInput = <HasVariable extends boolean>({
env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout
) )
useEffect(() => {
if (isTouched || value !== '' || !defaultValue) return
setValue(defaultValue?.toString() ?? '')
}, [defaultValue, isTouched, value])
useEffect( useEffect(
() => () => { () => () => {
onValueChangeDebounced.flush() onValueChangeDebounced.flush()
@ -64,6 +70,7 @@ export const NumberInput = <HasVariable extends boolean>({
) )
const handleValueChange = (newValue: string) => { const handleValueChange = (newValue: string) => {
if (!isTouched) setIsTouched(true)
if (value.startsWith('{{') && value.endsWith('}}') && newValue !== '') if (value.startsWith('{{') && value.endsWith('}}') && newValue !== '')
return return
setValue(newValue) setValue(newValue)

View File

@ -12,7 +12,7 @@ import { isDefined } from '@typebot.io/lib'
export type SwitchWithLabelProps = { export type SwitchWithLabelProps = {
label: string label: string
initialValue: boolean initialValue: boolean | undefined
moreInfoContent?: string moreInfoContent?: string
onCheckChange?: (isChecked: boolean) => void onCheckChange?: (isChecked: boolean) => void
justifyContent?: FormControlProps['justifyContent'] justifyContent?: FormControlProps['justifyContent']

View File

@ -0,0 +1,16 @@
import { Icon, IconProps } from '@chakra-ui/react'
export const StripeLogo = (props: IconProps) => {
return (
<Icon viewBox="0 0 400 400" {...props}>
<path
style={{ fillRule: 'evenodd', clipRule: 'evenodd', fill: '#635bff' }}
d="M0 0h400v400H0z"
/>
<path
d="M184.4 155.5c0-9.4 7.7-13.1 20.5-13.1 18.4 0 41.6 5.6 60 15.5v-56.8C244.8 93.1 225 90 205 90c-49.1 0-81.7 25.6-81.7 68.4 0 66.7 91.9 56.1 91.9 84.9 0 11.1-9.7 14.7-23.2 14.7-20.1 0-45.7-8.2-66-19.3v57.5c22.5 9.7 45.2 13.8 66 13.8 50.3 0 84.9-24.9 84.9-68.2-.4-72-92.5-59.2-92.5-86.3z"
style={{ fillRule: 'evenodd', clipRule: 'evenodd', fill: '#fff' }}
/>
</Icon>
)
}

View File

@ -36,6 +36,21 @@ export const StripeConfigModal = ({
onNewCredentials, onNewCredentials,
onClose, onClose,
}: Props) => { }: Props) => {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<StripeCreateModalContent
onNewCredentials={onNewCredentials}
onClose={onClose}
/>
</Modal>
)
}
export const StripeCreateModalContent = ({
onNewCredentials,
onClose,
}: Pick<Props, 'onClose' | 'onNewCredentials'>) => {
const { t } = useTranslate() const { t } = useTranslate()
const { user } = useUser() const { user } = useUser()
const { workspace } = useWorkspace() const { workspace } = useWorkspace()
@ -99,7 +114,8 @@ export const StripeConfigModal = ({
test: { ...stripeConfig.test, secretKey }, test: { ...stripeConfig.test, secretKey },
}) })
const createCredentials = async () => { const createCredentials = async (e: React.FormEvent) => {
e.preventDefault()
if (!user?.email || !workspace?.id) return if (!user?.email || !workspace?.id) return
mutate({ mutate({
credentials: { credentials: {
@ -120,16 +136,16 @@ export const StripeConfigModal = ({
}, },
}) })
} }
return ( return (
<Modal isOpen={isOpen} onClose={onClose}> <ModalContent>
<ModalOverlay /> <ModalHeader>
<ModalContent> {t('blocks.inputs.payment.settings.stripeConfig.title.label')}
<ModalHeader> </ModalHeader>
{t('blocks.inputs.payment.settings.stripeConfig.title.label')} <ModalCloseButton />
</ModalHeader> <form onSubmit={createCredentials}>
<ModalCloseButton />
<ModalBody> <ModalBody>
<Stack as="form" spacing={4}> <Stack spacing={4}>
<TextInput <TextInput
isRequired isRequired
label={t( label={t(
@ -208,8 +224,8 @@ export const StripeConfigModal = ({
<ModalFooter> <ModalFooter>
<Button <Button
type="submit"
colorScheme="blue" colorScheme="blue"
onClick={createCredentials}
isDisabled={ isDisabled={
stripeConfig.live.publicKey === '' || stripeConfig.live.publicKey === '' ||
stripeConfig.name === '' || stripeConfig.name === '' ||
@ -220,7 +236,7 @@ export const StripeConfigModal = ({
{t('connect')} {t('connect')}
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </form>
</Modal> </ModalContent>
) )
} }

View File

@ -0,0 +1,236 @@
import { TextInput } from '@/components/inputs'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { TextLink } from '@/components/TextLink'
import { useUser } from '@/features/account/hooks/useUser'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { trpc } from '@/lib/trpc'
import {
Text,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
Stack,
FormLabel,
HStack,
FormControl,
ModalFooter,
Button,
} from '@chakra-ui/react'
import { useTranslate } from '@tolgee/react'
import { isNotEmpty } from '@typebot.io/lib'
import { StripeCredentials } from '@typebot.io/schemas'
import { useEffect, useState } from 'react'
type Props = {
credentialsId: string
onUpdate: () => void
}
export const UpdateStripeCredentialsModalContent = ({
credentialsId,
onUpdate,
}: Props) => {
const { t } = useTranslate()
const { user } = useUser()
const { workspace } = useWorkspace()
const [isCreating, setIsCreating] = useState(false)
const [stripeConfig, setStripeConfig] = useState<
StripeCredentials['data'] & { name: string }
>()
const { data: existingCredentials } =
trpc.credentials.getCredentials.useQuery(
{
credentialsId,
workspaceId: workspace!.id,
},
{
enabled: !!workspace?.id,
}
)
useEffect(() => {
if (!existingCredentials || stripeConfig) return
setStripeConfig({
name: existingCredentials.name,
live: existingCredentials.data.live,
test: existingCredentials.data.test,
})
}, [existingCredentials, stripeConfig])
const { mutate } = trpc.credentials.updateCredentials.useMutation({
onMutate: () => setIsCreating(true),
onSettled: () => setIsCreating(false),
onSuccess: () => {
onUpdate()
},
})
const handleNameChange = (name: string) =>
stripeConfig &&
setStripeConfig({
...stripeConfig,
name,
})
const handlePublicKeyChange = (publicKey: string) =>
stripeConfig &&
setStripeConfig({
...stripeConfig,
live: { ...stripeConfig.live, publicKey },
})
const handleSecretKeyChange = (secretKey: string) =>
stripeConfig &&
setStripeConfig({
...stripeConfig,
live: { ...stripeConfig.live, secretKey },
})
const handleTestPublicKeyChange = (publicKey: string) =>
stripeConfig &&
setStripeConfig({
...stripeConfig,
test: { ...stripeConfig.test, publicKey },
})
const handleTestSecretKeyChange = (secretKey: string) =>
stripeConfig &&
setStripeConfig({
...stripeConfig,
test: { ...stripeConfig.test, secretKey },
})
const updateCreds = async (e: React.FormEvent) => {
e.preventDefault()
if (!user?.email || !workspace?.id || !stripeConfig) return
mutate({
credentialsId,
credentials: {
data: {
live: stripeConfig.live,
test: {
publicKey: isNotEmpty(stripeConfig.test.publicKey)
? stripeConfig.test.publicKey
: undefined,
secretKey: isNotEmpty(stripeConfig.test.secretKey)
? stripeConfig.test.secretKey
: undefined,
},
},
name: stripeConfig.name,
type: 'stripe',
workspaceId: workspace.id,
},
})
}
return (
<ModalContent>
<ModalHeader>
{t('blocks.inputs.payment.settings.stripeConfig.title.label')}
</ModalHeader>
<ModalCloseButton />
<form onSubmit={updateCreds}>
<ModalBody>
<Stack as="form" spacing={4}>
<TextInput
isRequired
label={t(
'blocks.inputs.payment.settings.stripeConfig.accountName.label'
)}
defaultValue={stripeConfig?.name}
onChange={handleNameChange}
placeholder="Typebot"
withVariableButton={false}
debounceTimeout={0}
/>
<Stack>
<FormLabel>
{t(
'blocks.inputs.payment.settings.stripeConfig.testKeys.label'
)}{' '}
<MoreInfoTooltip>
{t(
'blocks.inputs.payment.settings.stripeConfig.testKeys.infoText.label'
)}
</MoreInfoTooltip>
</FormLabel>
<HStack>
<TextInput
onChange={handleTestPublicKeyChange}
placeholder="pk_test_..."
withVariableButton={false}
defaultValue={stripeConfig?.test?.publicKey}
debounceTimeout={0}
/>
<TextInput
onChange={handleTestSecretKeyChange}
placeholder="sk_test_..."
withVariableButton={false}
debounceTimeout={0}
defaultValue={stripeConfig?.test?.secretKey}
type="password"
/>
</HStack>
</Stack>
<Stack>
<FormLabel>
{t(
'blocks.inputs.payment.settings.stripeConfig.liveKeys.label'
)}
</FormLabel>
<HStack>
<FormControl>
<TextInput
onChange={handlePublicKeyChange}
placeholder="pk_live_..."
withVariableButton={false}
defaultValue={stripeConfig?.live?.publicKey}
debounceTimeout={0}
/>
</FormControl>
<FormControl>
<TextInput
onChange={handleSecretKeyChange}
placeholder="sk_live_..."
withVariableButton={false}
defaultValue={stripeConfig?.live?.secretKey}
debounceTimeout={0}
type="password"
/>
</FormControl>
</HStack>
</Stack>
<Text>
({t('blocks.inputs.payment.settings.stripeConfig.findKeys.label')}{' '}
<TextLink href="https://dashboard.stripe.com/apikeys" isExternal>
{t(
'blocks.inputs.payment.settings.stripeConfig.findKeys.here.label'
)}
</TextLink>
)
</Text>
</Stack>
</ModalBody>
<ModalFooter>
<Button
type="submit"
colorScheme="blue"
isDisabled={
stripeConfig?.live.publicKey === '' ||
stripeConfig?.name === '' ||
stripeConfig?.live.secretKey === ''
}
isLoading={isCreating}
>
{t('connect')}
</Button>
</ModalFooter>
</form>
</ModalContent>
)
}

View File

@ -20,7 +20,8 @@ test.describe('Payment input block', () => {
await page.goto(`/typebots/${typebotId}/edit`) await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Configure...') await page.click('text=Configure...')
await page.getByRole('button', { name: 'Add Stripe account' }).click() await page.getByRole('button', { name: 'Select Stripe account' }).click()
await page.getByRole('menuitem', { name: 'Connect new' }).click()
await page.fill('[placeholder="Typebot"]', 'My Stripe Account') await page.fill('[placeholder="Typebot"]', 'My Stripe Account')
await page.fill('[placeholder="sk_test_..."]', env.STRIPE_SECRET_KEY ?? '') await page.fill('[placeholder="sk_test_..."]', env.STRIPE_SECRET_KEY ?? '')
await page.fill('[placeholder="sk_live_..."]', env.STRIPE_SECRET_KEY ?? '') await page.fill('[placeholder="sk_live_..."]', env.STRIPE_SECRET_KEY ?? '')

View File

@ -21,8 +21,8 @@ import { getGoogleSheetsConsentScreenUrlQuery } from '../queries/getGoogleSheets
type Props = { type Props = {
isOpen: boolean isOpen: boolean
typebotId: string typebotId?: string
blockId: string blockId?: string
onClose: () => void onClose: () => void
} }
@ -32,30 +32,45 @@ export const GoogleSheetConnectModal = ({
isOpen, isOpen,
onClose, onClose,
}: Props) => { }: Props) => {
const { workspace } = useWorkspace()
return ( return (
<Modal isOpen={isOpen} onClose={onClose} size="lg"> <Modal isOpen={isOpen} onClose={onClose} size="lg">
<ModalOverlay /> <ModalOverlay />
<ModalContent> <GoogleSheetConnectModalContent typebotId={typebotId} blockId={blockId} />
<ModalHeader>Connect Spreadsheets</ModalHeader> </Modal>
<ModalCloseButton /> )
<ModalBody as={Stack} spacing="6"> }
<Text>
Make sure to check all the permissions so that the integration works export const GoogleSheetConnectModalContent = ({
as expected: typebotId,
</Text> blockId,
<Image }: {
src="/images/google-spreadsheets-scopes.png" typebotId?: string
alt="Google Spreadsheets checkboxes" blockId?: string
rounded="md" }) => {
/> const { workspace } = useWorkspace()
<AlertInfo>
Google does not provide more granular permissions than return (
&quot;read&quot; or &quot;write&quot; access. That&apos;s why it <ModalContent>
states that Typebot can also delete your spreadsheets which it <ModalHeader>Connect Spreadsheets</ModalHeader>
won&apos;t. <ModalCloseButton />
</AlertInfo> <ModalBody as={Stack} spacing="6">
<Flex> <Text>
Make sure to check all the permissions so that the integration works
as expected:
</Text>
<Image
src="/images/google-spreadsheets-scopes.png"
alt="Google Spreadsheets checkboxes"
rounded="md"
/>
<AlertInfo>
Google does not provide more granular permissions than
&quot;read&quot; or &quot;write&quot; access. That&apos;s why it
states that Typebot can also delete your spreadsheets which it
won&apos;t.
</AlertInfo>
<Flex>
{workspace?.id && (
<Button <Button
as={Link} as={Link}
leftIcon={<GoogleLogo />} leftIcon={<GoogleLogo />}
@ -64,19 +79,18 @@ export const GoogleSheetConnectModal = ({
variant="outline" variant="outline"
href={getGoogleSheetsConsentScreenUrlQuery( href={getGoogleSheetsConsentScreenUrlQuery(
window.location.href, window.location.href,
workspace.id,
blockId, blockId,
workspace?.id,
typebotId typebotId
)} )}
mx="auto" mx="auto"
> >
Continue with Google Continue with Google
</Button> </Button>
</Flex> )}
</ModalBody> </Flex>
</ModalBody>
<ModalFooter /> <ModalFooter />
</ModalContent> </ModalContent>
</Modal>
) )
} }

View File

@ -5,44 +5,22 @@ export const GoogleSheetsLogo = (props: IconProps) => (
<title>Sheets-icon</title> <title>Sheets-icon</title>
<desc>Created with Sketch.</desc> <desc>Created with Sketch.</desc>
<defs> <defs>
<path <path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"></path>
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" <path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"></path>
id="path-1" <path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"></path>
></path>
<path
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
id="path-3"
></path>
<path
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
id="path-5"
></path>
<linearGradient <linearGradient
x1="50.0053945%" x1="50.0053945%"
y1="8.58610612%" y1="8.58610612%"
x2="50.0053945%" x2="50.0053945%"
y2="100.013939%" y2="100.013939%"
id="linearGradient-7"
> >
<stop stopColor="#263238" stopOpacity="0.2" offset="0%"></stop> <stop stopColor="#263238" stopOpacity="0.2" offset="0%"></stop>
<stop stopColor="#263238" stopOpacity="0.02" offset="100%"></stop> <stop stopColor="#263238" stopOpacity="0.02" offset="100%"></stop>
</linearGradient> </linearGradient>
<path <path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"></path>
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" <path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"></path>
id="path-8" <path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"></path>
></path> <path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"></path>
<path
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
id="path-10"
></path>
<path
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
id="path-12"
></path>
<path
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
id="path-14"
></path>
<radialGradient <radialGradient
cx="3.16804688%" cx="3.16804688%"
cy="2.71744318%" cy="2.71744318%"
@ -50,112 +28,101 @@ export const GoogleSheetsLogo = (props: IconProps) => (
fy="2.71744318%" fy="2.71744318%"
r="161.248516%" r="161.248516%"
gradientTransform="translate(0.031680,0.027174),scale(1.000000,0.727273),translate(-0.031680,-0.027174)" gradientTransform="translate(0.031680,0.027174),scale(1.000000,0.727273),translate(-0.031680,-0.027174)"
id="radialGradient-16"
> >
<stop stopColor="#FFFFFF" stopOpacity="0.1" offset="0%"></stop> <stop stopColor="#FFFFFF" stopOpacity="0.1" offset="0%"></stop>
<stop stopColor="#FFFFFF" stopOpacity="0" offset="100%"></stop> <stop stopColor="#FFFFFF" stopOpacity="0" offset="100%"></stop>
</radialGradient> </radialGradient>
</defs> </defs>
<g id="Page-1" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd"> <g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g <g transform="translate(-451.000000, -451.000000)">
id="Consumer-Apps-Sheets-Large-VD-R8-" <g transform="translate(0.000000, 63.000000)">
transform="translate(-451.000000, -451.000000)" <g transform="translate(277.000000, 299.000000)">
> <g transform="translate(174.833333, 89.958333)">
<g id="Hero" transform="translate(0.000000, 63.000000)"> <g>
<g id="Personal" transform="translate(277.000000, 299.000000)"> <g>
<g id="Sheets-icon" transform="translate(174.833333, 89.958333)"> <mask fill="white">
<g id="Group">
<g id="Clipped">
<mask id="mask-2" fill="white">
<use xlinkHref="#path-1"></use> <use xlinkHref="#path-1"></use>
</mask> </mask>
<g id="SVGID_1_"></g> <g></g>
<path <path
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L36.9791667,10.3541667 L29.5833333,0 Z" d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L36.9791667,10.3541667 L29.5833333,0 Z"
id="Path"
fill="#0F9D58" fill="#0F9D58"
fillRule="nonzero" fillRule="nonzero"
mask="url(#mask-2)" mask="url(#mask-2)"
></path> ></path>
</g> </g>
<g id="Clipped"> <g>
<mask id="mask-4" fill="white"> <mask fill="white">
<use xlinkHref="#path-3"></use> <use xlinkHref="#path-3"></use>
</mask> </mask>
<g id="SVGID_1_"></g> <g></g>
<path <path
d="M11.8333333,31.8020833 L11.8333333,53.25 L35.5,53.25 L35.5,31.8020833 L11.8333333,31.8020833 Z M22.1875,50.2916667 L14.7916667,50.2916667 L14.7916667,46.59375 L22.1875,46.59375 L22.1875,50.2916667 Z M22.1875,44.375 L14.7916667,44.375 L14.7916667,40.6770833 L22.1875,40.6770833 L22.1875,44.375 Z M22.1875,38.4583333 L14.7916667,38.4583333 L14.7916667,34.7604167 L22.1875,34.7604167 L22.1875,38.4583333 Z M32.5416667,50.2916667 L25.1458333,50.2916667 L25.1458333,46.59375 L32.5416667,46.59375 L32.5416667,50.2916667 Z M32.5416667,44.375 L25.1458333,44.375 L25.1458333,40.6770833 L32.5416667,40.6770833 L32.5416667,44.375 Z M32.5416667,38.4583333 L25.1458333,38.4583333 L25.1458333,34.7604167 L32.5416667,34.7604167 L32.5416667,38.4583333 Z" d="M11.8333333,31.8020833 L11.8333333,53.25 L35.5,53.25 L35.5,31.8020833 L11.8333333,31.8020833 Z M22.1875,50.2916667 L14.7916667,50.2916667 L14.7916667,46.59375 L22.1875,46.59375 L22.1875,50.2916667 Z M22.1875,44.375 L14.7916667,44.375 L14.7916667,40.6770833 L22.1875,40.6770833 L22.1875,44.375 Z M22.1875,38.4583333 L14.7916667,38.4583333 L14.7916667,34.7604167 L22.1875,34.7604167 L22.1875,38.4583333 Z M32.5416667,50.2916667 L25.1458333,50.2916667 L25.1458333,46.59375 L32.5416667,46.59375 L32.5416667,50.2916667 Z M32.5416667,44.375 L25.1458333,44.375 L25.1458333,40.6770833 L32.5416667,40.6770833 L32.5416667,44.375 Z M32.5416667,38.4583333 L25.1458333,38.4583333 L25.1458333,34.7604167 L32.5416667,34.7604167 L32.5416667,38.4583333 Z"
id="Shape"
fill="#F1F1F1" fill="#F1F1F1"
fillRule="nonzero" fillRule="nonzero"
mask="url(#mask-4)" mask="url(#mask-4)"
></path> ></path>
</g> </g>
<g id="Clipped"> <g>
<mask id="mask-6" fill="white"> <mask fill="white">
<use xlinkHref="#path-5"></use> <use xlinkHref="#path-5"></use>
</mask> </mask>
<g id="SVGID_1_"></g> <g></g>
<polygon <polygon
id="Path"
fill="url(#linearGradient-7)" fill="url(#linearGradient-7)"
fillRule="nonzero" fillRule="nonzero"
mask="url(#mask-6)" mask="url(#mask-6)"
points="30.8813021 16.4520313 47.3333333 32.9003646 47.3333333 17.75" points="30.8813021 16.4520313 47.3333333 32.9003646 47.3333333 17.75"
></polygon> ></polygon>
</g> </g>
<g id="Clipped"> <g>
<mask id="mask-9" fill="white"> <mask fill="white">
<use xlinkHref="#path-8"></use> <use xlinkHref="#path-8"></use>
</mask> </mask>
<g id="SVGID_1_"></g> <g></g>
<g id="Group" mask="url(#mask-9)"> <g mask="url(#mask-9)">
<g transform="translate(26.625000, -2.958333)"> <g transform="translate(26.625000, -2.958333)">
<path <path
d="M2.95833333,2.95833333 L2.95833333,16.2708333 C2.95833333,18.7225521 4.94411458,20.7083333 7.39583333,20.7083333 L20.7083333,20.7083333 L2.95833333,2.95833333 Z" d="M2.95833333,2.95833333 L2.95833333,16.2708333 C2.95833333,18.7225521 4.94411458,20.7083333 7.39583333,20.7083333 L20.7083333,20.7083333 L2.95833333,2.95833333 Z"
id="Path"
fill="#87CEAC" fill="#87CEAC"
fillRule="nonzero" fillRule="nonzero"
></path> ></path>
</g> </g>
</g> </g>
</g> </g>
<g id="Clipped"> <g>
<mask id="mask-11" fill="white"> <mask fill="white">
<use xlinkHref="#path-10"></use> <use xlinkHref="#path-10"></use>
</mask> </mask>
<g id="SVGID_1_"></g> <g></g>
<path <path
d="M4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,4.80729167 C0,2.36666667 1.996875,0.369791667 4.4375,0.369791667 L29.5833333,0.369791667 L29.5833333,0 L4.4375,0 Z" d="M4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,4.80729167 C0,2.36666667 1.996875,0.369791667 4.4375,0.369791667 L29.5833333,0.369791667 L29.5833333,0 L4.4375,0 Z"
id="Path"
fillOpacity="0.2" fillOpacity="0.2"
fill="#FFFFFF" fill="#FFFFFF"
fillRule="nonzero" fillRule="nonzero"
mask="url(#mask-11)" mask="url(#mask-11)"
></path> ></path>
</g> </g>
<g id="Clipped"> <g>
<mask id="mask-13" fill="white"> <mask fill="white">
<use xlinkHref="#path-12"></use> <use xlinkHref="#path-12"></use>
</mask> </mask>
<g id="SVGID_1_"></g> <g></g>
<path <path
d="M42.8958333,64.7135417 L4.4375,64.7135417 C1.996875,64.7135417 0,62.7166667 0,60.2760417 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,60.2760417 C47.3333333,62.7166667 45.3364583,64.7135417 42.8958333,64.7135417 Z" d="M42.8958333,64.7135417 L4.4375,64.7135417 C1.996875,64.7135417 0,62.7166667 0,60.2760417 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,60.2760417 C47.3333333,62.7166667 45.3364583,64.7135417 42.8958333,64.7135417 Z"
id="Path"
fillOpacity="0.2" fillOpacity="0.2"
fill="#263238" fill="#263238"
fillRule="nonzero" fillRule="nonzero"
mask="url(#mask-13)" mask="url(#mask-13)"
></path> ></path>
</g> </g>
<g id="Clipped"> <g>
<mask id="mask-15" fill="white"> <mask fill="white">
<use xlinkHref="#path-14"></use> <use xlinkHref="#path-14"></use>
</mask> </mask>
<g id="SVGID_1_"></g> <g></g>
<path <path
d="M34.0208333,17.75 C31.5691146,17.75 29.5833333,15.7642188 29.5833333,13.3125 L29.5833333,13.6822917 C29.5833333,16.1340104 31.5691146,18.1197917 34.0208333,18.1197917 L47.3333333,18.1197917 L47.3333333,17.75 L34.0208333,17.75 Z" d="M34.0208333,17.75 C31.5691146,17.75 29.5833333,15.7642188 29.5833333,13.3125 L29.5833333,13.6822917 C29.5833333,16.1340104 31.5691146,18.1197917 34.0208333,18.1197917 L47.3333333,18.1197917 L47.3333333,17.75 L34.0208333,17.75 Z"
id="Path"
fillOpacity="0.1" fillOpacity="0.1"
fill="#263238" fill="#263238"
fillRule="nonzero" fillRule="nonzero"
@ -165,7 +132,6 @@ export const GoogleSheetsLogo = (props: IconProps) => (
</g> </g>
<path <path
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
id="Path"
fill="url(#radialGradient-16)" fill="url(#radialGradient-16)"
fillRule="nonzero" fillRule="nonzero"
></path> ></path>

View File

@ -2,8 +2,8 @@ import { stringify } from 'qs'
export const getGoogleSheetsConsentScreenUrlQuery = ( export const getGoogleSheetsConsentScreenUrlQuery = (
redirectUrl: string, redirectUrl: string,
blockId: string, workspaceId: string,
workspaceId?: string, blockId?: string,
typebotId?: string typebotId?: string
) => { ) => {
const queryParams = stringify({ const queryParams = stringify({

View File

@ -6,79 +6,93 @@ import { SmtpCredentials } from '@typebot.io/schemas'
import React from 'react' import React from 'react'
type Props = { type Props = {
config: SmtpCredentials['data'] config: SmtpCredentials['data'] | undefined
onConfigChange: (config: SmtpCredentials['data']) => void onConfigChange: (config: SmtpCredentials['data']) => void
} }
export const SmtpConfigForm = ({ config, onConfigChange }: Props) => { export const SmtpConfigForm = ({ config, onConfigChange }: Props) => {
const handleFromEmailChange = (email: string) => const handleFromEmailChange = (email: string) =>
onConfigChange({ ...config, from: { ...config.from, email } }) config && onConfigChange({ ...config, from: { ...config.from, email } })
const handleFromNameChange = (name: string) => const handleFromNameChange = (name: string) =>
onConfigChange({ ...config, from: { ...config.from, name } }) config && onConfigChange({ ...config, from: { ...config.from, name } })
const handleHostChange = (host: string) => onConfigChange({ ...config, host })
const handleHostChange = (host: string) =>
config && onConfigChange({ ...config, host })
const handleUsernameChange = (username: string) => const handleUsernameChange = (username: string) =>
onConfigChange({ ...config, username }) config && onConfigChange({ ...config, username })
const handlePasswordChange = (password: string) => const handlePasswordChange = (password: string) =>
onConfigChange({ ...config, password }) config && onConfigChange({ ...config, password })
const handleTlsCheck = (isTlsEnabled: boolean) => const handleTlsCheck = (isTlsEnabled: boolean) =>
onConfigChange({ ...config, isTlsEnabled }) config && onConfigChange({ ...config, isTlsEnabled })
const handlePortNumberChange = (port?: number) => const handlePortNumberChange = (port?: number) =>
isDefined(port) && onConfigChange({ ...config, port }) config && isDefined(port) && onConfigChange({ ...config, port })
return ( return (
<Stack as="form" spacing={4}> <Stack spacing={4}>
<TextInput <TextInput
isRequired isRequired
label="From email" label="From email"
defaultValue={config.from.email ?? ''} defaultValue={config?.from.email}
onChange={handleFromEmailChange} onChange={handleFromEmailChange}
placeholder="notifications@provider.com" placeholder="notifications@provider.com"
withVariableButton={false} withVariableButton={false}
isDisabled={!config}
/> />
<TextInput <TextInput
label="From name" label="From name"
defaultValue={config.from.name ?? ''} defaultValue={config?.from.name}
onChange={handleFromNameChange} onChange={handleFromNameChange}
placeholder="John Smith" placeholder="John Smith"
withVariableButton={false} withVariableButton={false}
isDisabled={!config}
/> />
<TextInput <TextInput
isRequired isRequired
label="Host" label="Host"
defaultValue={config.host ?? ''} defaultValue={config?.host}
onChange={handleHostChange} onChange={handleHostChange}
placeholder="mail.provider.com" placeholder="mail.provider.com"
withVariableButton={false} withVariableButton={false}
isDisabled={!config}
/> />
<TextInput <TextInput
isRequired isRequired
label="Username" label="Username"
type="email" type="email"
defaultValue={config.username ?? ''} defaultValue={config?.username}
onChange={handleUsernameChange} onChange={handleUsernameChange}
withVariableButton={false} withVariableButton={false}
isDisabled={!config}
/> />
<TextInput <TextInput
isRequired isRequired
label="Password" label="Password"
type="password" type="password"
defaultValue={config.password ?? ''} defaultValue={config?.password}
onChange={handlePasswordChange} onChange={handlePasswordChange}
withVariableButton={false} withVariableButton={false}
isDisabled={!config}
/> />
<SwitchWithLabel <SwitchWithLabel
label="Secure?" label="Secure?"
initialValue={config.isTlsEnabled ?? false} initialValue={config?.isTlsEnabled}
onCheckChange={handleTlsCheck} 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." 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."
isDisabled={!config}
/> />
<NumberInput <NumberInput
isRequired isRequired
label="Port number:" label="Port number:"
placeholder="25" placeholder="25"
defaultValue={config.port} defaultValue={config?.port}
onValueChange={handlePortNumberChange} onValueChange={handlePortNumberChange}
withVariableButton={false} withVariableButton={false}
isDisabled={!config}
/> />
</Stack> </Stack>
) )

View File

@ -26,9 +26,25 @@ type Props = {
export const SmtpConfigModal = ({ export const SmtpConfigModal = ({
isOpen, isOpen,
onNewCredentials,
onClose, onClose,
onNewCredentials,
}: Props) => { }: Props) => {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<SmtpCreateModalContent
onNewCredentials={(id) => {
onNewCredentials(id)
onClose()
}}
/>
</Modal>
)
}
export const SmtpCreateModalContent = ({
onNewCredentials,
}: Pick<Props, 'onNewCredentials'>) => {
const { user } = useUser() const { user } = useUser()
const { workspace } = useWorkspace() const { workspace } = useWorkspace()
const [isCreating, setIsCreating] = useState(false) const [isCreating, setIsCreating] = useState(false)
@ -53,11 +69,11 @@ export const SmtpConfigModal = ({
onSuccess: (data) => { onSuccess: (data) => {
refetchCredentials() refetchCredentials()
onNewCredentials(data.credentialsId) onNewCredentials(data.credentialsId)
onClose()
}, },
}) })
const handleCreateClick = async () => { const handleCreateClick = async (e: React.FormEvent) => {
e.preventDefault()
if (!user?.email || !workspace?.id) return if (!user?.email || !workspace?.id) return
setIsCreating(true) setIsCreating(true)
const { error: testSmtpError } = await testSmtpConfig( const { error: testSmtpError } = await testSmtpConfig(
@ -82,19 +98,18 @@ export const SmtpConfigModal = ({
}) })
} }
return ( return (
<Modal isOpen={isOpen} onClose={onClose}> <ModalContent>
<ModalOverlay /> <ModalHeader>Create SMTP config</ModalHeader>
<ModalContent> <ModalCloseButton />
<ModalHeader>Create SMTP config</ModalHeader> <form onSubmit={handleCreateClick}>
<ModalCloseButton />
<ModalBody> <ModalBody>
<SmtpConfigForm config={smtpConfig} onConfigChange={setSmtpConfig} /> <SmtpConfigForm config={smtpConfig} onConfigChange={setSmtpConfig} />
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button <Button
type="submit"
colorScheme="blue" colorScheme="blue"
onClick={handleCreateClick}
isDisabled={ isDisabled={
isNotDefined(smtpConfig.from.email) || isNotDefined(smtpConfig.from.email) ||
isNotDefined(smtpConfig.host) || isNotDefined(smtpConfig.host) ||
@ -107,7 +122,7 @@ export const SmtpConfigModal = ({
Create Create
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </form>
</Modal> </ModalContent>
) )
} }

View File

@ -0,0 +1,113 @@
import {
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
ModalFooter,
Button,
} from '@chakra-ui/react'
import { useUser } from '@/features/account/hooks/useUser'
import React, { useEffect, useState } from 'react'
import { isNotDefined } from '@typebot.io/lib'
import { SmtpConfigForm } from './SmtpConfigForm'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { useToast } from '@/hooks/useToast'
import { testSmtpConfig } from '../queries/testSmtpConfigQuery'
import { trpc } from '@/lib/trpc'
import { SmtpCredentials } from '@typebot.io/schemas/features/blocks/integrations/sendEmail/schema'
type Props = {
credentialsId: string
onUpdate: () => void
}
export const SmtpUpdateModalContent = ({ credentialsId, onUpdate }: Props) => {
const { user } = useUser()
const { workspace } = useWorkspace()
const [isCreating, setIsCreating] = useState(false)
const { showToast } = useToast()
const [smtpConfig, setSmtpConfig] = useState<SmtpCredentials['data']>()
const { data: existingCredentials } =
trpc.credentials.getCredentials.useQuery(
{
workspaceId: workspace!.id,
credentialsId: credentialsId,
},
{
enabled: !!workspace?.id,
}
)
useEffect(() => {
if (!existingCredentials || smtpConfig) return
setSmtpConfig(existingCredentials.data)
}, [existingCredentials, smtpConfig])
const { mutate } = trpc.credentials.updateCredentials.useMutation({
onSettled: () => setIsCreating(false),
onError: (err) => {
showToast({
description: err.message,
status: 'error',
})
},
onSuccess: () => {
onUpdate()
},
})
const handleUpdateClick = async (e: React.FormEvent) => {
e.preventDefault()
if (!user?.email || !workspace?.id || !smtpConfig) return
setIsCreating(true)
const { error: testSmtpError } = await testSmtpConfig(
smtpConfig,
user.email
)
if (testSmtpError) {
setIsCreating(false)
return showToast({
title: 'Invalid configuration',
description: "We couldn't send the test email with your configuration",
})
}
mutate({
credentialsId,
credentials: {
data: smtpConfig,
name: smtpConfig.from.email as string,
type: 'smtp',
workspaceId: workspace.id,
},
})
}
return (
<ModalContent>
<ModalHeader>Update SMTP config</ModalHeader>
<ModalCloseButton />
<form onSubmit={handleUpdateClick}>
<ModalBody>
<SmtpConfigForm config={smtpConfig} onConfigChange={setSmtpConfig} />
</ModalBody>
<ModalFooter>
<Button
type="submit"
colorScheme="blue"
isDisabled={
isNotDefined(smtpConfig?.from.email) ||
isNotDefined(smtpConfig?.host) ||
isNotDefined(smtpConfig?.username) ||
isNotDefined(smtpConfig?.password) ||
isNotDefined(smtpConfig?.port)
}
isLoading={isCreating}
>
Update
</Button>
</ModalFooter>
</form>
</ModalContent>
)
}

View File

@ -1,98 +1,77 @@
import test, { expect, Page } from '@playwright/test' import test, { expect, Page } from '@playwright/test'
import { import { importTypebotInDatabase } from '@typebot.io/playwright/databaseActions'
createWebhook,
importTypebotInDatabase,
} from '@typebot.io/playwright/databaseActions'
import { createId } from '@paralleldrive/cuid2' import { createId } from '@paralleldrive/cuid2'
import { getTestAsset } from '@/test/utils/playwright' import { getTestAsset } from '@/test/utils/playwright'
import { apiToken } from '@typebot.io/playwright/databaseSetup' import { apiToken } from '@typebot.io/playwright/databaseSetup'
import { env } from '@typebot.io/env' import { env } from '@typebot.io/env'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants' import { omit } from '@typebot.io/lib/utils'
test.describe('Builder', () => { test.describe.configure({ mode: 'parallel' })
test('easy configuration should work', async ({ page }) => {
const typebotId = createId()
await importTypebotInDatabase(
getTestAsset('typebots/integrations/easyConfigWebhook.json'),
{
id: typebotId,
}
)
await createWebhook(typebotId, { method: HttpMethod.POST })
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Configure...')
await page.fill(
'input[placeholder="Paste URL..."]',
`${env.NEXTAUTH_URL}/api/mock/webhook-easy-config`
)
await page.click('text=Test the request')
await expect(page.locator('div[role="textbox"] >> nth=-1')).toContainText(
`"Group #1": "answer value", "Group #2": "20", "Group #2 (1)": "Yes"`,
{ timeout: 10000 }
)
})
test('its configuration should work', async ({ page }) => { test('editor configuration should work', async ({ page }) => {
const typebotId = createId() const typebotId = createId()
await importTypebotInDatabase( await importTypebotInDatabase(
getTestAsset('typebots/integrations/webhook.json'), getTestAsset('typebots/integrations/webhook.json'),
{ {
id: typebotId, id: typebotId,
} }
) )
await createWebhook(typebotId)
await page.goto(`/typebots/${typebotId}/edit`) await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Configure...') await page.click('text=Configure...')
await page.fill( await page.fill(
'input[placeholder="Paste URL..."]', 'input[placeholder="Paste URL..."]',
`${env.NEXTAUTH_URL}/api/mock/webhook` `${env.NEXTAUTH_URL}/api/mock/webhook-easy-config`
) )
await page.click('text=Advanced configuration') await page.click('text=Test the request')
await page.getByRole('button', { name: 'GET' }).click() await expect(page.locator('div[role="textbox"] >> nth=-1')).toContainText(
await page.click('text=POST') `"Group #1": "Go", "secret 1": "content"`,
{ timeout: 10000 }
)
await page.click('text=Query params') await page.fill(
await page.click('text=Add a param') 'input[placeholder="Paste URL..."]',
await page.fill('input[placeholder="e.g. email"]', 'firstParam') `${env.NEXTAUTH_URL}/api/mock/webhook`
await page.fill('input[placeholder="e.g. {{Email}}"]', '{{secret 1}}') )
await page.click('text=Advanced configuration')
await page.click('text=Add a param') await page.click('text=Query params')
await page.fill('input[placeholder="e.g. email"] >> nth=1', 'secondParam') await page.click('text=Add a param')
await page.fill( await page.fill('input[placeholder="e.g. email"]', 'firstParam')
'input[placeholder="e.g. {{Email}}"] >> nth=1', await page.fill('input[placeholder="e.g. {{Email}}"]', '{{secret 1}}')
'{{secret 2}}'
)
await page.click('text=Headers') await page.click('text=Add a param')
await page.waitForTimeout(200) await page.fill('input[placeholder="e.g. email"] >> nth=1', 'secondParam')
await page.getByRole('button', { name: 'Add a value' }).click() await page.fill(
await page.fill('input[placeholder="e.g. Content-Type"]', 'Custom-Typebot') 'input[placeholder="e.g. {{Email}}"] >> nth=1',
await page.fill( '{{secret 2}}'
'input[placeholder="e.g. application/json"]', )
'{{secret 3}}'
)
await page.click('text=Body') await page.click('text=Headers')
await page.click('text=Custom body') await page.waitForTimeout(200)
await page.fill('div[role="textbox"]', '{ "customField": "{{secret 4}}" }') 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"]', '{{secret 3}}')
await page.click('text=Variable values for test') await page.click('text=Body')
await addTestVariable(page, 'secret 1', 'secret1') await page.click('text=Custom body')
await addTestVariable(page, 'secret 2', 'secret2') await page.fill('div[role="textbox"]', '{ "customField": "{{secret 4}}" }')
await addTestVariable(page, 'secret 3', 'secret3')
await addTestVariable(page, 'secret 4', 'secret4')
await page.click('text=Test the request') await page.click('text=Variable values for test')
await expect(page.locator('div[role="textbox"] >> nth=-1')).toContainText( await addTestVariable(page, 'secret 1', 'secret1')
'"statusCode": 200' await addTestVariable(page, 'secret 2', 'secret2')
) await addTestVariable(page, 'secret 3', 'secret3')
await addTestVariable(page, 'secret 4', 'secret4')
await page.click('text=Save in variables') await page.click('text=Test the request')
await page.click('text=Add an entry >> nth=-1') await expect(page.locator('div[role="textbox"] >> nth=-1')).toContainText(
await page.click('input[placeholder="Select the data"]') '"statusCode": 200'
await page.click('text=data.flatMap(item => item.name)') )
})
await page.click('text=Save in variables')
await page.click('text=Add an entry >> nth=-1')
await page.click('input[placeholder="Select the data"]')
await page.click('text=data.flatMap(item => item.name)')
}) })
const addTestVariable = async (page: Page, name: string, value: string) => { const addTestVariable = async (page: Page, name: string, value: string) => {
@ -102,83 +81,70 @@ const addTestVariable = async (page: Page, name: string, value: string) => {
await page.fill('input >> nth=-1', value) await page.fill('input >> nth=-1', value)
} }
test.describe('API', () => { test('Webhook API endpoints should work', async ({ request }) => {
const typebotId = 'webhook-flow' const typebotId = createId()
await importTypebotInDatabase(getTestAsset('typebots/api.json'), {
id: typebotId,
})
test.beforeAll(async () => { // GET webhook blocks
try { const getResponse = await request.get(
await importTypebotInDatabase(getTestAsset('typebots/api.json'), { `/api/v1/typebots/${typebotId}/webhookBlocks`,
id: typebotId, {
}) headers: { Authorization: `Bearer ${apiToken}` },
await createWebhook(typebotId)
} catch (err) {
console.log(err)
} }
)
const { webhookBlocks } = await getResponse.json()
expect(webhookBlocks).toHaveLength(1)
expect(webhookBlocks[0]).toEqual({
id: 'webhookBlock',
label: 'Webhook > webhookBlock',
type: 'Webhook',
}) })
test('can get webhook blocks', async ({ request }) => { // Subscribe webhook
const response = await request.get( const url = 'https://test.com'
`/api/v1/typebots/${typebotId}/webhookBlocks`, const subscribeResponse = await request.post(
{ `/api/v1/typebots/${typebotId}/webhookBlocks/webhookBlock/subscribe`,
headers: { Authorization: `Bearer ${apiToken}` }, {
} headers: {
) Authorization: `Bearer ${apiToken}`,
const { webhookBlocks } = await response.json() },
expect(webhookBlocks).toHaveLength(1) data: { url },
expect(webhookBlocks[0]).toEqual({ }
id: 'webhookBlock', )
label: 'Webhook > webhookBlock', expect(await subscribeResponse.json()).toEqual({
type: 'Webhook', id: 'webhookBlock',
}) url,
}) })
test('can subscribe webhook', async ({ request }) => { // Unsubscribe webhook
const url = 'https://test.com' const unsubResponse = await request.post(
const response = await request.post( `/api/v1/typebots/${typebotId}/webhookBlocks/webhookBlock/unsubscribe`,
`/api/v1/typebots/${typebotId}/webhookBlocks/webhookBlock/subscribe`, {
{ headers: { Authorization: `Bearer ${apiToken}` },
headers: { }
Authorization: `Bearer ${apiToken}`, )
}, expect(await unsubResponse.json()).toEqual({
data: { url }, id: 'webhookBlock',
} url: null,
)
const body = await response.json()
expect(body).toEqual({
id: 'webhookBlock',
url,
})
}) })
test('can unsubscribe webhook', async ({ request }) => { // Get sample result
const response = await request.post( const sampleResponse = await request.get(
`/api/v1/typebots/${typebotId}/webhookBlocks/webhookBlock/unsubscribe`, `/api/v1/typebots/${typebotId}/webhookBlocks/webhookBlock/getResultExample`,
{ {
headers: { Authorization: `Bearer ${apiToken}` }, headers: { Authorization: `Bearer ${apiToken}` },
} }
) )
const body = await response.json() const sample = await sampleResponse.json()
expect(body).toEqual({
id: 'webhookBlock',
url: null,
})
})
test('can get a sample result', async ({ request }) => { expect(omit(sample.resultExample, 'submittedAt')).toMatchObject({
const response = await request.get( message: 'This is a sample result, it has been generated ⬇️',
`/api/v1/typebots/${typebotId}/webhookBlocks/webhookBlock/getResultExample`, Welcome: 'Hi!',
{ Email: 'user@email.com',
headers: { Authorization: `Bearer ${apiToken}` }, Name: 'answer value',
} Services: 'Website dev, Content Marketing, Social Media, UI / UX Design',
) 'Additional information': 'answer value',
const data = await response.json()
expect(data.resultExample).toMatchObject({
message: 'This is a sample result, it has been generated ⬇️',
Welcome: 'Hi!',
Email: 'user@email.com',
Name: 'answer value',
Services: 'Website dev, Content Marketing, Social Media, UI / UX Design',
'Additional information': 'answer value',
})
}) })
}) })

View File

@ -1,125 +0,0 @@
import { TextInput } from '@/components/inputs/TextInput'
import { TextLink } from '@/components/TextLink'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { useToast } from '@/hooks/useToast'
import { trpc } from '@/lib/trpc'
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
Stack,
ModalFooter,
Button,
} from '@chakra-ui/react'
import React, { useState } from 'react'
const zemanticAIDashboardPage = 'https://zemantic.ai/dashboard/settings'
type Props = {
isOpen: boolean
onClose: () => void
onNewCredentials: (id: string) => void
}
export const ZemanticAiCredentialsModal = ({
isOpen,
onClose,
onNewCredentials,
}: Props) => {
const { workspace } = useWorkspace()
const { showToast } = useToast()
const [apiKey, setApiKey] = useState('')
const [name, setName] = useState('')
const [isCreating, setIsCreating] = useState(false)
const {
credentials: {
listCredentials: { refetch: refetchCredentials },
},
} = trpc.useContext()
const { mutate } = trpc.credentials.createCredentials.useMutation({
onMutate: () => setIsCreating(true),
onSettled: () => setIsCreating(false),
onError: (err) => {
showToast({
description: err.message,
status: 'error',
})
},
onSuccess: (data) => {
refetchCredentials()
onNewCredentials(data.credentialsId)
onClose()
},
})
const createZemanticAiCredentials = async (e: React.FormEvent) => {
e.preventDefault()
if (!workspace) return
mutate({
credentials: {
type: 'zemanticAi',
workspaceId: workspace.id,
name,
data: {
apiKey,
},
},
})
}
return (
<Modal isOpen={isOpen} onClose={onClose} size="lg">
<ModalOverlay />
<ModalContent>
<ModalHeader>Add Zemantic AI account</ModalHeader>
<ModalCloseButton />
<form onSubmit={createZemanticAiCredentials}>
<ModalBody as={Stack} spacing="6">
<TextInput
isRequired
label="Name"
onChange={setName}
placeholder="My account"
withVariableButton={false}
debounceTimeout={0}
/>
<TextInput
isRequired
type="password"
label="API key"
helperText={
<>
You can generate an API key{' '}
<TextLink href={zemanticAIDashboardPage} isExternal>
here
</TextLink>
.
</>
}
onChange={setApiKey}
placeholder="ze..."
withVariableButton={false}
debounceTimeout={0}
/>
</ModalBody>
<ModalFooter>
<Button
type="submit"
isLoading={isCreating}
isDisabled={apiKey === '' || name === ''}
colorScheme="blue"
>
Create
</Button>
</ModalFooter>
</form>
</ModalContent>
</Modal>
)
}

View File

@ -1,6 +1,4 @@
import { TextInput, Textarea, NumberInput } from '@/components/inputs' import { TextInput, Textarea, NumberInput } from '@/components/inputs'
import { CredentialsDropdown } from '@/features/credentials/components/CredentialsDropdown'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { import {
Accordion, Accordion,
AccordionButton, AccordionButton,
@ -9,15 +7,12 @@ import {
AccordionPanel, AccordionPanel,
Stack, Stack,
Text, Text,
useDisclosure,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { isEmpty } from '@typebot.io/lib' import { isEmpty } from '@typebot.io/lib'
import { ZemanticAiBlock } from '@typebot.io/schemas' import { ZemanticAiBlock } from '@typebot.io/schemas'
import { ZemanticAiCredentialsModal } from './ZemanticAiCredentialsModal'
import { ProjectsDropdown } from './ProjectsDropdown' import { ProjectsDropdown } from './ProjectsDropdown'
import { SearchResponseItem } from './SearchResponseItem' import { SearchResponseItem } from './SearchResponseItem'
import { TableList } from '@/components/TableList' import { TableList } from '@/components/TableList'
import { createId } from '@paralleldrive/cuid2'
type Props = { type Props = {
block: ZemanticAiBlock block: ZemanticAiBlock
@ -28,22 +23,6 @@ export const ZemanticAiSettings = ({
block: { id: blockId, options }, block: { id: blockId, options },
onOptionsChange, onOptionsChange,
}: Props) => { }: Props) => {
const { workspace } = useWorkspace()
const { isOpen, onOpen, onClose } = useDisclosure()
const updateCredentialsId = (credentialsId: string | undefined) => {
onOptionsChange({
...options,
credentialsId,
responseMapping: [
{
id: createId(),
valueToExtract: 'Summary',
},
],
})
}
const updateProjectId = (projectId: string | undefined) => { const updateProjectId = (projectId: string | undefined) => {
onOptionsChange({ onOptionsChange({
...options, ...options,
@ -92,23 +71,6 @@ export const ZemanticAiSettings = ({
return ( return (
<Stack spacing={4}> <Stack spacing={4}>
{workspace && (
<>
<CredentialsDropdown
type="zemanticAi"
workspaceId={workspace.id}
currentCredentialsId={options?.credentialsId}
onCredentialsSelect={updateCredentialsId}
onCreateNewClick={onOpen}
credentialsName="Zemantic AI account"
/>
<ZemanticAiCredentialsModal
isOpen={isOpen}
onClose={onClose}
onNewCredentials={updateCredentialsId}
/>
</>
)}
{options?.credentialsId && ( {options?.credentialsId && (
<> <>
<ProjectsDropdown <ProjectsDropdown

View File

@ -1,7 +1,6 @@
import prisma from '@typebot.io/lib/prisma' import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc' import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { openAICredentialsSchema } from '@typebot.io/schemas/features/blocks/integrations/openai'
import { smtpCredentialsSchema } from '@typebot.io/schemas/features/blocks/integrations/sendEmail' import { smtpCredentialsSchema } from '@typebot.io/schemas/features/blocks/integrations/sendEmail'
import { encrypt } from '@typebot.io/lib/api/encryption/encrypt' import { encrypt } from '@typebot.io/lib/api/encryption/encrypt'
import { z } from 'zod' import { z } from 'zod'
@ -10,11 +9,11 @@ import {
Credentials, Credentials,
googleSheetsCredentialsSchema, googleSheetsCredentialsSchema,
stripeCredentialsSchema, stripeCredentialsSchema,
zemanticAiCredentialsSchema,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { isDefined } from '@typebot.io/lib/utils' import { isDefined } from '@typebot.io/lib/utils'
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden' import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden'
import { trackEvents } from '@typebot.io/telemetry/trackEvents' import { trackEvents } from '@typebot.io/telemetry/trackEvents'
import { forgedCredentialsSchemas } from '@typebot.io/forge-repository/credentials'
const inputShape = { const inputShape = {
data: true, data: true,
@ -40,9 +39,10 @@ export const createCredentials = authenticatedProcedure
stripeCredentialsSchema.pick(inputShape), stripeCredentialsSchema.pick(inputShape),
smtpCredentialsSchema.pick(inputShape), smtpCredentialsSchema.pick(inputShape),
googleSheetsCredentialsSchema.pick(inputShape), googleSheetsCredentialsSchema.pick(inputShape),
openAICredentialsSchema.pick(inputShape),
whatsAppCredentialsSchema.pick(inputShape), whatsAppCredentialsSchema.pick(inputShape),
zemanticAiCredentialsSchema.pick(inputShape), ...Object.values(forgedCredentialsSchemas).map((i) =>
i.pick(inputShape)
),
]) ])
.and(z.object({ id: z.string().cuid2().optional() })), .and(z.object({ id: z.string().cuid2().optional() })),
}) })
@ -53,7 +53,12 @@ export const createCredentials = authenticatedProcedure
}) })
) )
.mutation(async ({ input: { credentials }, ctx: { user } }) => { .mutation(async ({ input: { credentials }, ctx: { user } }) => {
if (await isNotAvailable(credentials.name, credentials.type)) if (
await isNotAvailable(
credentials.name,
credentials.type as Credentials['type']
)
)
throw new TRPCError({ throw new TRPCError({
code: 'CONFLICT', code: 'CONFLICT',
message: 'Credentials already exist.', message: 'Credentials already exist.',
@ -62,7 +67,7 @@ export const createCredentials = authenticatedProcedure
where: { where: {
id: credentials.workspaceId, id: credentials.workspaceId,
}, },
select: { id: true, members: true }, select: { id: true, members: { select: { userId: true, role: true } } },
}) })
if (!workspace || isWriteWorkspaceForbidden(workspace, user)) if (!workspace || isWriteWorkspaceForbidden(workspace, user))
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' }) throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })

View File

@ -30,11 +30,8 @@ export const deleteCredentials = authenticatedProcedure
const workspace = await prisma.workspace.findFirst({ const workspace = await prisma.workspace.findFirst({
where: { where: {
id: workspaceId, id: workspaceId,
members: {
some: { userId: user.id, role: { in: ['ADMIN', 'MEMBER'] } },
},
}, },
select: { id: true, members: true }, select: { id: true, members: { select: { userId: true, role: true } } },
}) })
if (!workspace || isWriteWorkspaceForbidden(workspace, user)) if (!workspace || isWriteWorkspaceForbidden(workspace, user))
throw new TRPCError({ throw new TRPCError({

View File

@ -0,0 +1,66 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden'
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
export const getCredentials = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/v1/credentials/{credentialsId}',
protect: true,
summary: 'Get credentials data',
tags: ['Credentials'],
},
})
.input(
z.object({
workspaceId: z.string(),
credentialsId: z.string(),
})
)
.output(
z.object({
name: z.string(),
data: z.any(),
})
)
.query(async ({ input: { workspaceId, credentialsId }, ctx: { user } }) => {
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
},
select: {
id: true,
members: true,
},
})
if (!workspace || isWriteWorkspaceForbidden(workspace, user))
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
const credentials = await prisma.credentials.findFirst({
where: {
id: credentialsId,
},
select: {
data: true,
iv: true,
name: true,
},
})
if (!credentials)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Credentials not found',
})
const credentialsData = await decrypt(credentials.data, credentials.iv)
return {
name: credentials.name,
data: credentialsData,
}
})

View File

@ -1,16 +1,18 @@
import prisma from '@typebot.io/lib/prisma' import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc' import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { openAICredentialsSchema } from '@typebot.io/schemas/features/blocks/integrations/openai'
import { smtpCredentialsSchema } from '@typebot.io/schemas/features/blocks/integrations/sendEmail'
import { z } from 'zod' import { z } from 'zod'
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden' import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
import { whatsAppCredentialsSchema } from '@typebot.io/schemas/features/whatsapp' import { credentialsTypeSchema } from '@typebot.io/schemas'
import { zemanticAiCredentialsSchema } from '@typebot.io/schemas/features/blocks/integrations/zemanticAi' import { isDefined } from '@udecode/plate-common'
import {
googleSheetsCredentialsSchema, const outputCredentialsSchema = z.array(
stripeCredentialsSchema, z.object({
} from '@typebot.io/schemas' id: z.string(),
type: credentialsTypeSchema,
name: z.string(),
})
)
export const listCredentials = authenticatedProcedure export const listCredentials = authenticatedProcedure
.meta({ .meta({
@ -25,17 +27,12 @@ export const listCredentials = authenticatedProcedure
.input( .input(
z.object({ z.object({
workspaceId: z.string(), workspaceId: z.string(),
type: stripeCredentialsSchema.shape.type type: credentialsTypeSchema.optional(),
.or(smtpCredentialsSchema.shape.type)
.or(googleSheetsCredentialsSchema.shape.type)
.or(openAICredentialsSchema.shape.type)
.or(whatsAppCredentialsSchema.shape.type)
.or(zemanticAiCredentialsSchema.shape.type),
}) })
) )
.output( .output(
z.object({ z.object({
credentials: z.array(z.object({ id: z.string(), name: z.string() })), credentials: outputCredentialsSchema,
}) })
) )
.query(async ({ input: { workspaceId, type }, ctx: { user } }) => { .query(async ({ input: { workspaceId, type }, ctx: { user } }) => {
@ -52,6 +49,7 @@ export const listCredentials = authenticatedProcedure
}, },
select: { select: {
id: true, id: true,
type: true,
name: true, name: true,
}, },
}, },
@ -60,5 +58,11 @@ export const listCredentials = authenticatedProcedure
if (!workspace || isReadWorkspaceFobidden(workspace, user)) if (!workspace || isReadWorkspaceFobidden(workspace, user))
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' }) throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
return { credentials: workspace.credentials } return {
credentials: outputCredentialsSchema.parse(
isDefined(type)
? workspace.credentials
: workspace.credentials.sort((a, b) => a.type.localeCompare(b.type))
),
}
}) })

View File

@ -2,9 +2,13 @@ import { router } from '@/helpers/server/trpc'
import { createCredentials } from './createCredentials' import { createCredentials } from './createCredentials'
import { deleteCredentials } from './deleteCredentials' import { deleteCredentials } from './deleteCredentials'
import { listCredentials } from './listCredentials' import { listCredentials } from './listCredentials'
import { updateCredentials } from './updateCredentials'
import { getCredentials } from './getCredentials'
export const credentialsRouter = router({ export const credentialsRouter = router({
createCredentials, createCredentials,
listCredentials, listCredentials,
getCredentials,
deleteCredentials, deleteCredentials,
updateCredentials,
}) })

View File

@ -0,0 +1,81 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { smtpCredentialsSchema } from '@typebot.io/schemas/features/blocks/integrations/sendEmail'
import { encrypt } from '@typebot.io/lib/api/encryption/encrypt'
import { z } from 'zod'
import { whatsAppCredentialsSchema } from '@typebot.io/schemas/features/whatsapp'
import {
googleSheetsCredentialsSchema,
stripeCredentialsSchema,
} from '@typebot.io/schemas'
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden'
import { forgedCredentialsSchemas } from '../../../../../../packages/forge/repository/credentials'
const inputShape = {
name: true,
data: true,
type: true,
workspaceId: true,
} as const
export const updateCredentials = authenticatedProcedure
.meta({
openapi: {
method: 'PATCH',
path: '/v1/credentials/{credentialsId}',
protect: true,
summary: 'Create credentials',
tags: ['Credentials'],
},
})
.input(
z.object({
credentialsId: z.string(),
credentials: z.discriminatedUnion('type', [
stripeCredentialsSchema.pick(inputShape),
smtpCredentialsSchema.pick(inputShape),
googleSheetsCredentialsSchema.pick(inputShape),
whatsAppCredentialsSchema.pick(inputShape),
...Object.values(forgedCredentialsSchemas).map((i) =>
i.pick(inputShape)
),
]),
})
)
.output(
z.object({
credentialsId: z.string(),
})
)
.mutation(
async ({ input: { credentialsId, credentials }, ctx: { user } }) => {
const workspace = await prisma.workspace.findFirst({
where: {
id: credentials.workspaceId,
},
select: {
id: true,
members: true,
},
})
if (!workspace || isWriteWorkspaceForbidden(workspace, user))
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Workspace not found',
})
const { encryptedData, iv } = await encrypt(credentials.data)
const createdCredentials = await prisma.credentials.update({
where: {
id: credentialsId,
},
data: {
name: credentials.name,
data: encryptedData,
iv,
},
})
return { credentialsId: createdCredentials.id }
}
)

View File

@ -0,0 +1,82 @@
import { StripeCreateModalContent } from '@/features/blocks/inputs/payment/components/StripeConfigModal'
import { GoogleSheetConnectModalContent } from '@/features/blocks/integrations/googleSheets/components/GoogleSheetsConnectModal'
import { SmtpCreateModalContent } from '@/features/blocks/integrations/sendEmail/components/SmtpConfigModal'
import { CreateForgedCredentialsModalContent } from '@/features/forge/components/credentials/CreateForgedCredentialsModal'
import { WhatsAppCreateModalContent } from '@/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppCredentialsModal'
import { Modal, ModalOverlay } from '@chakra-ui/react'
import { forgedBlocks } from '@typebot.io/forge-repository/definitions'
import { Credentials } from '@typebot.io/schemas/features/credentials'
export const CredentialsCreateModal = ({
creatingType,
onSubmit,
onClose,
}: {
creatingType?: Credentials['type']
onClose: () => void
onSubmit: () => void
}) => {
return (
<Modal
isOpen={creatingType !== undefined}
onClose={onClose}
size={parseModalSize(creatingType)}
>
<ModalOverlay />
{creatingType && (
<CredentialsCreateModalContent
type={creatingType}
onSubmit={onSubmit}
onClose={onClose}
/>
)}
</Modal>
)
}
const CredentialsCreateModalContent = ({
type,
onSubmit,
onClose,
}: {
type: Credentials['type']
onClose: () => void
onSubmit: () => void
}) => {
switch (type) {
case 'google sheets':
return <GoogleSheetConnectModalContent />
case 'smtp':
return <SmtpCreateModalContent onNewCredentials={onSubmit} />
case 'stripe':
return (
<StripeCreateModalContent
onNewCredentials={onSubmit}
onClose={onClose}
/>
)
case 'whatsApp':
return (
<WhatsAppCreateModalContent
onNewCredentials={onSubmit}
onClose={onClose}
/>
)
default:
return (
<CreateForgedCredentialsModalContent
blockDef={forgedBlocks[type]}
onNewCredentials={onSubmit}
/>
)
}
}
const parseModalSize = (type?: Credentials['type']) => {
switch (type) {
case 'whatsApp':
return '3xl'
default:
return 'lg'
}
}

View File

@ -0,0 +1,336 @@
import { trpc } from '@/lib/trpc'
import {
Heading,
HStack,
Stack,
IconButton,
Divider,
Button,
Menu,
MenuButton,
MenuList,
MenuItem,
IconProps,
TextProps,
Popover,
PopoverTrigger,
PopoverFooter,
PopoverArrow,
PopoverBody,
PopoverContent,
Flex,
Skeleton,
SkeletonCircle,
} from '@chakra-ui/react'
import React, { useMemo, useRef, useState } from 'react'
import { Credentials, credentialsTypes } from '@typebot.io/schemas'
import { BlockIcon } from '@/features/editor/components/BlockIcon'
import { BlockLabel } from '@/features/editor/components/BlockLabel'
import { Text } from '@chakra-ui/react'
import { WhatsAppLogo } from '@/components/logos/WhatsAppLogo'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
import { StripeLogo } from '@/components/logos/StripeLogo'
import { EditIcon, PlusIcon, TrashIcon } from '@/components/icons'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { toast } from 'sonner'
import { CredentialsCreateModal } from './CredentialsCreateModal'
import { CredentialsUpdateModal } from './CredentialsUpdateModal'
const nonEditableTypes = ['whatsApp', 'google sheets'] as const
type CredentialsInfo = Pick<Credentials, 'id' | 'type' | 'name'>
export const CredentialsSettingsForm = () => {
const [creatingType, setCreatingType] = useState<Credentials['type']>()
const [editingCredentials, setEditingCredentials] = useState<{
id: string
type: Credentials['type']
}>()
const [deletingCredentialsId, setDeletingCredentialsId] = useState<string>()
const { workspace } = useWorkspace()
const { data, isLoading, refetch } =
trpc.credentials.listCredentials.useQuery(
{
workspaceId: workspace!.id,
},
{
enabled: !!workspace?.id,
}
)
const { mutate: deleteCredentials } =
trpc.credentials.deleteCredentials.useMutation({
onMutate: ({ credentialsId }) =>
setDeletingCredentialsId(credentialsId as string),
onSettled: () => {
setDeletingCredentialsId(undefined)
},
onError: (error) => {
toast.error(error.message)
},
onSuccess: () => {
refetch()
},
})
const credentials = useMemo(
() =>
data?.credentials ? groupCredentialsByType(data.credentials) : undefined,
[data?.credentials]
)
return (
<Stack spacing="6" w="full">
<CredentialsCreateModal
creatingType={creatingType}
onSubmit={() => {
refetch()
setCreatingType(undefined)
}}
onClose={() => setCreatingType(undefined)}
/>
<CredentialsUpdateModal
editingCredentials={editingCredentials}
onSubmit={() => {
refetch()
setEditingCredentials(undefined)
}}
onClose={() => setEditingCredentials(undefined)}
/>
<HStack justifyContent="space-between">
<Heading fontSize="2xl">Credentials</Heading>
<Menu isLazy>
<MenuButton as={Button} size="sm" leftIcon={<PlusIcon />}>
Create new
</MenuButton>
<MenuList>
{credentialsTypes.map((type) => (
<MenuItem
key={type}
icon={<CredentialsIcon type={type} boxSize="16px" />}
onClick={() => setCreatingType(type)}
>
<CredentialsLabel type={type} />
</MenuItem>
))}
</MenuList>
</Menu>
</HStack>
{credentials && !isLoading ? (
(Object.keys(credentials) as Credentials['type'][]).map((type) => (
<Stack
key={type}
borderWidth="1px"
borderRadius="md"
p="4"
spacing={4}
data-testid={type}
>
<HStack spacing="3">
<CredentialsIcon type={type} boxSize="24px" />
<CredentialsLabel type={type} fontWeight="semibold" />
</HStack>
<Stack>
{credentials[type].map((cred) => (
<Stack key={cred.id}>
<CredentialsItem
type={cred.type}
name={cred.name}
isDeleting={deletingCredentialsId === cred.id}
onEditClick={
nonEditableTypes.includes(
cred.type as (typeof nonEditableTypes)[number]
)
? undefined
: () =>
setEditingCredentials({
id: cred.id,
type: cred.type,
})
}
onDeleteClick={() =>
deleteCredentials({
workspaceId: workspace!.id,
credentialsId: cred.id,
})
}
/>
<Divider />
</Stack>
))}
</Stack>
</Stack>
))
) : (
<Stack borderRadius="md" spacing="6">
<Stack spacing={4}>
<SkeletonCircle />
<Stack>
<Skeleton height="20px" />
<Skeleton height="20px" />
</Stack>
</Stack>
<Stack spacing={4}>
<SkeletonCircle />
<Stack>
<Skeleton height="20px" />
<Skeleton height="20px" />
</Stack>
</Stack>
<Stack spacing={4}>
<SkeletonCircle />
<Stack>
<Skeleton height="20px" />
<Skeleton height="20px" />
</Stack>
</Stack>
<Stack spacing={4}>
<SkeletonCircle />
<Stack>
<Skeleton height="20px" />
<Skeleton height="20px" />
</Stack>
</Stack>
</Stack>
)}
</Stack>
)
}
const CredentialsIcon = ({
type,
...props
}: { type: Credentials['type'] } & IconProps) => {
switch (type) {
case 'google sheets':
return <BlockIcon type={IntegrationBlockType.GOOGLE_SHEETS} {...props} />
case 'smtp':
return <BlockIcon type={IntegrationBlockType.EMAIL} {...props} />
case 'stripe':
return <StripeLogo rounded="sm" {...props} />
case 'whatsApp':
return <WhatsAppLogo {...props} />
default:
return <BlockIcon type={type} {...props} />
}
}
const CredentialsLabel = ({
type,
...props
}: { type: Credentials['type'] } & TextProps) => {
switch (type) {
case 'google sheets':
return (
<Text fontSize="sm" {...props}>
Google Sheets
</Text>
)
case 'smtp':
return (
<Text fontSize="sm" {...props}>
SMTP
</Text>
)
case 'stripe':
return (
<Text fontSize="sm" {...props}>
Stripe
</Text>
)
case 'whatsApp':
return (
<Text fontSize="sm" {...props}>
WhatsApp
</Text>
)
default:
return <BlockLabel type={type} {...props} />
}
}
const CredentialsItem = ({
isDeleting,
onEditClick,
onDeleteClick,
...cred
}: Pick<Credentials, 'name' | 'type'> & {
isDeleting: boolean
onEditClick?: () => void
onDeleteClick: () => void
}) => {
const initialFocusRef = useRef<HTMLButtonElement>(null)
return (
<HStack justifyContent="space-between" py="2">
<Text fontSize="sm">{cred.name}</Text>
<HStack>
{onEditClick && (
<IconButton
aria-label="Edit"
icon={<EditIcon />}
size="xs"
onClick={onEditClick}
/>
)}
<Popover isLazy initialFocusRef={initialFocusRef}>
{({ onClose }) => (
<>
<PopoverTrigger>
<IconButton
aria-label="Delete"
icon={<TrashIcon />}
size="xs"
/>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverBody>
<Stack spacing="2">
<Text fontSize="sm" fontWeight="semibold">
Are you sure?
</Text>
<Text fontSize="sm">
Make sure this credentials is not used in any of your
published bot before proceeding.
</Text>
</Stack>
</PopoverBody>
<PopoverFooter as={Flex} justifyContent="flex-end">
<HStack>
<Button ref={initialFocusRef} onClick={onClose} size="sm">
Cancel
</Button>
<Button
colorScheme="red"
onClick={onDeleteClick}
isLoading={isDeleting}
size="sm"
>
Delete
</Button>
</HStack>
</PopoverFooter>
</PopoverContent>
</>
)}
</Popover>
</HStack>
</HStack>
)
}
const groupCredentialsByType = (
credentials: CredentialsInfo[]
): Record<CredentialsInfo['type'], CredentialsInfo[]> => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const groupedCredentials: any = {}
credentials.forEach((cred) => {
if (!groupedCredentials[cred.type]) {
groupedCredentials[cred.type] = []
}
groupedCredentials[cred.type].push(cred)
})
return groupedCredentials
}

View File

@ -0,0 +1,71 @@
import { UpdateStripeCredentialsModalContent } from '@/features/blocks/inputs/payment/components/UpdateStripeCredentialsModalContent'
import { SmtpUpdateModalContent } from '@/features/blocks/integrations/sendEmail/components/SmtpUpdateModalContent'
import { UpdateForgedCredentialsModalContent } from '@/features/forge/components/credentials/UpdateForgedCredentialsModalContent'
import { Modal, ModalOverlay } from '@chakra-ui/react'
import { forgedBlocks } from '@typebot.io/forge-repository/definitions'
import { Credentials } from '@typebot.io/schemas/features/credentials'
export const CredentialsUpdateModal = ({
editingCredentials,
onSubmit,
onClose,
}: {
editingCredentials?: {
id: string
type: Credentials['type']
}
onClose: () => void
onSubmit: () => void
}) => {
return (
<Modal isOpen={editingCredentials !== undefined} onClose={onClose}>
<ModalOverlay />
{editingCredentials && (
<CredentialsUpdateModalContent
editingCredentials={editingCredentials}
onSubmit={onSubmit}
/>
)}
</Modal>
)
}
const CredentialsUpdateModalContent = ({
editingCredentials,
onSubmit,
}: {
editingCredentials: {
id: string
type: Credentials['type']
}
onSubmit: () => void
}) => {
switch (editingCredentials.type) {
case 'google sheets':
return null
case 'smtp':
return (
<SmtpUpdateModalContent
credentialsId={editingCredentials.id}
onUpdate={onSubmit}
/>
)
case 'stripe':
return (
<UpdateStripeCredentialsModalContent
credentialsId={editingCredentials.id}
onUpdate={onSubmit}
/>
)
case 'whatsApp':
return null
default:
return (
<UpdateForgedCredentialsModalContent
credentialsId={editingCredentials.id}
blockDef={forgedBlocks[editingCredentials.type]}
onUpdate={onSubmit}
/>
)
}
}

View File

@ -0,0 +1,39 @@
import { expect, test } from '@playwright/test'
test('should be able to create, update and delete credentials', async ({
page,
}) => {
await page.goto('/typebots')
await page.click('text=Settings & Members')
await page.click('text=Credentials')
// Create
await page.getByRole('button', { name: 'Create new' }).click()
await page.getByRole('menuitem', { name: 'OpenAI' }).click()
await page.getByPlaceholder('My account').fill('Typebot')
await page.getByPlaceholder('sk-').fill('sk-test')
await page.getByRole('button', { name: 'Create' }).click()
await expect(page.getByTestId('openai').getByText('Typebot')).toBeVisible()
// Edit
await page.pause()
await page.getByTestId('openai').getByRole('button', { name: 'Edit' }).click()
await expect(page.getByPlaceholder('My account')).toHaveValue('Typebot')
await expect(page.getByPlaceholder('sk-')).toHaveValue('sk-test')
await page.getByPlaceholder('sk-').fill('sk-test-2')
await page.getByPlaceholder('My account').fill('Typebot 2')
await page.getByRole('button', { name: 'Update' }).click()
await expect(page.getByTestId('openai').getByText('Typebot 2')).toBeVisible()
// Delete
await page
.getByTestId('openai')
.getByRole('button', { name: 'Delete' })
.click()
await page
.getByTestId('openai')
.getByRole('button', { name: 'Delete' })
.nth(1)
.click()
await expect(page.getByTestId('openai').getByText('Typebot 2')).toBeHidden()
})

View File

@ -10,13 +10,19 @@ import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { WorkspaceDropdown } from '@/features/workspace/components/WorkspaceDropdown' import { WorkspaceDropdown } from '@/features/workspace/components/WorkspaceDropdown'
import { WorkspaceSettingsModal } from '@/features/workspace/components/WorkspaceSettingsModal' import { WorkspaceSettingsModal } from '@/features/workspace/components/WorkspaceSettingsModal'
import { ParentModalProvider } from '@/features/graph/providers/ParentModalProvider' import { ParentModalProvider } from '@/features/graph/providers/ParentModalProvider'
import { useRouter } from 'next/router'
export const DashboardHeader = () => { export const DashboardHeader = () => {
const { t } = useTranslate() const { t } = useTranslate()
const { user, logOut } = useUser() const { user, logOut } = useUser()
const { workspace, switchWorkspace, createWorkspace } = useWorkspace() const { workspace, switchWorkspace, createWorkspace } = useWorkspace()
const { asPath } = useRouter()
const { isOpen, onOpen, onClose } = useDisclosure() const isRedirectFromCredentialsCreation = asPath.includes('credentials')
const { isOpen, onOpen, onClose } = useDisclosure({
defaultIsOpen: isRedirectFromCredentialsCreation,
})
const handleCreateNewWorkspace = () => const handleCreateNewWorkspace = () =>
createWorkspace(user?.name ?? undefined) createWorkspace(user?.name ?? undefined)
@ -45,6 +51,9 @@ export const DashboardHeader = () => {
onClose={onClose} onClose={onClose}
user={user} user={user}
workspace={workspace} workspace={workspace}
defaultTab={
isRedirectFromCredentialsCreation ? 'credentials' : undefined
}
/> />
</ParentModalProvider> </ParentModalProvider>
)} )}

View File

@ -1,6 +1,5 @@
import { useColorModeValue } from '@chakra-ui/react' import { IconProps, useColorModeValue } from '@chakra-ui/react'
import React from 'react' import React from 'react'
import { FlagIcon, SendEmailIcon, ThunderIcon } from '@/components/icons'
import { WaitIcon } from '@/features/blocks/logic/wait/components/WaitIcon' import { WaitIcon } from '@/features/blocks/logic/wait/components/WaitIcon'
import { ScriptIcon } from '@/features/blocks/logic/script/components/ScriptIcon' import { ScriptIcon } from '@/features/blocks/logic/script/components/ScriptIcon'
import { JumpIcon } from '@/features/blocks/logic/jump/components/JumpIcon' import { JumpIcon } from '@/features/blocks/logic/jump/components/JumpIcon'
@ -40,10 +39,12 @@ import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/consta
import { Block } from '@typebot.io/schemas' import { Block } from '@typebot.io/schemas'
import { OpenAILogo } from '@/features/blocks/integrations/openai/components/OpenAILogo' import { OpenAILogo } from '@/features/blocks/integrations/openai/components/OpenAILogo'
import { ForgedBlockIcon } from '@/features/forge/ForgedBlockIcon' import { ForgedBlockIcon } from '@/features/forge/ForgedBlockIcon'
import { SendEmailIcon } from '@/features/blocks/integrations/sendEmail/components/SendEmailIcon'
import { FlagIcon, ThunderIcon } from '@/components/icons'
type BlockIconProps = { type: Block['type']; mt?: string } type BlockIconProps = { type: Block['type'] } & IconProps
export const BlockIcon = ({ type, mt }: BlockIconProps): JSX.Element => { export const BlockIcon = ({ type, ...props }: BlockIconProps): JSX.Element => {
const blue = useColorModeValue('blue.500', 'blue.300') const blue = useColorModeValue('blue.500', 'blue.300')
const orange = useColorModeValue('orange.500', 'orange.300') const orange = useColorModeValue('orange.500', 'orange.300')
const purple = useColorModeValue('purple.500', 'purple.300') const purple = useColorModeValue('purple.500', 'purple.300')
@ -51,78 +52,78 @@ export const BlockIcon = ({ type, mt }: BlockIconProps): JSX.Element => {
switch (type) { switch (type) {
case BubbleBlockType.TEXT: case BubbleBlockType.TEXT:
return <TextBubbleIcon color={blue} mt={mt} /> return <TextBubbleIcon color={blue} {...props} />
case BubbleBlockType.IMAGE: case BubbleBlockType.IMAGE:
return <ImageBubbleIcon color={blue} mt={mt} /> return <ImageBubbleIcon color={blue} {...props} />
case BubbleBlockType.VIDEO: case BubbleBlockType.VIDEO:
return <VideoBubbleIcon color={blue} mt={mt} /> return <VideoBubbleIcon color={blue} {...props} />
case BubbleBlockType.EMBED: case BubbleBlockType.EMBED:
return <EmbedBubbleIcon color={blue} mt={mt} /> return <EmbedBubbleIcon color={blue} {...props} />
case BubbleBlockType.AUDIO: case BubbleBlockType.AUDIO:
return <AudioBubbleIcon color={blue} mt={mt} /> return <AudioBubbleIcon color={blue} {...props} />
case InputBlockType.TEXT: case InputBlockType.TEXT:
return <TextInputIcon color={orange} mt={mt} /> return <TextInputIcon color={orange} {...props} />
case InputBlockType.NUMBER: case InputBlockType.NUMBER:
return <NumberInputIcon color={orange} mt={mt} /> return <NumberInputIcon color={orange} {...props} />
case InputBlockType.EMAIL: case InputBlockType.EMAIL:
return <EmailInputIcon color={orange} mt={mt} /> return <EmailInputIcon color={orange} {...props} />
case InputBlockType.URL: case InputBlockType.URL:
return <UrlInputIcon color={orange} mt={mt} /> return <UrlInputIcon color={orange} {...props} />
case InputBlockType.DATE: case InputBlockType.DATE:
return <DateInputIcon color={orange} mt={mt} /> return <DateInputIcon color={orange} {...props} />
case InputBlockType.PHONE: case InputBlockType.PHONE:
return <PhoneInputIcon color={orange} mt={mt} /> return <PhoneInputIcon color={orange} {...props} />
case InputBlockType.CHOICE: case InputBlockType.CHOICE:
return <ButtonsInputIcon color={orange} mt={mt} /> return <ButtonsInputIcon color={orange} {...props} />
case InputBlockType.PICTURE_CHOICE: case InputBlockType.PICTURE_CHOICE:
return <PictureChoiceIcon color={orange} mt={mt} /> return <PictureChoiceIcon color={orange} {...props} />
case InputBlockType.PAYMENT: case InputBlockType.PAYMENT:
return <PaymentInputIcon color={orange} mt={mt} /> return <PaymentInputIcon color={orange} {...props} />
case InputBlockType.RATING: case InputBlockType.RATING:
return <RatingInputIcon color={orange} mt={mt} /> return <RatingInputIcon color={orange} {...props} />
case InputBlockType.FILE: case InputBlockType.FILE:
return <FileInputIcon color={orange} mt={mt} /> return <FileInputIcon color={orange} {...props} />
case LogicBlockType.SET_VARIABLE: case LogicBlockType.SET_VARIABLE:
return <SetVariableIcon color={purple} mt={mt} /> return <SetVariableIcon color={purple} {...props} />
case LogicBlockType.CONDITION: case LogicBlockType.CONDITION:
return <ConditionIcon color={purple} mt={mt} /> return <ConditionIcon color={purple} {...props} />
case LogicBlockType.REDIRECT: case LogicBlockType.REDIRECT:
return <RedirectIcon color={purple} mt={mt} /> return <RedirectIcon color={purple} {...props} />
case LogicBlockType.SCRIPT: case LogicBlockType.SCRIPT:
return <ScriptIcon mt={mt} /> return <ScriptIcon {...props} />
case LogicBlockType.WAIT: case LogicBlockType.WAIT:
return <WaitIcon color={purple} mt={mt} /> return <WaitIcon color={purple} {...props} />
case LogicBlockType.JUMP: case LogicBlockType.JUMP:
return <JumpIcon color={purple} mt={mt} /> return <JumpIcon color={purple} {...props} />
case LogicBlockType.TYPEBOT_LINK: case LogicBlockType.TYPEBOT_LINK:
return <TypebotLinkIcon color={purple} mt={mt} /> return <TypebotLinkIcon color={purple} {...props} />
case LogicBlockType.AB_TEST: case LogicBlockType.AB_TEST:
return <AbTestIcon color={purple} mt={mt} /> return <AbTestIcon color={purple} {...props} />
case IntegrationBlockType.GOOGLE_SHEETS: case IntegrationBlockType.GOOGLE_SHEETS:
return <GoogleSheetsLogo mt={mt} /> return <GoogleSheetsLogo {...props} />
case IntegrationBlockType.GOOGLE_ANALYTICS: case IntegrationBlockType.GOOGLE_ANALYTICS:
return <GoogleAnalyticsLogo mt={mt} /> return <GoogleAnalyticsLogo {...props} />
case IntegrationBlockType.WEBHOOK: case IntegrationBlockType.WEBHOOK:
return <ThunderIcon mt={mt} /> return <ThunderIcon {...props} />
case IntegrationBlockType.ZAPIER: case IntegrationBlockType.ZAPIER:
return <ZapierLogo mt={mt} /> return <ZapierLogo {...props} />
case IntegrationBlockType.MAKE_COM: case IntegrationBlockType.MAKE_COM:
return <MakeComLogo mt={mt} /> return <MakeComLogo {...props} />
case IntegrationBlockType.PABBLY_CONNECT: case IntegrationBlockType.PABBLY_CONNECT:
return <PabblyConnectLogo mt={mt} /> return <PabblyConnectLogo {...props} />
case IntegrationBlockType.EMAIL: case IntegrationBlockType.EMAIL:
return <SendEmailIcon mt={mt} /> return <SendEmailIcon {...props} />
case IntegrationBlockType.CHATWOOT: case IntegrationBlockType.CHATWOOT:
return <ChatwootLogo mt={mt} /> return <ChatwootLogo {...props} />
case IntegrationBlockType.PIXEL: case IntegrationBlockType.PIXEL:
return <PixelLogo mt={mt} /> return <PixelLogo {...props} />
case IntegrationBlockType.ZEMANTIC_AI: case IntegrationBlockType.ZEMANTIC_AI:
return <ZemanticAiLogo mt={mt} /> return <ZemanticAiLogo {...props} />
case 'start': case 'start':
return <FlagIcon mt={mt} /> return <FlagIcon {...props} />
case IntegrationBlockType.OPEN_AI: case IntegrationBlockType.OPEN_AI:
return <OpenAILogo mt={mt} fill={openAIColor} /> return <OpenAILogo {...props} fill={openAIColor} />
default: default:
return <ForgedBlockIcon type={type} mt={mt} /> return <ForgedBlockIcon type={type} {...props} />
} }
} }

View File

@ -1,4 +1,4 @@
import { Text } from '@chakra-ui/react' import { Text, TextProps } from '@chakra-ui/react'
import React from 'react' import React from 'react'
import { useTranslate } from '@tolgee/react' import { useTranslate } from '@tolgee/react'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants' import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
@ -8,98 +8,224 @@ import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/consta
import { Block } from '@typebot.io/schemas' import { Block } from '@typebot.io/schemas'
import { ForgedBlockLabel } from '@/features/forge/ForgedBlockLabel' import { ForgedBlockLabel } from '@/features/forge/ForgedBlockLabel'
type Props = { type: Block['type'] } type Props = { type: Block['type'] } & TextProps
export const BlockLabel = ({ type }: Props): JSX.Element => { export const BlockLabel = ({ type, ...props }: Props): JSX.Element => {
const { t } = useTranslate() const { t } = useTranslate()
switch (type) { switch (type) {
case 'start': case 'start':
return <Text fontSize="sm">{t('editor.sidebarBlock.start.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.start.label')}
</Text>
)
case BubbleBlockType.TEXT: case BubbleBlockType.TEXT:
case InputBlockType.TEXT: case InputBlockType.TEXT:
return <Text fontSize="sm">{t('editor.sidebarBlock.text.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.text.label')}
</Text>
)
case BubbleBlockType.IMAGE: case BubbleBlockType.IMAGE:
return <Text fontSize="sm">{t('editor.sidebarBlock.image.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.image.label')}
</Text>
)
case BubbleBlockType.VIDEO: case BubbleBlockType.VIDEO:
return <Text fontSize="sm">{t('editor.sidebarBlock.video.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.video.label')}
</Text>
)
case BubbleBlockType.EMBED: case BubbleBlockType.EMBED:
return <Text fontSize="sm">{t('editor.sidebarBlock.embed.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.embed.label')}
</Text>
)
case BubbleBlockType.AUDIO: case BubbleBlockType.AUDIO:
return <Text fontSize="sm">{t('editor.sidebarBlock.audio.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.audio.label')}
</Text>
)
case InputBlockType.NUMBER: case InputBlockType.NUMBER:
return <Text fontSize="sm">{t('editor.sidebarBlock.number.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.number.label')}
</Text>
)
case InputBlockType.EMAIL: case InputBlockType.EMAIL:
return <Text fontSize="sm">{t('editor.sidebarBlock.email.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.email.label')}
</Text>
)
case InputBlockType.URL: case InputBlockType.URL:
return <Text fontSize="sm">{t('editor.sidebarBlock.website.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.website.label')}
</Text>
)
case InputBlockType.DATE: case InputBlockType.DATE:
return <Text fontSize="sm">{t('editor.sidebarBlock.date.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.date.label')}
</Text>
)
case InputBlockType.PHONE: case InputBlockType.PHONE:
return <Text fontSize="sm">{t('editor.sidebarBlock.phone.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.phone.label')}
</Text>
)
case InputBlockType.CHOICE: case InputBlockType.CHOICE:
return <Text fontSize="sm">{t('editor.sidebarBlock.button.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.button.label')}
</Text>
)
case InputBlockType.PICTURE_CHOICE: case InputBlockType.PICTURE_CHOICE:
return ( return (
<Text fontSize="sm">{t('editor.sidebarBlock.picChoice.label')}</Text> <Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.picChoice.label')}
</Text>
) )
case InputBlockType.PAYMENT: case InputBlockType.PAYMENT:
return <Text fontSize="sm">{t('editor.sidebarBlock.payment.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.payment.label')}
</Text>
)
case InputBlockType.RATING: case InputBlockType.RATING:
return <Text fontSize="sm">{t('editor.sidebarBlock.rating.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.rating.label')}
</Text>
)
case InputBlockType.FILE: case InputBlockType.FILE:
return <Text fontSize="sm">{t('editor.sidebarBlock.file.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.file.label')}
</Text>
)
case LogicBlockType.SET_VARIABLE: case LogicBlockType.SET_VARIABLE:
return ( return (
<Text fontSize="sm">{t('editor.sidebarBlock.setVariable.label')}</Text> <Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.setVariable.label')}
</Text>
) )
case LogicBlockType.CONDITION: case LogicBlockType.CONDITION:
return ( return (
<Text fontSize="sm">{t('editor.sidebarBlock.condition.label')}</Text> <Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.condition.label')}
</Text>
) )
case LogicBlockType.REDIRECT: case LogicBlockType.REDIRECT:
return ( return (
<Text fontSize="sm">{t('editor.sidebarBlock.redirect.label')}</Text> <Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.redirect.label')}
</Text>
) )
case LogicBlockType.SCRIPT: case LogicBlockType.SCRIPT:
return <Text fontSize="sm">{t('editor.sidebarBlock.script.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.script.label')}
</Text>
)
case LogicBlockType.TYPEBOT_LINK: case LogicBlockType.TYPEBOT_LINK:
return <Text fontSize="sm">{t('editor.sidebarBlock.typebot.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.typebot.label')}
</Text>
)
case LogicBlockType.WAIT: case LogicBlockType.WAIT:
return <Text fontSize="sm">{t('editor.sidebarBlock.wait.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.wait.label')}
</Text>
)
case LogicBlockType.JUMP: case LogicBlockType.JUMP:
return <Text fontSize="sm">{t('editor.sidebarBlock.jump.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.jump.label')}
</Text>
)
case LogicBlockType.AB_TEST: case LogicBlockType.AB_TEST:
return <Text fontSize="sm">{t('editor.sidebarBlock.abTest.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.abTest.label')}
</Text>
)
case IntegrationBlockType.GOOGLE_SHEETS: case IntegrationBlockType.GOOGLE_SHEETS:
return <Text fontSize="sm">{t('editor.sidebarBlock.sheets.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.sheets.label')}
</Text>
)
case IntegrationBlockType.GOOGLE_ANALYTICS: case IntegrationBlockType.GOOGLE_ANALYTICS:
return ( return (
<Text fontSize="sm">{t('editor.sidebarBlock.analytics.label')}</Text> <Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.analytics.label')}
</Text>
) )
case IntegrationBlockType.WEBHOOK: case IntegrationBlockType.WEBHOOK:
return <Text fontSize="sm">HTTP request</Text> return (
<Text fontSize="sm" {...props}>
HTTP request
</Text>
)
case IntegrationBlockType.ZAPIER: case IntegrationBlockType.ZAPIER:
return <Text fontSize="sm">{t('editor.sidebarBlock.zapier.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.zapier.label')}
</Text>
)
case IntegrationBlockType.MAKE_COM: case IntegrationBlockType.MAKE_COM:
return <Text fontSize="sm">{t('editor.sidebarBlock.makecom.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.makecom.label')}
</Text>
)
case IntegrationBlockType.PABBLY_CONNECT: case IntegrationBlockType.PABBLY_CONNECT:
return <Text fontSize="sm">{t('editor.sidebarBlock.pabbly.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.pabbly.label')}
</Text>
)
case IntegrationBlockType.EMAIL: case IntegrationBlockType.EMAIL:
return <Text fontSize="sm">{t('editor.sidebarBlock.email.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.email.label')}
</Text>
)
case IntegrationBlockType.CHATWOOT: case IntegrationBlockType.CHATWOOT:
return ( return (
<Text fontSize="sm">{t('editor.sidebarBlock.chatwoot.label')}</Text> <Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.chatwoot.label')}
</Text>
) )
case IntegrationBlockType.OPEN_AI: case IntegrationBlockType.OPEN_AI:
return <Text fontSize="sm">{t('editor.sidebarBlock.openai.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.openai.label')}
</Text>
)
case IntegrationBlockType.PIXEL: case IntegrationBlockType.PIXEL:
return <Text fontSize="sm">{t('editor.sidebarBlock.pixel.label')}</Text> return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.pixel.label')}
</Text>
)
case IntegrationBlockType.ZEMANTIC_AI: case IntegrationBlockType.ZEMANTIC_AI:
return ( return (
<Text fontSize="sm">{t('editor.sidebarBlock.zemanticAi.label')}</Text> <Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.zemanticAi.label')}
</Text>
) )
default: default:
return <ForgedBlockLabel type={type} /> return <ForgedBlockLabel type={type} {...props} />
} }
} }

View File

@ -1,18 +1,27 @@
import { useColorMode } from '@chakra-ui/react' import { IconProps, useColorMode } from '@chakra-ui/react'
import { ForgedBlock } from '@typebot.io/forge-repository/types' import { ForgedBlock } from '@typebot.io/forge-repository/types'
import { useForgedBlock } from './hooks/useForgedBlock' import { useForgedBlock } from './hooks/useForgedBlock'
export const ForgedBlockIcon = ({ export const ForgedBlockIcon = ({
type, type,
mt, ...props
}: { }: {
type: ForgedBlock['type'] type: ForgedBlock['type']
mt?: string } & IconProps): JSX.Element => {
}): JSX.Element => {
const { colorMode } = useColorMode() const { colorMode } = useColorMode()
const { blockDef } = useForgedBlock(type) const { blockDef } = useForgedBlock(type)
if (!blockDef) return <></> if (!blockDef) return <></>
if (colorMode === 'dark' && blockDef.DarkLogo) if (colorMode === 'dark' && blockDef.DarkLogo)
return <blockDef.DarkLogo width="1rem" style={{ marginTop: mt }} /> return (
return <blockDef.LightLogo width="1rem" style={{ marginTop: mt }} /> <blockDef.DarkLogo
width="1rem"
style={{ marginTop: props.mt?.toString() }}
/>
)
return (
<blockDef.LightLogo
width="1rem"
style={{ marginTop: props.mt?.toString() }}
/>
)
} }

View File

@ -1,9 +1,16 @@
import { ForgedBlock } from '@typebot.io/forge-repository/types' import { ForgedBlock } from '@typebot.io/forge-repository/types'
import { useForgedBlock } from './hooks/useForgedBlock' import { useForgedBlock } from './hooks/useForgedBlock'
import { Text } from '@chakra-ui/react' import { Text, TextProps } from '@chakra-ui/react'
export const ForgedBlockLabel = ({ type }: { type: ForgedBlock['type'] }) => { export const ForgedBlockLabel = ({
type,
...props
}: { type: ForgedBlock['type'] } & TextProps) => {
const { blockDef } = useForgedBlock(type) const { blockDef } = useForgedBlock(type)
return <Text fontSize="sm">{blockDef?.name}</Text> return (
<Text fontSize="sm" {...props}>
{blockDef?.name}
</Text>
)
} }

View File

@ -1,54 +0,0 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { encrypt } from '@typebot.io/lib/api/encryption/encrypt'
import { z } from 'zod'
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden'
import { forgedCredentialsSchemas } from '@typebot.io/forge-repository/credentials'
import { isDefined } from '@typebot.io/lib'
const inputShape = {
data: true,
type: true,
workspaceId: true,
name: true,
} as const
export const createCredentials = authenticatedProcedure
.input(
z.object({
credentials: z.discriminatedUnion(
'type',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Object.values(forgedCredentialsSchemas)
.filter(isDefined)
.map((i) => i.pick(inputShape))
),
})
)
.mutation(async ({ input: { credentials }, ctx: { user } }) => {
const workspace = await prisma.workspace.findFirst({
where: {
id: credentials.workspaceId,
},
select: { id: true, members: true },
})
if (!workspace || isWriteWorkspaceForbidden(workspace, user))
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
const { encryptedData, iv } = await encrypt(credentials.data)
const createdCredentials = await prisma.credentials.create({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
data: {
...credentials,
data: encryptedData,
iv,
},
select: {
id: true,
},
})
return { credentialsId: createdCredentials.id }
})

View File

@ -1,38 +0,0 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden'
export const deleteCredentials = authenticatedProcedure
.input(
z.object({
credentialsId: z.string(),
workspaceId: z.string(),
})
)
.mutation(
async ({ input: { credentialsId, workspaceId }, ctx: { user } }) => {
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
members: {
some: { userId: user.id, role: { in: ['ADMIN', 'MEMBER'] } },
},
},
select: { id: true, members: true },
})
if (!workspace || isWriteWorkspaceForbidden(workspace, user))
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Workspace not found',
})
await prisma.credentials.delete({
where: {
id: credentialsId,
},
})
return { credentialsId }
}
)

View File

@ -1,38 +0,0 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
import { forgedBlockIds } from '@typebot.io/forge-repository/constants'
export const listCredentials = authenticatedProcedure
.input(
z.object({
workspaceId: z.string(),
type: z.enum(forgedBlockIds),
})
)
.query(async ({ input: { workspaceId, type }, ctx: { user } }) => {
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
},
select: {
id: true,
members: true,
credentials: {
where: {
type,
},
select: {
id: true,
name: true,
},
},
},
})
if (!workspace || isReadWorkspaceFobidden(workspace, user))
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
return { credentials: workspace.credentials }
})

View File

@ -1,12 +1,6 @@
import { router } from '@/helpers/server/trpc' import { router } from '@/helpers/server/trpc'
import { fetchSelectItems } from './fetchSelectItems' import { fetchSelectItems } from './fetchSelectItems'
import { createCredentials } from './credentials/createCredentials'
import { deleteCredentials } from './credentials/deleteCredentials'
import { listCredentials } from './credentials/listCredentials'
export const forgeRouter = router({ export const forgeRouter = router({
fetchSelectItems, fetchSelectItems,
createCredentials,
listCredentials,
deleteCredentials,
}) })

View File

@ -1,7 +1,7 @@
import { Stack, useDisclosure } from '@chakra-ui/react' import { Stack, useDisclosure } from '@chakra-ui/react'
import { BlockOptions } from '@typebot.io/schemas' import { BlockOptions } from '@typebot.io/schemas'
import { ForgedCredentialsDropdown } from './credentials/ForgedCredentialsDropdown' import { ForgedCredentialsDropdown } from './credentials/ForgedCredentialsDropdown'
import { ForgedCredentialsModal } from './credentials/ForgedCredentialsModal' import { CreateForgedCredentialsModal } from './credentials/CreateForgedCredentialsModal'
import { ZodObjectLayout } from './zodLayouts/ZodObjectLayout' import { ZodObjectLayout } from './zodLayouts/ZodObjectLayout'
import { ZodActionDiscriminatedUnion } from './zodLayouts/ZodActionDiscriminatedUnion' import { ZodActionDiscriminatedUnion } from './zodLayouts/ZodActionDiscriminatedUnion'
import { useForgedBlock } from '../hooks/useForgedBlock' import { useForgedBlock } from '../hooks/useForgedBlock'
@ -64,7 +64,7 @@ export const ForgedBlockSettings = ({ block, onOptionsChange }: Props) => {
<Stack spacing={4}> <Stack spacing={4}>
{blockDef.auth && ( {blockDef.auth && (
<> <>
<ForgedCredentialsModal <CreateForgedCredentialsModal
blockDef={blockDef} blockDef={blockDef}
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}

View File

@ -16,20 +16,44 @@ import {
import React, { useState } from 'react' import React, { useState } from 'react'
import { ZodObjectLayout } from '../zodLayouts/ZodObjectLayout' import { ZodObjectLayout } from '../zodLayouts/ZodObjectLayout'
import { ForgedBlockDefinition } from '@typebot.io/forge-repository/types' import { ForgedBlockDefinition } from '@typebot.io/forge-repository/types'
import { Credentials } from '@typebot.io/schemas'
type Props = { type Props = {
blockDef: ForgedBlockDefinition blockDef: ForgedBlockDefinition
isOpen: boolean isOpen: boolean
// eslint-disable-next-line @typescript-eslint/no-explicit-any
defaultData?: any
onClose: () => void onClose: () => void
onNewCredentials: (id: string) => void onNewCredentials: (id: string) => void
} }
export const ForgedCredentialsModal = ({ export const CreateForgedCredentialsModal = ({
blockDef, blockDef,
isOpen, isOpen,
defaultData,
onClose, onClose,
onNewCredentials, onNewCredentials,
}: Props) => { }: Props) => {
if (!blockDef.auth) return null
return (
<Modal isOpen={isOpen} onClose={onClose} size="lg">
<ModalOverlay />
<CreateForgedCredentialsModalContent
defaultData={defaultData}
blockDef={blockDef}
onNewCredentials={(id) => {
onClose()
onNewCredentials(id)
}}
/>
</Modal>
)
}
export const CreateForgedCredentialsModalContent = ({
blockDef,
onNewCredentials,
}: Pick<Props, 'blockDef' | 'onNewCredentials' | 'defaultData'>) => {
const { workspace } = useWorkspace() const { workspace } = useWorkspace()
const { showToast } = useToast() const { showToast } = useToast()
const [name, setName] = useState('') const [name, setName] = useState('')
@ -42,7 +66,8 @@ export const ForgedCredentialsModal = ({
listCredentials: { refetch: refetchCredentials }, listCredentials: { refetch: refetchCredentials },
}, },
} = trpc.useContext() } = trpc.useContext()
const { mutate } = trpc.forge.createCredentials.useMutation({
const { mutate } = trpc.credentials.createCredentials.useMutation({
onMutate: () => setIsCreating(true), onMutate: () => setIsCreating(true),
onSettled: () => setIsCreating(false), onSettled: () => setIsCreating(false),
onError: (err) => { onError: (err) => {
@ -54,59 +79,55 @@ export const ForgedCredentialsModal = ({
onSuccess: (data) => { onSuccess: (data) => {
refetchCredentials() refetchCredentials()
onNewCredentials(data.credentialsId) onNewCredentials(data.credentialsId)
onClose()
}, },
}) })
const createOpenAICredentials = async (e: React.FormEvent) => { const createOpenAICredentials = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
if (!workspace) return if (!workspace || !blockDef.auth) return
mutate({ mutate({
credentials: { credentials: {
type: blockDef.id, type: blockDef.id as Credentials['type'],
workspaceId: workspace.id, workspaceId: workspace.id,
name, name,
data, data,
}, } as Credentials,
}) })
} }
if (!blockDef.auth) return null if (!blockDef.auth) return null
return ( return (
<Modal isOpen={isOpen} onClose={onClose} size="lg"> <ModalContent>
<ModalOverlay /> <ModalHeader>Add {blockDef.auth.name}</ModalHeader>
<ModalContent> <ModalCloseButton />
<ModalHeader>Add {blockDef.auth.name}</ModalHeader> <form onSubmit={createOpenAICredentials}>
<ModalCloseButton /> <ModalBody as={Stack} spacing="6">
<form onSubmit={createOpenAICredentials}> <TextInput
<ModalBody as={Stack} spacing="6"> isRequired
<TextInput label="Name"
isRequired onChange={setName}
label="Name" placeholder="My account"
onChange={setName} withVariableButton={false}
placeholder="My account" debounceTimeout={0}
withVariableButton={false} />
debounceTimeout={0} <ZodObjectLayout
/> schema={blockDef.auth.schema}
<ZodObjectLayout data={data}
schema={blockDef.auth.schema} onDataChange={setData}
data={data} />
onDataChange={setData} </ModalBody>
/>
</ModalBody>
<ModalFooter> <ModalFooter>
<Button <Button
type="submit" type="submit"
isLoading={isCreating} isLoading={isCreating}
isDisabled={Object.keys(data).length === 0} isDisabled={Object.keys(data).length === 0}
colorScheme="blue" colorScheme="blue"
> >
Create Create
</Button> </Button>
</ModalFooter> </ModalFooter>
</form> </form>
</ModalContent> </ModalContent>
</Modal>
) )
} }

View File

@ -16,6 +16,7 @@ import { trpc } from '@/lib/trpc'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider' import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { ForgedBlockDefinition } from '@typebot.io/forge-repository/types' import { ForgedBlockDefinition } from '@typebot.io/forge-repository/types'
import { useToast } from '@/hooks/useToast' import { useToast } from '@/hooks/useToast'
import { Credentials } from '@typebot.io/schemas/features/credentials'
type Props = Omit<ButtonProps, 'type'> & { type Props = Omit<ButtonProps, 'type'> & {
blockDef: ForgedBlockDefinition blockDef: ForgedBlockDefinition
@ -34,13 +35,14 @@ export const ForgedCredentialsDropdown = ({
const router = useRouter() const router = useRouter()
const { showToast } = useToast() const { showToast } = useToast()
const { workspace, currentRole } = useWorkspace() const { workspace, currentRole } = useWorkspace()
const { data, refetch, isLoading } = trpc.forge.listCredentials.useQuery( const { data, refetch, isLoading } =
{ trpc.credentials.listCredentials.useQuery(
workspaceId: workspace?.id as string, {
type: blockDef.id, workspaceId: workspace?.id as string,
}, type: blockDef.id as Credentials['type'],
{ enabled: !!workspace?.id } },
) { enabled: !!workspace?.id }
)
const [isDeleting, setIsDeleting] = useState<string>() const [isDeleting, setIsDeleting] = useState<string>()
const { mutate } = trpc.credentials.deleteCredentials.useMutation({ const { mutate } = trpc.credentials.deleteCredentials.useMutation({

View File

@ -0,0 +1,119 @@
import { TextInput } from '@/components/inputs/TextInput'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { useToast } from '@/hooks/useToast'
import { trpc } from '@/lib/trpc'
import {
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
Stack,
ModalFooter,
Button,
} from '@chakra-ui/react'
import React, { useEffect, useState } from 'react'
import { ZodObjectLayout } from '../zodLayouts/ZodObjectLayout'
import { ForgedBlockDefinition } from '@typebot.io/forge-repository/types'
import { Credentials } from '@typebot.io/schemas'
type Props = {
credentialsId: string
blockDef: ForgedBlockDefinition
onUpdate: () => void
}
export const UpdateForgedCredentialsModalContent = ({
credentialsId,
blockDef,
onUpdate,
}: Props) => {
const { workspace } = useWorkspace()
const { showToast } = useToast()
const [name, setName] = useState('')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [data, setData] = useState<any>()
const [isUpdating, setIsUpdating] = useState(false)
const { data: existingCredentials } =
trpc.credentials.getCredentials.useQuery(
{
workspaceId: workspace?.id as string,
credentialsId,
},
{
enabled: !!workspace?.id,
}
)
useEffect(() => {
if (!existingCredentials || data) return
setName(existingCredentials.name)
setData(existingCredentials.data)
}, [data, existingCredentials])
const { mutate } = trpc.credentials.updateCredentials.useMutation({
onMutate: () => setIsUpdating(true),
onSettled: () => setIsUpdating(false),
onError: (err) => {
showToast({
description: err.message,
status: 'error',
})
},
onSuccess: () => {
onUpdate()
},
})
const updateCredentials = async (e: React.FormEvent) => {
e.preventDefault()
if (!workspace || !blockDef.auth) return
mutate({
credentialsId,
credentials: {
type: blockDef.id,
workspaceId: workspace.id,
name,
data,
} as Credentials,
})
}
if (!blockDef.auth) return null
return (
<ModalContent>
<ModalHeader>Update {blockDef.auth.name}</ModalHeader>
<ModalCloseButton />
<form onSubmit={updateCredentials}>
<ModalBody as={Stack} spacing="6">
<TextInput
isRequired
label="Name"
defaultValue={name}
onChange={setName}
placeholder="My account"
withVariableButton={false}
debounceTimeout={0}
/>
<ZodObjectLayout
schema={blockDef.auth.schema}
data={data}
onDataChange={setData}
/>
</ModalBody>
<ModalFooter>
<Button
type="submit"
isLoading={isUpdating}
isDisabled={!data || Object.keys(data).length === 0}
colorScheme="blue"
>
Update
</Button>
</ModalFooter>
</form>
</ModalContent>
)
}

View File

@ -64,6 +64,21 @@ export const WhatsAppCredentialsModal = ({
onClose, onClose,
onNewCredentials, onNewCredentials,
}: Props) => { }: Props) => {
return (
<Modal isOpen={isOpen} onClose={onClose} size="3xl">
<ModalOverlay />
<WhatsAppCreateModalContent
onNewCredentials={onNewCredentials}
onClose={onClose}
/>
</Modal>
)
}
export const WhatsAppCreateModalContent = ({
onNewCredentials,
onClose,
}: Pick<Props, 'onNewCredentials' | 'onClose'>) => {
const { workspace } = useWorkspace() const { workspace } = useWorkspace()
const { showToast } = useToast() const { showToast } = useToast()
const { activeStep, goToNext, goToPrevious, setActiveStep } = useSteps({ const { activeStep, goToNext, goToPrevious, setActiveStep } = useSteps({
@ -226,82 +241,78 @@ export const WhatsAppCredentialsModal = ({
goToNext() goToNext()
} }
return ( return (
<Modal isOpen={isOpen} onClose={onClose} size="3xl"> <ModalContent>
<ModalOverlay /> <ModalHeader>
<ModalContent> <HStack h="40px">
<ModalHeader> {activeStep > 0 && (
<HStack h="40px"> <IconButton
{activeStep > 0 && ( icon={<ChevronLeftIcon />}
<IconButton aria-label={'Go back'}
icon={<ChevronLeftIcon />} variant="ghost"
aria-label={'Go back'} onClick={goToPrevious}
variant="ghost" />
onClick={goToPrevious} )}
/> <Heading size="md">Add a WhatsApp phone number</Heading>
)} </HStack>
<Heading size="md">Add a WhatsApp phone number</Heading> </ModalHeader>
</HStack> <ModalCloseButton />
</ModalHeader> <ModalBody as={Stack} spacing="10">
<ModalCloseButton /> <Stepper index={activeStep} size="sm" pt="4">
<ModalBody as={Stack} spacing="10"> {steps.map((step, index) => (
<Stepper index={activeStep} size="sm" pt="4"> <Step key={index}>
{steps.map((step, index) => ( <StepIndicator>
<Step key={index}> <StepStatus
<StepIndicator> complete={<StepIcon />}
<StepStatus incomplete={<StepNumber />}
complete={<StepIcon />} active={<StepNumber />}
incomplete={<StepNumber />} />
active={<StepNumber />} </StepIndicator>
/>
</StepIndicator>
<Box flexShrink="0"> <Box flexShrink="0">
<StepTitle>{step.title}</StepTitle> <StepTitle>{step.title}</StepTitle>
</Box> </Box>
<StepSeparator /> <StepSeparator />
</Step> </Step>
))} ))}
</Stepper> </Stepper>
{activeStep === 0 && <Requirements />} {activeStep === 0 && <Requirements />}
{activeStep === 1 && ( {activeStep === 1 && (
<SystemUserToken <SystemUserToken
initialToken={systemUserAccessToken} initialToken={systemUserAccessToken}
setToken={setSystemUserAccessToken} setToken={setSystemUserAccessToken}
/> />
)} )}
{activeStep === 2 && ( {activeStep === 2 && (
<PhoneNumber <PhoneNumber
appId={tokenInfoData?.appId} appId={tokenInfoData?.appId}
initialPhoneNumberId={phoneNumberId} initialPhoneNumberId={phoneNumberId}
setPhoneNumberId={setPhoneNumberId} setPhoneNumberId={setPhoneNumberId}
/> />
)} )}
{activeStep === 3 && ( {activeStep === 3 && (
<Webhook <Webhook
appId={tokenInfoData?.appId} appId={tokenInfoData?.appId}
verificationToken={verificationToken} verificationToken={verificationToken}
credentialsId={credentialsId} credentialsId={credentialsId}
/> />
)} )}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button <Button
onClick={goToNextStep} onClick={goToNextStep}
colorScheme="blue" colorScheme="blue"
isDisabled={ isDisabled={
(activeStep === 1 && isEmpty(systemUserAccessToken)) || (activeStep === 1 && isEmpty(systemUserAccessToken)) ||
(activeStep === 2 && isEmpty(phoneNumberId)) (activeStep === 2 && isEmpty(phoneNumberId))
} }
isLoading={isVerifying || isCreating} isLoading={isVerifying || isCreating}
> >
{activeStep === steps.length - 1 ? 'Submit' : 'Continue'} {activeStep === steps.length - 1 ? 'Submit' : 'Continue'}
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
</Modal>
) )
} }

View File

@ -13,6 +13,7 @@ import {
HardDriveIcon, HardDriveIcon,
SettingsIcon, SettingsIcon,
UsersIcon, UsersIcon,
WalletIcon,
} from '@/components/icons' } from '@/components/icons'
import { EmojiOrImageIcon } from '@/components/EmojiOrImageIcon' import { EmojiOrImageIcon } from '@/components/EmojiOrImageIcon'
import { User, WorkspaceRole } from '@typebot.io/prisma' import { User, WorkspaceRole } from '@typebot.io/prisma'
@ -26,11 +27,13 @@ import { MyAccountForm } from '@/features/account/components/MyAccountForm'
import { BillingSettingsLayout } from '@/features/billing/components/BillingSettingsLayout' import { BillingSettingsLayout } from '@/features/billing/components/BillingSettingsLayout'
import { useTranslate } from '@tolgee/react' import { useTranslate } from '@tolgee/react'
import { useParentModal } from '@/features/graph/providers/ParentModalProvider' import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
import { CredentialsSettingsForm } from '@/features/credentials/components/CredentialsSettingsForm'
type Props = { type Props = {
isOpen: boolean isOpen: boolean
user: User user: User
workspace: WorkspaceInApp workspace: WorkspaceInApp
defaultTab?: SettingsTab
onClose: () => void onClose: () => void
} }
@ -40,17 +43,19 @@ type SettingsTab =
| 'workspace-settings' | 'workspace-settings'
| 'members' | 'members'
| 'billing' | 'billing'
| 'credentials'
export const WorkspaceSettingsModal = ({ export const WorkspaceSettingsModal = ({
isOpen, isOpen,
user, user,
workspace, workspace,
defaultTab = 'my-account',
onClose, onClose,
}: Props) => { }: Props) => {
const { t } = useTranslate() const { t } = useTranslate()
const { ref } = useParentModal() const { ref } = useParentModal()
const { currentRole } = useWorkspace() const { currentRole } = useWorkspace()
const [selectedTab, setSelectedTab] = useState<SettingsTab>('my-account') const [selectedTab, setSelectedTab] = useState<SettingsTab>(defaultTab)
const canEditWorkspace = currentRole === WorkspaceRole.ADMIN const canEditWorkspace = currentRole === WorkspaceRole.ADMIN
@ -121,6 +126,18 @@ export const WorkspaceSettingsModal = ({
{t('workspace.settings.modal.menu.settings.label')} {t('workspace.settings.modal.menu.settings.label')}
</Button> </Button>
)} )}
{canEditWorkspace && (
<Button
variant={selectedTab === 'credentials' ? 'solid' : 'ghost'}
onClick={() => setSelectedTab('credentials')}
leftIcon={<WalletIcon />}
size="sm"
justifyContent="flex-start"
pl="4"
>
Credentials
</Button>
)}
{currentRole !== WorkspaceRole.GUEST && ( {currentRole !== WorkspaceRole.GUEST && (
<Button <Button
variant={selectedTab === 'members' ? 'solid' : 'ghost'} variant={selectedTab === 'members' ? 'solid' : 'ghost'}
@ -186,6 +203,8 @@ const SettingsContent = ({
return <MembersList /> return <MembersList />
case 'billing': case 'billing':
return <BillingSettingsLayout /> return <BillingSettingsLayout />
case 'credentials':
return <CredentialsSettingsForm />
default: default:
return null return null
} }

View File

@ -16,7 +16,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (!state) return badRequest(res) if (!state) return badRequest(res)
const { typebotId, redirectUrl, blockId, workspaceId } = JSON.parse( const { typebotId, redirectUrl, blockId, workspaceId } = JSON.parse(
Buffer.from(state, 'base64').toString() Buffer.from(state, 'base64').toString()
) ) as {
redirectUrl: string
workspaceId: string
typebotId?: string
blockId?: string
}
if (req.method === 'GET') { if (req.method === 'GET') {
const code = req.query.code as string | undefined const code = req.query.code as string | undefined
if (!workspaceId) return badRequest(res) if (!workspaceId) return badRequest(res)
@ -55,6 +60,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { id: credentialsId } = await prisma.credentials.create({ const { id: credentialsId } = await prisma.credentials.create({
data: credentials, data: credentials,
}) })
if (!typebotId) return res.redirect(`${redirectUrl.split('?')[0]}`)
const typebot = await prisma.typebot.findFirst({ const typebot = await prisma.typebot.findFirst({
where: { where: {
id: typebotId, id: typebotId,

View File

@ -1,31 +1,24 @@
{ {
"id": "qujHPjZ44xbrHb1hS1d8qC", "version": "6",
"createdAt": "2022-02-05T06:21:16.522Z", "id": "clyoe5iot0001grw96sdkhsfo",
"updatedAt": "2022-02-05T06:21:16.522Z",
"name": "My typebot", "name": "My typebot",
"folderId": null, "events": [
"groups": [
{ {
"id": "k6kY6gwRE6noPoYQNGzgUq", "id": "k6kY6gwRE6noPoYQNGzgUq",
"blocks": [ "outgoingEdgeId": "oNvqaqNExdSH2kKEhKZHuE",
{ "graphCoordinates": { "x": 0, "y": 0 },
"id": "22HP69iipkLjJDTUcc1AWW", "type": "start"
"type": "start", }
"label": "Start", ],
"groupId": "k6kY6gwRE6noPoYQNGzgUq", "groups": [
"outgoingEdgeId": "oNvqaqNExdSH2kKEhKZHuE"
}
],
"title": "Start",
"graphCoordinates": { "x": 0, "y": 0 }
},
{ {
"id": "kinRXxYop2X4d7F9qt8WNB", "id": "kinRXxYop2X4d7F9qt8WNB",
"title": "Welcome",
"graphCoordinates": { "x": 1, "y": 148 },
"blocks": [ "blocks": [
{ {
"id": "sc1y8VwDabNJgiVTBi4qtif", "id": "sc1y8VwDabNJgiVTBi4qtif",
"type": "text", "type": "text",
"groupId": "kinRXxYop2X4d7F9qt8WNB",
"content": { "content": {
"richText": [ "richText": [
{ {
@ -42,37 +35,27 @@
{ {
"id": "s7YqZTBeyCa4Hp3wN2j922c", "id": "s7YqZTBeyCa4Hp3wN2j922c",
"type": "image", "type": "image",
"groupId": "kinRXxYop2X4d7F9qt8WNB",
"content": { "content": {
"url": "https://media2.giphy.com/media/XD9o33QG9BoMis7iM4/giphy.gif?cid=fe3852a3ihg8rvipzzky5lybmdyq38fhke2tkrnshwk52c7d&rid=giphy.gif&ct=g" "url": "https://media2.giphy.com/media/XD9o33QG9BoMis7iM4/giphy.gif?cid=fe3852a3ihg8rvipzzky5lybmdyq38fhke2tkrnshwk52c7d&rid=giphy.gif&ct=g"
} }
}, },
{ {
"id": "sbjZWLJGVkHAkDqS4JQeGow", "id": "sbjZWLJGVkHAkDqS4JQeGow",
"outgoingEdgeId": "i51YhHpk1dtSyduFNf5Wim",
"type": "choice input", "type": "choice input",
"items": [ "items": [{ "id": "hQw2zbp7FDX7XYK9cFpbgC", "content": "Hi!" }],
{ "options": { "isMultipleChoice": false, "buttonLabel": "Send" }
"id": "hQw2zbp7FDX7XYK9cFpbgC",
"type": 0,
"blockId": "sbjZWLJGVkHAkDqS4JQeGow",
"content": "Hi!"
}
],
"groupId": "kinRXxYop2X4d7F9qt8WNB",
"options": { "buttonLabel": "Send", "isMultipleChoice": false },
"outgoingEdgeId": "i51YhHpk1dtSyduFNf5Wim"
} }
], ]
"title": "Welcome",
"graphCoordinates": { "x": 1, "y": 148 }
}, },
{ {
"id": "o4SH1UtKANnW5N5D67oZUz", "id": "o4SH1UtKANnW5N5D67oZUz",
"title": "Email",
"graphCoordinates": { "x": 669, "y": 141 },
"blocks": [ "blocks": [
{ {
"id": "sxeYubYN6XzhAfG7m9Fivhc", "id": "sxeYubYN6XzhAfG7m9Fivhc",
"type": "text", "type": "text",
"groupId": "o4SH1UtKANnW5N5D67oZUz",
"content": { "content": {
"richText": [ "richText": [
{ {
@ -85,7 +68,6 @@
{ {
"id": "scQ5kduafAtfP9T8SHUJnGi", "id": "scQ5kduafAtfP9T8SHUJnGi",
"type": "text", "type": "text",
"groupId": "o4SH1UtKANnW5N5D67oZUz",
"content": { "content": {
"richText": [ "richText": [
{ {
@ -99,25 +81,23 @@
}, },
{ {
"id": "snbsad18Bgry8yZ8DZCfdFD", "id": "snbsad18Bgry8yZ8DZCfdFD",
"outgoingEdgeId": "w3MiN1Ct38jT5NykVsgmb5",
"type": "email input", "type": "email input",
"groupId": "o4SH1UtKANnW5N5D67oZUz",
"options": { "options": {
"labels": { "button": "Send", "placeholder": "Type your email..." }, "variableId": "3VFChNVSCXQ2rXv4DrJ8Ah",
"variableId": "3VFChNVSCXQ2rXv4DrJ8Ah" "labels": { "placeholder": "Type your email...", "button": "Send" }
}, }
"outgoingEdgeId": "w3MiN1Ct38jT5NykVsgmb5"
} }
], ]
"title": "Email",
"graphCoordinates": { "x": 669, "y": 141 }
}, },
{ {
"id": "q5dAhqSTCaNdiGSJm9B9Rw", "id": "q5dAhqSTCaNdiGSJm9B9Rw",
"title": "Name",
"graphCoordinates": { "x": 340, "y": 143 },
"blocks": [ "blocks": [
{ {
"id": "sgtE2Sy7cKykac9B223Kq9R", "id": "sgtE2Sy7cKykac9B223Kq9R",
"type": "text", "type": "text",
"groupId": "q5dAhqSTCaNdiGSJm9B9Rw",
"content": { "content": {
"richText": [ "richText": [
{ "type": "p", "children": [{ "text": "What's your name?" }] } { "type": "p", "children": [{ "text": "What's your name?" }] }
@ -126,29 +106,27 @@
}, },
{ {
"id": "sqEsMo747LTDnY9FjQcEwUv", "id": "sqEsMo747LTDnY9FjQcEwUv",
"outgoingEdgeId": "4tYbERpi5Po4goVgt6rWXg",
"type": "text input", "type": "text input",
"groupId": "q5dAhqSTCaNdiGSJm9B9Rw",
"options": { "options": {
"isLong": false,
"labels": { "labels": {
"button": "Send", "placeholder": "Type your answer...",
"placeholder": "Type your answer..." "button": "Send"
}, },
"variableId": "giiLFGw5xXBCHzvp1qAbdX" "variableId": "giiLFGw5xXBCHzvp1qAbdX",
}, "isLong": false
"outgoingEdgeId": "4tYbERpi5Po4goVgt6rWXg" }
} }
], ]
"title": "Name",
"graphCoordinates": { "x": 340, "y": 143 }
}, },
{ {
"id": "fKqRz7iswk7ULaj5PJocZL", "id": "fKqRz7iswk7ULaj5PJocZL",
"title": "Services",
"graphCoordinates": { "x": 1002, "y": 144 },
"blocks": [ "blocks": [
{ {
"id": "su7HceVXWyTCzi2vv3m4QbK", "id": "su7HceVXWyTCzi2vv3m4QbK",
"type": "text", "type": "text",
"groupId": "fKqRz7iswk7ULaj5PJocZL",
"content": { "content": {
"richText": [ "richText": [
{ {
@ -160,48 +138,26 @@
}, },
{ {
"id": "s5VQGsVF4hQgziQsXVdwPDW", "id": "s5VQGsVF4hQgziQsXVdwPDW",
"outgoingEdgeId": "ohTRakmcYJ7GdFWRZrWRjk",
"type": "choice input", "type": "choice input",
"items": [ "items": [
{ { "id": "fnLCBF4NdraSwcubnBhk8H", "content": "Website dev" },
"id": "fnLCBF4NdraSwcubnBhk8H", { "id": "a782h8ynMouY84QjH7XSnR", "content": "Content Marketing" },
"type": 0, { "id": "jGvh94zBByvVFpSS3w97zY", "content": "Social Media" },
"blockId": "s5VQGsVF4hQgziQsXVdwPDW", { "id": "6PRLbKUezuFmwWtLVbvAQ7", "content": "UI / UX Design" }
"content": "Website dev"
},
{
"id": "a782h8ynMouY84QjH7XSnR",
"type": 0,
"blockId": "s5VQGsVF4hQgziQsXVdwPDW",
"content": "Content Marketing"
},
{
"id": "jGvh94zBByvVFpSS3w97zY",
"type": 0,
"blockId": "s5VQGsVF4hQgziQsXVdwPDW",
"content": "Social Media"
},
{
"id": "6PRLbKUezuFmwWtLVbvAQ7",
"type": 0,
"blockId": "s5VQGsVF4hQgziQsXVdwPDW",
"content": "UI / UX Design"
}
], ],
"groupId": "fKqRz7iswk7ULaj5PJocZL", "options": { "isMultipleChoice": true, "buttonLabel": "Send" }
"options": { "buttonLabel": "Send", "isMultipleChoice": true },
"outgoingEdgeId": "ohTRakmcYJ7GdFWRZrWRjk"
} }
], ]
"title": "Services",
"graphCoordinates": { "x": 1002, "y": 144 }
}, },
{ {
"id": "7qHBEyCMvKEJryBHzPmHjV", "id": "7qHBEyCMvKEJryBHzPmHjV",
"title": "Additional information",
"graphCoordinates": { "x": 1337, "y": 145 },
"blocks": [ "blocks": [
{ {
"id": "sqR8Sz9gW21aUYKtUikq7qZ", "id": "sqR8Sz9gW21aUYKtUikq7qZ",
"type": "text", "type": "text",
"groupId": "7qHBEyCMvKEJryBHzPmHjV",
"content": { "content": {
"richText": [ "richText": [
{ {
@ -215,33 +171,34 @@
}, },
{ {
"id": "sqFy2G3C1mh9p6s3QBdSS5x", "id": "sqFy2G3C1mh9p6s3QBdSS5x",
"outgoingEdgeId": "sH5nUssG2XQbm6ZidGv9BY",
"type": "text input", "type": "text input",
"groupId": "7qHBEyCMvKEJryBHzPmHjV",
"options": { "options": {
"isLong": true, "labels": {
"labels": { "button": "Send", "placeholder": "Type your answer..." } "placeholder": "Type your answer...",
}, "button": "Send"
"outgoingEdgeId": "sH5nUssG2XQbm6ZidGv9BY" },
"isLong": true
}
} }
], ]
"title": "Additional information",
"graphCoordinates": { "x": 1337, "y": 145 }
}, },
{ {
"id": "vF7AD7zSAj7SNvN3gr9N94", "id": "vF7AD7zSAj7SNvN3gr9N94",
"title": "Bye",
"graphCoordinates": { "x": 1668, "y": 143 },
"blocks": [ "blocks": [
{ {
"id": "seLegenCgUwMopRFeAefqZ7", "id": "seLegenCgUwMopRFeAefqZ7",
"type": "text", "type": "text",
"groupId": "vF7AD7zSAj7SNvN3gr9N94",
"content": { "content": {
"richText": [{ "type": "p", "children": [{ "text": "Perfect!" }] }] "richText": [{ "type": "p", "children": [{ "text": "Perfect!" }] }]
} }
}, },
{ {
"id": "s779Q1y51aVaDUJVrFb16vv", "id": "s779Q1y51aVaDUJVrFb16vv",
"outgoingEdgeId": "fTVo43AG97eKcaTrZf9KyV",
"type": "text", "type": "text",
"groupId": "vF7AD7zSAj7SNvN3gr9N94",
"content": { "content": {
"richText": [ "richText": [
{ {
@ -249,110 +206,107 @@
"children": [{ "text": "We'll get back to you at {{Email}}" }] "children": [{ "text": "We'll get back to you at {{Email}}" }]
} }
] ]
}, }
"outgoingEdgeId": "fTVo43AG97eKcaTrZf9KyV"
} }
], ]
"title": "Bye",
"graphCoordinates": { "x": 1668, "y": 143 }
}, },
{ {
"id": "webhookGroup", "id": "webhookGroup",
"graphCoordinates": { "x": 1996, "y": 134 },
"title": "Webhook", "title": "Webhook",
"graphCoordinates": { "x": 1996, "y": 134 },
"blocks": [ "blocks": [
{ {
"id": "webhookBlock", "id": "webhookBlock",
"groupId": "webhookGroup",
"type": "Webhook", "type": "Webhook",
"options": { "responseVariableMapping": [], "variablesForTest": [] }, "options": {
"webhookId": "webhook1" "variablesForTest": [],
"responseVariableMapping": [],
"webhook": { "method": "POST" }
}
} }
] ]
} }
], ],
"variables": [
{ "id": "giiLFGw5xXBCHzvp1qAbdX", "name": "Name" },
{ "id": "3VFChNVSCXQ2rXv4DrJ8Ah", "name": "Email" }
],
"edges": [ "edges": [
{ {
"id": "oNvqaqNExdSH2kKEhKZHuE", "id": "oNvqaqNExdSH2kKEhKZHuE",
"to": { "groupId": "kinRXxYop2X4d7F9qt8WNB" }, "from": { "eventId": "k6kY6gwRE6noPoYQNGzgUq" },
"from": { "to": { "groupId": "kinRXxYop2X4d7F9qt8WNB" }
"blockId": "22HP69iipkLjJDTUcc1AWW",
"groupId": "k6kY6gwRE6noPoYQNGzgUq"
}
}, },
{ {
"id": "i51YhHpk1dtSyduFNf5Wim", "id": "i51YhHpk1dtSyduFNf5Wim",
"to": { "groupId": "q5dAhqSTCaNdiGSJm9B9Rw" }, "from": { "blockId": "sbjZWLJGVkHAkDqS4JQeGow" },
"from": { "to": { "groupId": "q5dAhqSTCaNdiGSJm9B9Rw" }
"blockId": "sbjZWLJGVkHAkDqS4JQeGow",
"groupId": "kinRXxYop2X4d7F9qt8WNB"
}
}, },
{ {
"id": "4tYbERpi5Po4goVgt6rWXg", "id": "4tYbERpi5Po4goVgt6rWXg",
"to": { "groupId": "o4SH1UtKANnW5N5D67oZUz" }, "from": { "blockId": "sqEsMo747LTDnY9FjQcEwUv" },
"from": { "to": { "groupId": "o4SH1UtKANnW5N5D67oZUz" }
"blockId": "sqEsMo747LTDnY9FjQcEwUv",
"groupId": "q5dAhqSTCaNdiGSJm9B9Rw"
}
}, },
{ {
"id": "w3MiN1Ct38jT5NykVsgmb5", "id": "w3MiN1Ct38jT5NykVsgmb5",
"to": { "groupId": "fKqRz7iswk7ULaj5PJocZL" }, "from": { "blockId": "snbsad18Bgry8yZ8DZCfdFD" },
"from": { "to": { "groupId": "fKqRz7iswk7ULaj5PJocZL" }
"blockId": "snbsad18Bgry8yZ8DZCfdFD",
"groupId": "o4SH1UtKANnW5N5D67oZUz"
}
}, },
{ {
"id": "ohTRakmcYJ7GdFWRZrWRjk", "id": "ohTRakmcYJ7GdFWRZrWRjk",
"to": { "groupId": "7qHBEyCMvKEJryBHzPmHjV" }, "from": { "blockId": "s5VQGsVF4hQgziQsXVdwPDW" },
"from": { "to": { "groupId": "7qHBEyCMvKEJryBHzPmHjV" }
"blockId": "s5VQGsVF4hQgziQsXVdwPDW",
"groupId": "fKqRz7iswk7ULaj5PJocZL"
}
}, },
{ {
"id": "sH5nUssG2XQbm6ZidGv9BY", "id": "sH5nUssG2XQbm6ZidGv9BY",
"to": { "groupId": "vF7AD7zSAj7SNvN3gr9N94" }, "from": { "blockId": "sqFy2G3C1mh9p6s3QBdSS5x" },
"from": { "to": { "groupId": "vF7AD7zSAj7SNvN3gr9N94" }
"blockId": "sqFy2G3C1mh9p6s3QBdSS5x",
"groupId": "7qHBEyCMvKEJryBHzPmHjV"
}
}, },
{ {
"from": { "id": "fTVo43AG97eKcaTrZf9KyV",
"groupId": "vF7AD7zSAj7SNvN3gr9N94", "from": { "blockId": "s779Q1y51aVaDUJVrFb16vv" },
"blockId": "s779Q1y51aVaDUJVrFb16vv" "to": { "groupId": "webhookGroup" }
}, }
"to": { "groupId": "webhookGroup" }, ],
"id": "fTVo43AG97eKcaTrZf9KyV" "variables": [
{
"id": "giiLFGw5xXBCHzvp1qAbdX",
"name": "Name",
"isSessionVariable": true
},
{
"id": "3VFChNVSCXQ2rXv4DrJ8Ah",
"name": "Email",
"isSessionVariable": true
} }
], ],
"theme": { "theme": {
"general": { "font": "Open Sans", "background": { "type": "None" } },
"chat": { "chat": {
"hostBubbles": { "backgroundColor": "#F7F8FF", "color": "#303235" },
"guestBubbles": { "backgroundColor": "#FF8E21", "color": "#FFFFFF" },
"buttons": { "backgroundColor": "#0042DA", "color": "#FFFFFF" },
"inputs": { "inputs": {
"color": "#303235",
"backgroundColor": "#FFFFFF", "backgroundColor": "#FFFFFF",
"color": "#303235",
"placeholderColor": "#9095A0" "placeholderColor": "#9095A0"
}, }
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" }, }
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
},
"general": { "font": "Open Sans", "background": { "type": "None" } }
}, },
"selectedThemeTemplateId": null,
"settings": { "settings": {
"general": { "isBrandingEnabled": true }, "general": { "isBrandingEnabled": true },
"typingEmulation": { "enabled": true, "speed": 300, "maxDelay": 1.5 },
"metadata": { "metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form." "description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
}, }
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
}, },
"createdAt": "2024-07-16T12:30:05.790Z",
"updatedAt": "2024-07-16T12:30:05.790Z",
"icon": null,
"folderId": null,
"publicId": null, "publicId": null,
"customDomain": null "customDomain": null,
"workspaceId": "proWorkspace",
"resultsTablePreferences": null,
"isArchived": false,
"isClosed": false,
"whatsAppCredentialsId": null,
"riskLevel": null
} }

View File

@ -118,8 +118,7 @@
"variablesForTest": [], "variablesForTest": [],
"isAdvancedConfig": false, "isAdvancedConfig": false,
"isCustomBody": false "isCustomBody": false
}, }
"webhookId": "webhook1"
} }
] ]
} }

View File

@ -1,32 +1,23 @@
{ {
"id": "ckz8gli9e9842no1afuppdn0z", "version": "6",
"createdAt": "2022-02-04T13:44:30.386Z", "id": "clyoe6owl0003grw9k9hzc9qs",
"updatedAt": "2022-02-04T13:44:30.386Z",
"name": "My typebot", "name": "My typebot",
"folderId": null, "events": [
"groups": [
{ {
"id": "p6GeeRXHgwiJeoJRBkKaMJ", "id": "p6GeeRXHgwiJeoJRBkKaMJ",
"blocks": [ "outgoingEdgeId": "cyEJPaLU7AchnBSaeWoyiS",
{ "graphCoordinates": { "x": 0, "y": 0 },
"id": "iDS7jFemUsQ7Sp3eu3xg3w", "type": "start"
"type": "start", }
"label": "Start", ],
"groupId": "p6GeeRXHgwiJeoJRBkKaMJ", "groups": [
"outgoingEdgeId": "cyEJPaLU7AchnBSaeWoyiS"
}
],
"title": "Start",
"graphCoordinates": { "x": 0, "y": 0 }
},
{ {
"id": "kBneEpKdMYrF65XxUQ5GS7", "id": "kBneEpKdMYrF65XxUQ5GS7",
"graphCoordinates": { "x": 260, "y": 186 },
"title": "Group #1", "title": "Group #1",
"graphCoordinates": { "x": 260, "y": 186 },
"blocks": [ "blocks": [
{ {
"id": "skSkZ4PNP7m1gYvu9Ew6ngM", "id": "skSkZ4PNP7m1gYvu9Ew6ngM",
"groupId": "kBneEpKdMYrF65XxUQ5GS7",
"type": "text", "type": "text",
"content": { "content": {
"richText": [{ "type": "p", "children": [{ "text": "Ready?" }] }] "richText": [{ "type": "p", "children": [{ "text": "Ready?" }] }]
@ -34,85 +25,89 @@
}, },
{ {
"id": "sh6ZVRA3o72y6BEiNKVcoma", "id": "sh6ZVRA3o72y6BEiNKVcoma",
"groupId": "kBneEpKdMYrF65XxUQ5GS7",
"type": "choice input", "type": "choice input",
"options": { "buttonLabel": "Send", "isMultipleChoice": false },
"items": [ "items": [
{ {
"id": "rr5mKKBPq73ZrfXZ3uuupz", "id": "rr5mKKBPq73ZrfXZ3uuupz",
"blockId": "sh6ZVRA3o72y6BEiNKVcoma", "outgoingEdgeId": "1sLicz8gq2QxytFTwBd8ac",
"type": 0, "content": "Go"
"content": "Go",
"outgoingEdgeId": "1sLicz8gq2QxytFTwBd8ac"
} }
] ],
"options": { "isMultipleChoice": false, "buttonLabel": "Send" }
} }
] ]
}, },
{ {
"id": "8XnDM1QsqPms4LQHh8q3Jo", "id": "8XnDM1QsqPms4LQHh8q3Jo",
"graphCoordinates": { "x": 646, "y": 511 },
"title": "Group #2", "title": "Group #2",
"graphCoordinates": { "x": 646, "y": 511 },
"blocks": [ "blocks": [
{ {
"id": "soSmiE7zyb3WF77GxFxAjYX", "id": "soSmiE7zyb3WF77GxFxAjYX",
"groupId": "8XnDM1QsqPms4LQHh8q3Jo",
"type": "Webhook", "type": "Webhook",
"options": { "options": {
"responseVariableMapping": [],
"variablesForTest": [], "variablesForTest": [],
"responseVariableMapping": [],
"isAdvancedConfig": false, "isAdvancedConfig": false,
"isCustomBody": false "isCustomBody": false,
}, "webhook": { "method": "POST" }
"webhookId": "webhook1" }
} }
] ]
} }
], ],
"edges": [
{
"id": "cyEJPaLU7AchnBSaeWoyiS",
"from": { "eventId": "p6GeeRXHgwiJeoJRBkKaMJ" },
"to": { "groupId": "kBneEpKdMYrF65XxUQ5GS7" }
},
{
"id": "1sLicz8gq2QxytFTwBd8ac",
"from": {
"blockId": "sh6ZVRA3o72y6BEiNKVcoma",
"itemId": "rr5mKKBPq73ZrfXZ3uuupz"
},
"to": { "groupId": "8XnDM1QsqPms4LQHh8q3Jo" }
}
],
"variables": [ "variables": [
{ "id": "var1", "name": "secret 1" }, { "id": "var1", "name": "secret 1" },
{ "id": "var2", "name": "secret 2" }, { "id": "var2", "name": "secret 2" },
{ "id": "var3", "name": "secret 3" }, { "id": "var3", "name": "secret 3" },
{ "id": "var4", "name": "secret 4" } { "id": "var4", "name": "secret 4" }
], ],
"edges": [
{
"from": {
"groupId": "p6GeeRXHgwiJeoJRBkKaMJ",
"blockId": "iDS7jFemUsQ7Sp3eu3xg3w"
},
"to": { "groupId": "kBneEpKdMYrF65XxUQ5GS7" },
"id": "cyEJPaLU7AchnBSaeWoyiS"
},
{
"from": {
"groupId": "kBneEpKdMYrF65XxUQ5GS7",
"blockId": "sh6ZVRA3o72y6BEiNKVcoma",
"itemId": "rr5mKKBPq73ZrfXZ3uuupz"
},
"to": { "groupId": "8XnDM1QsqPms4LQHh8q3Jo" },
"id": "1sLicz8gq2QxytFTwBd8ac"
}
],
"theme": { "theme": {
"general": { "font": "Open Sans", "background": { "type": "None" } },
"chat": { "chat": {
"hostBubbles": { "backgroundColor": "#F7F8FF", "color": "#303235" },
"guestBubbles": { "backgroundColor": "#FF8E21", "color": "#FFFFFF" },
"buttons": { "backgroundColor": "#0042DA", "color": "#FFFFFF" },
"inputs": { "inputs": {
"color": "#303235",
"backgroundColor": "#FFFFFF", "backgroundColor": "#FFFFFF",
"color": "#303235",
"placeholderColor": "#9095A0" "placeholderColor": "#9095A0"
}, }
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" }, }
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
},
"general": { "font": "Open Sans", "background": { "type": "None" } }
}, },
"selectedThemeTemplateId": null,
"settings": { "settings": {
"general": { "isBrandingEnabled": true }, "general": { "isBrandingEnabled": true },
"typingEmulation": { "enabled": true, "speed": 300, "maxDelay": 1.5 },
"metadata": { "metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form." "description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
}, }
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
}, },
"publicId": null "createdAt": "2024-07-16T12:31:00.501Z",
"updatedAt": "2024-07-16T12:31:00.501Z",
"icon": null,
"folderId": null,
"publicId": null,
"customDomain": null,
"workspaceId": "proWorkspace",
"resultsTablePreferences": null,
"isArchived": false,
"isClosed": false,
"whatsAppCredentialsId": null,
"riskLevel": null
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
src/test/reporters

View File

@ -40,7 +40,7 @@
"devDependencies": { "devDependencies": {
"@faire/mjml-react": "3.3.0", "@faire/mjml-react": "3.3.0",
"@paralleldrive/cuid2": "2.2.1", "@paralleldrive/cuid2": "2.2.1",
"@playwright/test": "1.43.1", "@playwright/test": "1.45.2",
"@typebot.io/emails": "workspace:*", "@typebot.io/emails": "workspace:*",
"@typebot.io/env": "workspace:*", "@typebot.io/env": "workspace:*",
"@typebot.io/forge": "workspace:*", "@typebot.io/forge": "workspace:*",

View File

@ -10,13 +10,13 @@ export default defineConfig({
timeout: process.env.CI ? 10 * 1000 : 5 * 1000, timeout: process.env.CI ? 10 * 1000 : 5 * 1000,
}, },
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
workers: process.env.CI ? 1 : 3, workers: process.env.CI ? 1 : 4,
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 1,
reporter: [ reporter: [
[process.env.CI ? 'github' : 'list'], [process.env.CI ? 'github' : 'list'],
['html', { outputFolder: 'src/test/reporters' }], ['html', { outputFolder: 'src/test/reporters' }],
], ],
maxFailures: process.env.CI ? 10 : undefined, maxFailures: 10,
webServer: process.env.CI webServer: process.env.CI
? { ? {
command: 'pnpm run start', command: 'pnpm run start',

View File

@ -1,28 +1,20 @@
{ {
"id": "chat-sub-bot", "version": "6",
"createdAt": "2022-11-24T09:06:52.903Z", "id": "clyoehfmp0007grw9ubdop6u0",
"updatedAt": "2022-11-24T09:13:16.782Z",
"icon": "👶",
"name": "Sub bot", "name": "Sub bot",
"folderId": null, "events": [
"groups": [
{ {
"id": "clauup2lh0002vs1a5ei32mmi", "id": "clauup2lh0002vs1a5ei32mmi",
"title": "Start", "outgoingEdgeId": "clauupl9n001b3b6qdk4czgom",
"blocks": [ "graphCoordinates": { "x": 0, "y": 0 },
{ "type": "start"
"id": "clauup2li0003vs1aas14fwpc", }
"type": "start", ],
"label": "Start", "groups": [
"groupId": "clauup2lh0002vs1a5ei32mmi",
"outgoingEdgeId": "clauupl9n001b3b6qdk4czgom"
}
],
"graphCoordinates": { "x": 0, "y": 0 }
},
{ {
"id": "clauupd6q00183b6qcm8qbz62", "id": "clauupd6q00183b6qcm8qbz62",
"title": "Group #1", "title": "Group #1",
"graphCoordinates": { "x": 375.36328125, "y": 167.2578125 },
"blocks": [ "blocks": [
{ {
"id": "clauupd6q00193b6qhegmlnxj", "id": "clauupd6q00193b6qhegmlnxj",
@ -36,69 +28,69 @@
] ]
} }
] ]
}, }
"groupId": "clauupd6q00183b6qcm8qbz62"
}, },
{ {
"id": "clauupk97001a3b6q2w9qqkec", "id": "clauupk97001a3b6q2w9qqkec",
"type": "rating input", "type": "rating input",
"groupId": "clauupd6q00183b6qcm8qbz62",
"options": { "options": {
"labels": { "button": "Send" },
"length": 10,
"buttonType": "Numbers", "buttonType": "Numbers",
"length": 10,
"labels": { "button": "Send" },
"customIcon": { "isEnabled": false } "customIcon": { "isEnabled": false }
} }
} }
], ]
"graphCoordinates": { "x": 375.36328125, "y": 167.2578125 }
} }
], ],
"variables": [],
"edges": [ "edges": [
{ {
"id": "clauupl9n001b3b6qdk4czgom", "id": "clauupl9n001b3b6qdk4czgom",
"to": { "groupId": "clauupd6q00183b6qcm8qbz62" }, "from": { "eventId": "clauup2lh0002vs1a5ei32mmi" },
"from": { "to": { "groupId": "clauupd6q00183b6qcm8qbz62" }
"blockId": "clauup2li0003vs1aas14fwpc",
"groupId": "clauup2lh0002vs1a5ei32mmi"
}
} }
], ],
"variables": [],
"theme": { "theme": {
"general": { "font": "Open Sans", "background": { "type": "None" } },
"chat": { "chat": {
"inputs": {
"color": "#303235",
"backgroundColor": "#FFFFFF",
"placeholderColor": "#9095A0"
},
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
"hostAvatar": { "hostAvatar": {
"url": "https://avatars.githubusercontent.com/u/16015833?v=4", "isEnabled": true,
"isEnabled": true "url": "https://avatars.githubusercontent.com/u/16015833?v=4"
}, },
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" }, "hostBubbles": { "backgroundColor": "#F7F8FF", "color": "#303235" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" } "guestBubbles": { "backgroundColor": "#FF8E21", "color": "#FFFFFF" },
}, "buttons": { "backgroundColor": "#0042DA", "color": "#FFFFFF" },
"general": { "font": "Open Sans", "background": { "type": "None" } } "inputs": {
"backgroundColor": "#FFFFFF",
"color": "#303235",
"placeholderColor": "#9095A0"
}
}
}, },
"selectedThemeTemplateId": null,
"settings": { "settings": {
"general": { "general": {
"isBrandingEnabled": false, "isBrandingEnabled": false,
"isInputPrefillEnabled": true, "isInputPrefillEnabled": true,
"isResultSavingEnabled": true,
"isHideQueryParamsEnabled": true, "isHideQueryParamsEnabled": true,
"isNewResultOnRefreshEnabled": false "isNewResultOnRefreshEnabled": false
}, },
"typingEmulation": { "enabled": true, "speed": 300, "maxDelay": 1.5 },
"metadata": { "metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form." "description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
}, }
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
}, },
"createdAt": "2024-07-16T12:39:21.697Z",
"updatedAt": "2024-07-16T12:39:21.697Z",
"icon": "👶",
"folderId": null,
"publicId": null, "publicId": null,
"customDomain": null, "customDomain": null,
"workspaceId": "proWorkspace", "workspaceId": "proWorkspace",
"resultsTablePreferences": null, "resultsTablePreferences": null,
"isArchived": false, "isArchived": false,
"isClosed": false "isClosed": false,
"whatsAppCredentialsId": null,
"riskLevel": null
} }

View File

@ -1,28 +1,20 @@
{ {
"id": "clauujawp00011avs2vj97zma", "version": "6",
"createdAt": "2022-11-24T09:02:23.737Z", "id": "clyoegjca0005grw9ek6h984v",
"updatedAt": "2022-11-24T09:12:57.036Z",
"icon": "🤖",
"name": "Complete bot", "name": "Complete bot",
"folderId": null, "events": [
"groups": [
{ {
"id": "clauujawn0000vs1a8z6k2k7d", "id": "clauujawn0000vs1a8z6k2k7d",
"title": "Start", "outgoingEdgeId": "clauuk4o300083b6q7b2iowv3",
"blocks": [ "graphCoordinates": { "x": 0, "y": 0 },
{ "type": "start"
"id": "clauujawn0001vs1a0mk8docp", }
"type": "start", ],
"label": "Start", "groups": [
"groupId": "clauujawn0000vs1a8z6k2k7d",
"outgoingEdgeId": "clauuk4o300083b6q7b2iowv3"
}
],
"graphCoordinates": { "x": 0, "y": 0 }
},
{ {
"id": "clauujxdc00063b6q42ca20gj", "id": "clauujxdc00063b6q42ca20gj",
"title": "Welcome", "title": "Welcome",
"graphCoordinates": { "x": 5.81640625, "y": 172.359375 },
"blocks": [ "blocks": [
{ {
"id": "clauujxdd00073b6qpejnkzcy", "id": "clauujxdd00073b6qpejnkzcy",
@ -31,8 +23,7 @@
"richText": [ "richText": [
{ "type": "p", "children": [{ "text": "Hi there! 👋" }] } { "type": "p", "children": [{ "text": "Hi there! 👋" }] }
] ]
}, }
"groupId": "clauujxdc00063b6q42ca20gj"
}, },
{ {
"id": "clauukaad00093b6q07av51yc", "id": "clauukaad00093b6q07av51yc",
@ -44,29 +35,27 @@
"children": [{ "text": "Welcome. What's your name?" }] "children": [{ "text": "Welcome. What's your name?" }]
} }
] ]
}, }
"groupId": "clauujxdc00063b6q42ca20gj"
}, },
{ {
"id": "clauukip8000a3b6qtzl288tu", "id": "clauukip8000a3b6qtzl288tu",
"outgoingEdgeId": "clauul0sk000f3b6q2tvy5wfi",
"type": "text input", "type": "text input",
"groupId": "clauujxdc00063b6q42ca20gj",
"options": { "options": {
"isLong": false,
"labels": { "labels": {
"button": "Send", "placeholder": "Type your answer...",
"placeholder": "Type your answer..." "button": "Send"
}, },
"variableId": "vclauuklnc000b3b6q7xchq4yf" "variableId": "vclauuklnc000b3b6q7xchq4yf",
}, "isLong": false
"outgoingEdgeId": "clauul0sk000f3b6q2tvy5wfi" }
} }
], ]
"graphCoordinates": { "x": 5.81640625, "y": 172.359375 }
}, },
{ {
"id": "clauukoka000c3b6qe6chawis", "id": "clauukoka000c3b6qe6chawis",
"title": "Age", "title": "Age",
"graphCoordinates": { "x": 361.17578125, "y": 170.10546875 },
"blocks": [ "blocks": [
{ {
"id": "clauukoka000d3b6qxqi38cmk", "id": "clauukoka000d3b6qxqi38cmk",
@ -78,16 +67,14 @@
"children": [{ "text": "Nice to meet you {{Name}}" }] "children": [{ "text": "Nice to meet you {{Name}}" }]
} }
] ]
}, }
"groupId": "clauukoka000c3b6qe6chawis"
}, },
{ {
"id": "clauuku5o000e3b6q90rm30p1", "id": "clauuku5o000e3b6q90rm30p1",
"type": "image", "type": "image",
"content": { "content": {
"url": "https://media2.giphy.com/media/l0MYGb1LuZ3n7dRnO/giphy-downsized.gif?cid=fe3852a3yd2leg4yi8iual3wgyw893zzocuuqlp3wytt802h&rid=giphy-downsized.gif&ct=g" "url": "https://media2.giphy.com/media/l0MYGb1LuZ3n7dRnO/giphy-downsized.gif?cid=fe3852a3yd2leg4yi8iual3wgyw893zzocuuqlp3wytt802h&rid=giphy-downsized.gif&ct=g"
}, }
"groupId": "clauukoka000c3b6qe6chawis"
}, },
{ {
"id": "clauul4vg000g3b6qr0q2h0uy", "id": "clauul4vg000g3b6qr0q2h0uy",
@ -96,60 +83,56 @@
"richText": [ "richText": [
{ "type": "p", "children": [{ "text": "How old are you?" }] } { "type": "p", "children": [{ "text": "How old are you?" }] }
] ]
}, }
"groupId": "clauukoka000c3b6qe6chawis"
}, },
{ {
"id": "clauul90j000h3b6qjfrw9js4", "id": "clauul90j000h3b6qjfrw9js4",
"outgoingEdgeId": "clauum41j000n3b6qpqu12icm",
"type": "number input", "type": "number input",
"groupId": "clauukoka000c3b6qe6chawis",
"options": { "options": {
"labels": { "button": "Send", "placeholder": "Type a number..." }, "variableId": "vclauulfjk000i3b6qmujooweu",
"variableId": "vclauulfjk000i3b6qmujooweu" "labels": { "placeholder": "Type a number...", "button": "Send" }
}, }
"outgoingEdgeId": "clauum41j000n3b6qpqu12icm"
} }
], ]
"graphCoordinates": { "x": 361.17578125, "y": 170.10546875 }
}, },
{ {
"id": "clauulhqf000j3b6qm8y5oifc", "id": "clauulhqf000j3b6qm8y5oifc",
"title": "Is major?", "title": "Is major?",
"graphCoordinates": { "x": 726.2265625, "y": 240.80078125 },
"blocks": [ "blocks": [
{ {
"id": "clauulhqf000k3b6qsrc1hd74", "id": "clauulhqf000k3b6qsrc1hd74",
"outgoingEdgeId": "clauumm5v000t3b6qu62qcft8",
"type": "Condition", "type": "Condition",
"items": [ "items": [
{ {
"id": "clauulhqg000l3b6qaxn4qli5", "id": "clauulhqg000l3b6qaxn4qli5",
"type": 1, "outgoingEdgeId": "clauumi0x000q3b6q9bwkqnmr",
"blockId": "clauulhqf000k3b6qsrc1hd74",
"content": { "content": {
"logicalOperator": "AND",
"comparisons": [ "comparisons": [
{ {
"id": "clauuliyn000m3b6q10gwx8ii", "id": "clauuliyn000m3b6q10gwx8ii",
"value": "21",
"variableId": "vclauulfjk000i3b6qmujooweu", "variableId": "vclauulfjk000i3b6qmujooweu",
"comparisonOperator": "Greater than" "comparisonOperator": "Greater than",
"value": "21"
} }
], ]
"logicalOperator": "AND" }
},
"outgoingEdgeId": "clauumi0x000q3b6q9bwkqnmr"
} }
], ]
"groupId": "clauulhqf000j3b6qm8y5oifc",
"outgoingEdgeId": "clauumm5v000t3b6qu62qcft8"
} }
], ]
"graphCoordinates": { "x": 726.2265625, "y": 240.80078125 }
}, },
{ {
"id": "clauum8x7000o3b6qx8hqduf8", "id": "clauum8x7000o3b6qx8hqduf8",
"title": "Group #4", "title": "Group #4",
"graphCoordinates": { "x": 1073.38671875, "y": 232.25 },
"blocks": [ "blocks": [
{ {
"id": "clauum8x7000p3b6qxjud5hdc", "id": "clauum8x7000p3b6qxjud5hdc",
"outgoingEdgeId": "clauuom2y000y3b6qkcjy2ri7",
"type": "text", "type": "text",
"content": { "content": {
"richText": [ "richText": [
@ -158,39 +141,35 @@
"children": [{ "text": "Ok, you are an adult then 😁" }] "children": [{ "text": "Ok, you are an adult then 😁" }]
} }
] ]
}, }
"groupId": "clauum8x7000o3b6qx8hqduf8",
"outgoingEdgeId": "clauuom2y000y3b6qkcjy2ri7"
} }
], ]
"graphCoordinates": { "x": 1073.38671875, "y": 232.25 }
}, },
{ {
"id": "clauumjq4000r3b6q8l6bi9ra", "id": "clauumjq4000r3b6q8l6bi9ra",
"title": "Group #4 copy", "title": "Group #4 copy",
"graphCoordinates": { "x": 1073.984375, "y": 408.6875 },
"blocks": [ "blocks": [
{ {
"id": "clauumjq5000s3b6qqjhrklv4", "id": "clauumjq5000s3b6qqjhrklv4",
"outgoingEdgeId": "clauuol8t000x3b6qcw1few70",
"type": "text", "type": "text",
"content": { "content": {
"richText": [ "richText": [
{ "type": "p", "children": [{ "text": "Oh, you are a kid 😁" }] } { "type": "p", "children": [{ "text": "Oh, you are a kid 😁" }] }
] ]
}, }
"groupId": "clauumjq4000r3b6q8l6bi9ra",
"outgoingEdgeId": "clauuol8t000x3b6qcw1few70"
} }
], ]
"graphCoordinates": { "x": 1073.984375, "y": 408.6875 }
}, },
{ {
"id": "clauuoekh000u3b6q6zmlx7f9", "id": "clauuoekh000u3b6q6zmlx7f9",
"title": "Magic number", "title": "Magic number",
"graphCoordinates": { "x": 1465.359375, "y": 299.25390625 },
"blocks": [ "blocks": [
{ {
"id": "clauuoeki000v3b6qvsh7kde1", "id": "clauuoeki000v3b6qvsh7kde1",
"type": "Set variable", "type": "Set variable",
"groupId": "clauuoekh000u3b6q6zmlx7f9",
"options": { "options": {
"variableId": "vclauuohyp000w3b6qbqrs6c6w", "variableId": "vclauuohyp000w3b6qbqrs6c6w",
"expressionToEvaluate": "42" "expressionToEvaluate": "42"
@ -198,6 +177,7 @@
}, },
{ {
"id": "clauuontu000z3b6q3ydx6ao1", "id": "clauuontu000z3b6q3ydx6ao1",
"outgoingEdgeId": "clauuq8je001e3b6qksm4j11g",
"type": "text", "type": "text",
"content": { "content": {
"richText": [ "richText": [
@ -206,38 +186,33 @@
"children": [{ "text": "My magic number is {{Magic number}}" }] "children": [{ "text": "My magic number is {{Magic number}}" }]
} }
] ]
}, }
"groupId": "clauuoekh000u3b6q6zmlx7f9",
"outgoingEdgeId": "clauuq8je001e3b6qksm4j11g"
} }
], ]
"graphCoordinates": { "x": 1465.359375, "y": 299.25390625 }
}, },
{ {
"id": "clauuq2l6001c3b6qpmq3ivwk", "id": "clauuq2l6001c3b6qpmq3ivwk",
"graphCoordinates": { "x": 1836.43359375, "y": 295.39453125 },
"title": "Rate the experience", "title": "Rate the experience",
"graphCoordinates": { "x": 1836.43359375, "y": 295.39453125 },
"blocks": [ "blocks": [
{ {
"id": "clauuq2l6001d3b6qyltfcvgb", "id": "clauuq2l6001d3b6qyltfcvgb",
"groupId": "clauuq2l6001c3b6qpmq3ivwk", "outgoingEdgeId": "clauureo3001h3b6qk6epabxq",
"type": "Typebot link", "type": "Typebot link",
"options": { "options": {
"typebotId": "chat-sub-bot", "typebotId": "chat-sub-bot",
"groupId": "clauupd6q00183b6qcm8qbz62" "groupId": "clauupd6q00183b6qcm8qbz62"
}, }
"outgoingEdgeId": "clauureo3001h3b6qk6epabxq"
} }
] ]
}, },
{ {
"id": "clauur7od001f3b6qq140oe55", "id": "clauur7od001f3b6qq140oe55",
"graphCoordinates": { "x": 2201.45703125, "y": 299.1328125 },
"title": "Multiple input in group", "title": "Multiple input in group",
"graphCoordinates": { "x": 2201.45703125, "y": 299.1328125 },
"blocks": [ "blocks": [
{ {
"id": "clauur7od001g3b6qkoeij3f7", "id": "clauur7od001g3b6qkoeij3f7",
"groupId": "clauur7od001f3b6qq140oe55",
"type": "text", "type": "text",
"content": { "content": {
"richText": [ "richText": [
@ -252,53 +227,39 @@
}, },
{ {
"id": "clauurluf001i3b6qjf78puug", "id": "clauurluf001i3b6qjf78puug",
"groupId": "clauur7od001f3b6qq140oe55",
"type": "email input", "type": "email input",
"options": { "options": {
"labels": { "button": "Send", "placeholder": "Type your email..." }, "labels": { "placeholder": "Type your email...", "button": "Send" },
"retryMessageContent": "This email doesn't seem to be valid. Can you type it again?" "retryMessageContent": "This email doesn't seem to be valid. Can you type it again?"
} }
}, },
{ {
"id": "clauurokp001j3b6qyrw7boca", "id": "clauurokp001j3b6qyrw7boca",
"groupId": "clauur7od001f3b6qq140oe55",
"type": "url input", "type": "url input",
"options": { "options": {
"labels": { "button": "Send", "placeholder": "Type a URL..." }, "labels": { "placeholder": "Type a URL...", "button": "Send" },
"retryMessageContent": "This URL doesn't seem to be valid. Can you type it again?" "retryMessageContent": "This URL doesn't seem to be valid. Can you type it again?"
} }
}, },
{ {
"id": "clauurs1o001k3b6qgrj0xf59", "id": "clauurs1o001k3b6qgrj0xf59",
"groupId": "clauur7od001f3b6qq140oe55", "outgoingEdgeId": "clauushy3001p3b6qqnyrxgtb",
"type": "choice input", "type": "choice input",
"options": { "buttonLabel": "Send", "isMultipleChoice": false },
"items": [ "items": [
{ { "id": "clauurs1o001l3b6qu9hr712h", "content": "Yes" },
"id": "clauurs1o001l3b6qu9hr712h", { "id": "clauuru6t001m3b6qp8vkt23l", "content": "No" }
"blockId": "clauurs1o001k3b6qgrj0xf59",
"type": 0,
"content": "Yes"
},
{
"id": "clauuru6t001m3b6qp8vkt23l",
"content": "No",
"blockId": "clauurs1o001k3b6qgrj0xf59",
"type": 0
}
], ],
"outgoingEdgeId": "clauushy3001p3b6qqnyrxgtb" "options": { "isMultipleChoice": false, "buttonLabel": "Send" }
} }
] ]
}, },
{ {
"id": "clauusa9z001n3b6qys3xvz1l", "id": "clauusa9z001n3b6qys3xvz1l",
"graphCoordinates": { "x": 2558.609375, "y": 297.078125 },
"title": "Get Chuck Norris joke", "title": "Get Chuck Norris joke",
"graphCoordinates": { "x": 2558.609375, "y": 297.078125 },
"blocks": [ "blocks": [
{ {
"id": "clauusaa0001o3b6qgddldaen", "id": "clauusaa0001o3b6qgddldaen",
"groupId": "clauusa9z001n3b6qys3xvz1l",
"type": "text", "type": "text",
"content": { "content": {
"richText": [ "richText": [
@ -308,7 +269,6 @@
}, },
{ {
"id": "clauusrfh001q3b6q7xaapi4h", "id": "clauusrfh001q3b6q7xaapi4h",
"groupId": "clauusa9z001n3b6qys3xvz1l",
"type": "text", "type": "text",
"content": { "content": {
"richText": [ "richText": [
@ -321,33 +281,31 @@
}, },
{ {
"id": "clauut2nq001r3b6qi437ixc7", "id": "clauut2nq001r3b6qi437ixc7",
"groupId": "clauusa9z001n3b6qys3xvz1l", "outgoingEdgeId": "clauuwjq2001x3b6qciu53855",
"type": "Webhook", "type": "Webhook",
"options": { "options": {
"variablesForTest": [],
"responseVariableMapping": [ "responseVariableMapping": [
{ {
"id": "clauuvvdr001t3b6qqdxzc057", "id": "clauuvvdr001t3b6qqdxzc057",
"bodyPath": "data.value", "variableId": "vclauuwchv001u3b6qepx6e0a9",
"variableId": "vclauuwchv001u3b6qepx6e0a9" "bodyPath": "data.value"
} }
], ],
"variablesForTest": [],
"isAdvancedConfig": true, "isAdvancedConfig": true,
"isCustomBody": false "isCustomBody": false,
}, "webhook": { "method": "POST" }
"webhookId": "chat-webhook-id", }
"outgoingEdgeId": "clauuwjq2001x3b6qciu53855"
} }
] ]
}, },
{ {
"id": "clauuwhyl001v3b6qarbpiqbv", "id": "clauuwhyl001v3b6qarbpiqbv",
"graphCoordinates": { "x": 2900.9609375, "y": 288.29296875 },
"title": "Display joke", "title": "Display joke",
"graphCoordinates": { "x": 2900.9609375, "y": 288.29296875 },
"blocks": [ "blocks": [
{ {
"id": "clauuwhyl001w3b6q7ai0zeyt", "id": "clauuwhyl001w3b6q7ai0zeyt",
"groupId": "clauuwhyl001v3b6qarbpiqbv",
"type": "text", "type": "text",
"content": { "content": {
"richText": [{ "type": "p", "children": [{ "text": "{{Joke}}" }] }] "richText": [{ "type": "p", "children": [{ "text": "{{Joke}}" }] }]
@ -356,137 +314,120 @@
] ]
} }
], ],
"variables": [
{ "id": "vclauuklnc000b3b6q7xchq4yf", "name": "Name" },
{ "id": "vclauulfjk000i3b6qmujooweu", "name": "Age" },
{ "id": "vclauuohyp000w3b6qbqrs6c6w", "name": "Magic number" },
{ "id": "vclauuwchv001u3b6qepx6e0a9", "name": "Joke" }
],
"edges": [ "edges": [
{ {
"id": "clauuk4o300083b6q7b2iowv3", "id": "clauuk4o300083b6q7b2iowv3",
"to": { "groupId": "clauujxdc00063b6q42ca20gj" }, "from": { "eventId": "clauujawn0000vs1a8z6k2k7d" },
"from": { "to": { "groupId": "clauujxdc00063b6q42ca20gj" }
"blockId": "clauujawn0001vs1a0mk8docp",
"groupId": "clauujawn0000vs1a8z6k2k7d"
}
}, },
{ {
"id": "clauul0sk000f3b6q2tvy5wfi", "id": "clauul0sk000f3b6q2tvy5wfi",
"to": { "groupId": "clauukoka000c3b6qe6chawis" }, "from": { "blockId": "clauukip8000a3b6qtzl288tu" },
"from": { "to": { "groupId": "clauukoka000c3b6qe6chawis" }
"blockId": "clauukip8000a3b6qtzl288tu",
"groupId": "clauujxdc00063b6q42ca20gj"
}
}, },
{ {
"id": "clauum41j000n3b6qpqu12icm", "id": "clauum41j000n3b6qpqu12icm",
"to": { "groupId": "clauulhqf000j3b6qm8y5oifc" }, "from": { "blockId": "clauul90j000h3b6qjfrw9js4" },
"from": { "to": { "groupId": "clauulhqf000j3b6qm8y5oifc" }
"blockId": "clauul90j000h3b6qjfrw9js4",
"groupId": "clauukoka000c3b6qe6chawis"
}
}, },
{ {
"id": "clauumi0x000q3b6q9bwkqnmr", "id": "clauumi0x000q3b6q9bwkqnmr",
"to": { "groupId": "clauum8x7000o3b6qx8hqduf8" },
"from": { "from": {
"itemId": "clauulhqg000l3b6qaxn4qli5",
"blockId": "clauulhqf000k3b6qsrc1hd74", "blockId": "clauulhqf000k3b6qsrc1hd74",
"groupId": "clauulhqf000j3b6qm8y5oifc" "itemId": "clauulhqg000l3b6qaxn4qli5"
} },
"to": { "groupId": "clauum8x7000o3b6qx8hqduf8" }
}, },
{ {
"id": "clauumm5v000t3b6qu62qcft8", "id": "clauumm5v000t3b6qu62qcft8",
"to": { "groupId": "clauumjq4000r3b6q8l6bi9ra" }, "from": { "blockId": "clauulhqf000k3b6qsrc1hd74" },
"from": { "to": { "groupId": "clauumjq4000r3b6q8l6bi9ra" }
"blockId": "clauulhqf000k3b6qsrc1hd74",
"groupId": "clauulhqf000j3b6qm8y5oifc"
}
}, },
{ {
"id": "clauuol8t000x3b6qcw1few70", "id": "clauuol8t000x3b6qcw1few70",
"to": { "groupId": "clauuoekh000u3b6q6zmlx7f9" }, "from": { "blockId": "clauumjq5000s3b6qqjhrklv4" },
"from": { "to": { "groupId": "clauuoekh000u3b6q6zmlx7f9" }
"blockId": "clauumjq5000s3b6qqjhrklv4",
"groupId": "clauumjq4000r3b6q8l6bi9ra"
}
}, },
{ {
"id": "clauuom2y000y3b6qkcjy2ri7", "id": "clauuom2y000y3b6qkcjy2ri7",
"to": { "groupId": "clauuoekh000u3b6q6zmlx7f9" }, "from": { "blockId": "clauum8x7000p3b6qxjud5hdc" },
"from": { "to": { "groupId": "clauuoekh000u3b6q6zmlx7f9" }
"blockId": "clauum8x7000p3b6qxjud5hdc",
"groupId": "clauum8x7000o3b6qx8hqduf8"
}
}, },
{ {
"from": { "id": "clauuq8je001e3b6qksm4j11g",
"groupId": "clauuoekh000u3b6q6zmlx7f9", "from": { "blockId": "clauuontu000z3b6q3ydx6ao1" },
"blockId": "clauuontu000z3b6q3ydx6ao1" "to": { "groupId": "clauuq2l6001c3b6qpmq3ivwk" }
},
"to": { "groupId": "clauuq2l6001c3b6qpmq3ivwk" },
"id": "clauuq8je001e3b6qksm4j11g"
}, },
{ {
"from": { "id": "clauureo3001h3b6qk6epabxq",
"groupId": "clauuq2l6001c3b6qpmq3ivwk", "from": { "blockId": "clauuq2l6001d3b6qyltfcvgb" },
"blockId": "clauuq2l6001d3b6qyltfcvgb" "to": { "groupId": "clauur7od001f3b6qq140oe55" }
},
"to": { "groupId": "clauur7od001f3b6qq140oe55" },
"id": "clauureo3001h3b6qk6epabxq"
}, },
{ {
"from": { "id": "clauushy3001p3b6qqnyrxgtb",
"groupId": "clauur7od001f3b6qq140oe55", "from": { "blockId": "clauurs1o001k3b6qgrj0xf59" },
"blockId": "clauurs1o001k3b6qgrj0xf59" "to": { "groupId": "clauusa9z001n3b6qys3xvz1l" }
},
"to": { "groupId": "clauusa9z001n3b6qys3xvz1l" },
"id": "clauushy3001p3b6qqnyrxgtb"
}, },
{ {
"from": { "id": "clauuwjq2001x3b6qciu53855",
"groupId": "clauusa9z001n3b6qys3xvz1l", "from": { "blockId": "clauut2nq001r3b6qi437ixc7" },
"blockId": "clauut2nq001r3b6qi437ixc7" "to": { "groupId": "clauuwhyl001v3b6qarbpiqbv" }
},
"to": { "groupId": "clauuwhyl001v3b6qarbpiqbv" },
"id": "clauuwjq2001x3b6qciu53855"
} }
], ],
"theme": { "variables": [
"chat": { {
"inputs": { "id": "vclauuklnc000b3b6q7xchq4yf",
"color": "#303235", "name": "Name",
"backgroundColor": "#FFFFFF", "isSessionVariable": true
"placeholderColor": "#9095A0"
},
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
"hostAvatar": {
"url": "https://avatars.githubusercontent.com/u/16015833?v=4",
"isEnabled": true
},
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
}, },
"general": { "font": "Open Sans", "background": { "type": "None" } } {
"id": "vclauulfjk000i3b6qmujooweu",
"name": "Age",
"isSessionVariable": true
},
{ "id": "vclauuohyp000w3b6qbqrs6c6w", "name": "Magic number" },
{ "id": "vclauuwchv001u3b6qepx6e0a9", "name": "Joke" }
],
"theme": {
"general": { "font": "Open Sans", "background": { "type": "None" } },
"chat": {
"hostAvatar": {
"isEnabled": true,
"url": "https://avatars.githubusercontent.com/u/16015833?v=4"
},
"hostBubbles": { "backgroundColor": "#F7F8FF", "color": "#303235" },
"guestBubbles": { "backgroundColor": "#FF8E21", "color": "#FFFFFF" },
"buttons": { "backgroundColor": "#0042DA", "color": "#FFFFFF" },
"inputs": {
"backgroundColor": "#FFFFFF",
"color": "#303235",
"placeholderColor": "#9095A0"
}
}
}, },
"selectedThemeTemplateId": null,
"settings": { "settings": {
"general": { "general": {
"isBrandingEnabled": false, "isBrandingEnabled": false,
"isInputPrefillEnabled": true, "isInputPrefillEnabled": true,
"isResultSavingEnabled": true,
"isHideQueryParamsEnabled": true, "isHideQueryParamsEnabled": true,
"isNewResultOnRefreshEnabled": false "isNewResultOnRefreshEnabled": false
}, },
"typingEmulation": { "enabled": true, "speed": 300, "maxDelay": 1.5 },
"metadata": { "metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form." "description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
}, }
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
}, },
"createdAt": "2024-07-16T12:38:39.850Z",
"updatedAt": "2024-07-16T12:38:39.850Z",
"icon": "🤖",
"folderId": null,
"publicId": null, "publicId": null,
"customDomain": null, "customDomain": null,
"workspaceId": "proWorkspace", "workspaceId": "proWorkspace",
"resultsTablePreferences": null, "resultsTablePreferences": null,
"isArchived": false, "isArchived": false,
"isClosed": false "isClosed": false,
"whatsAppCredentialsId": null,
"riskLevel": null
} }

View File

@ -1,22 +1,16 @@
{ {
"version": "5", "version": "6",
"id": "clnbugp6a00011ackz0k3zfkp", "id": "clyoehs240009grw9vcxfw1ku",
"name": "My typebot", "name": "My typebot",
"groups": [ "events": [
{ {
"id": "k2nokn9v0zyhae0wqcxsbqa7", "id": "k2nokn9v0zyhae0wqcxsbqa7",
"title": "Start", "outgoingEdgeId": "fj2ga89lctnuwcdsshwtxmhp",
"graphCoordinates": { "x": 0, "y": 0 }, "graphCoordinates": { "x": 0, "y": 0 },
"blocks": [ "type": "start"
{ }
"id": "sx4xmdbosubnxkhcg6x521p1", ],
"groupId": "k2nokn9v0zyhae0wqcxsbqa7", "groups": [
"outgoingEdgeId": "fj2ga89lctnuwcdsshwtxmhp",
"type": "start",
"label": "Start"
}
]
},
{ {
"id": "g8kdars2ahr3cyz2qf1f7w4i", "id": "g8kdars2ahr3cyz2qf1f7w4i",
"title": "Group #1", "title": "Group #1",
@ -24,7 +18,6 @@
"blocks": [ "blocks": [
{ {
"id": "prh6snup7cbmoxtf5vox8kjw", "id": "prh6snup7cbmoxtf5vox8kjw",
"groupId": "g8kdars2ahr3cyz2qf1f7w4i",
"type": "text input", "type": "text input",
"options": { "options": {
"labels": { "labels": {
@ -36,7 +29,6 @@
}, },
{ {
"id": "dpyyb38amnwwl4q461el2uf6", "id": "dpyyb38amnwwl4q461el2uf6",
"groupId": "g8kdars2ahr3cyz2qf1f7w4i",
"type": "text", "type": "text",
"content": { "content": {
"richText": [ "richText": [
@ -50,10 +42,7 @@
"edges": [ "edges": [
{ {
"id": "fj2ga89lctnuwcdsshwtxmhp", "id": "fj2ga89lctnuwcdsshwtxmhp",
"from": { "from": { "eventId": "k2nokn9v0zyhae0wqcxsbqa7" },
"groupId": "k2nokn9v0zyhae0wqcxsbqa7",
"blockId": "sx4xmdbosubnxkhcg6x521p1"
},
"to": { "groupId": "g8kdars2ahr3cyz2qf1f7w4i" } "to": { "groupId": "g8kdars2ahr3cyz2qf1f7w4i" }
} }
], ],
@ -88,8 +77,8 @@
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form." "description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
} }
}, },
"createdAt": "2023-10-04T14:28:55.282Z", "createdAt": "2024-07-16T12:39:37.804Z",
"updatedAt": "2023-10-04T14:29:11.949Z", "updatedAt": "2024-07-16T12:39:37.804Z",
"icon": null, "icon": null,
"folderId": null, "folderId": null,
"publicId": null, "publicId": null,
@ -98,5 +87,6 @@
"resultsTablePreferences": null, "resultsTablePreferences": null,
"isArchived": false, "isArchived": false,
"isClosed": false, "isClosed": false,
"whatsAppCredentialsId": null "whatsAppCredentialsId": null,
"riskLevel": null
} }

View File

@ -1,85 +1,61 @@
{ {
"id": "cl9ip9u0l00001ad79a2lzm55", "version": "6",
"createdAt": "2022-10-21T16:22:07.414Z", "id": "clyoep429000dgrw904vfzaez",
"updatedAt": "2022-10-21T16:30:57.642Z",
"icon": null,
"name": "My typebot", "name": "My typebot",
"folderId": null, "events": [
"version": "4",
"groups": [
{ {
"id": "cl9ip9u0j0000d71a5d98gwni", "id": "cl9ip9u0j0000d71a5d98gwni",
"title": "Start", "outgoingEdgeId": "cl9ipkkb2001b3b6oh3vptq9k",
"blocks": [ "graphCoordinates": { "x": 0, "y": 0 },
{ "type": "start"
"id": "cl9ip9u0j0001d71a44dsd2p1", }
"type": "start", ],
"label": "Start", "groups": [
"groupId": "cl9ip9u0j0000d71a5d98gwni",
"outgoingEdgeId": "cl9ipkkb2001b3b6oh3vptq9k"
}
],
"graphCoordinates": { "x": 0, "y": 0 }
},
{ {
"id": "cl9ipa38j00083b6o69e90m4t", "id": "cl9ipa38j00083b6o69e90m4t",
"graphCoordinates": { "x": 340, "y": 341 },
"title": "Group #1", "title": "Group #1",
"graphCoordinates": { "x": 340, "y": 341 },
"blocks": [ "blocks": [
{ {
"id": "cl9ipaaut000a3b6ovrqlec3x", "id": "cl9ipaaut000a3b6ovrqlec3x",
"groupId": "cl9ipa38j00083b6o69e90m4t",
"type": "text input", "type": "text input",
"options": { "options": {
"isLong": false, "labels": { "placeholder": "Type a name...", "button": "Send" },
"labels": { "button": "Send", "placeholder": "Type a name..." }, "variableId": "vcl9ipajth000c3b6okl97r81j",
"variableId": "vcl9ipajth000c3b6okl97r81j" "isLong": false
} }
}, },
{ {
"id": "cl9ipan8f000d3b6oo2ovi3ac", "id": "cl9ipan8f000d3b6oo2ovi3ac",
"groupId": "cl9ipa38j00083b6o69e90m4t",
"type": "number input", "type": "number input",
"options": { "options": {
"labels": { "button": "Send", "placeholder": "Type an age..." }, "variableId": "vcl9ipaszl000e3b6ousjxuw7b",
"variableId": "vcl9ipaszl000e3b6ousjxuw7b" "labels": { "placeholder": "Type an age...", "button": "Send" }
} }
}, },
{ {
"id": "cl9ipb08n000f3b6ok3mi2p48", "id": "cl9ipb08n000f3b6ok3mi2p48",
"groupId": "cl9ipa38j00083b6o69e90m4t", "outgoingEdgeId": "cl9ipcp83000o3b6odsn0a9a1",
"type": "choice input", "type": "choice input",
"options": {
"buttonLabel": "Send",
"isMultipleChoice": false,
"variableId": "vcl9ipg4tb00103b6oue08w3nm"
},
"items": [ "items": [
{ { "id": "cl9ipb08n000g3b6okr691uad", "content": "Male" },
"id": "cl9ipb08n000g3b6okr691uad", { "id": "cl9ipb2kk000h3b6oadwtonnz", "content": "Female" }
"blockId": "cl9ipb08n000f3b6ok3mi2p48",
"type": 0,
"content": "Male"
},
{
"blockId": "cl9ipb08n000f3b6ok3mi2p48",
"type": 0,
"id": "cl9ipb2kk000h3b6oadwtonnz",
"content": "Female"
}
], ],
"outgoingEdgeId": "cl9ipcp83000o3b6odsn0a9a1" "options": {
"variableId": "vcl9ipg4tb00103b6oue08w3nm",
"isMultipleChoice": false,
"buttonLabel": "Send"
}
} }
] ]
}, },
{ {
"id": "cl9ipbcjy000j3b6oqngo7luv", "id": "cl9ipbcjy000j3b6oqngo7luv",
"graphCoordinates": { "x": 781, "y": 91 },
"title": "Group #2", "title": "Group #2",
"graphCoordinates": { "x": 781, "y": 91 },
"blocks": [ "blocks": [
{ {
"id": "cl9ipbl6l000m3b6o3evn41kv", "id": "cl9ipbl6l000m3b6o3evn41kv",
"groupId": "cl9ipbcjy000j3b6oqngo7luv",
"type": "Set variable", "type": "Set variable",
"options": { "options": {
"variableId": "vcl9ipbokm000n3b6o06hvarrf", "variableId": "vcl9ipbokm000n3b6o06hvarrf",
@ -88,9 +64,9 @@
}, },
{ {
"id": "cl9ipbcjy000k3b6oe8lta5c1", "id": "cl9ipbcjy000k3b6oe8lta5c1",
"groupId": "cl9ipbcjy000j3b6oqngo7luv",
"type": "Webhook", "type": "Webhook",
"options": { "options": {
"variablesForTest": [],
"responseVariableMapping": [ "responseVariableMapping": [
{ {
"id": "cl9ipdspg000p3b6ognbfvmdx", "id": "cl9ipdspg000p3b6ognbfvmdx",
@ -98,15 +74,17 @@
"bodyPath": "data" "bodyPath": "data"
} }
], ],
"variablesForTest": [],
"isAdvancedConfig": true, "isAdvancedConfig": true,
"isCustomBody": true "isCustomBody": true,
}, "webhook": {
"webhookId": "full-body-webhook" "url": "http://localhost:3000/api/mock/webhook-easy-config",
"body": "{\n \"name\": \"{{Name}}\",\n \"age\": {{Age}},\n \"gender\": \"{{Gender}}\"\n }"
}
}
}, },
{ {
"id": "cl9ipe5t8000s3b6ocswre500", "id": "cl9ipe5t8000s3b6ocswre500",
"groupId": "cl9ipbcjy000j3b6oqngo7luv", "outgoingEdgeId": "cl9ipet83000z3b6of6zfqota",
"type": "text", "type": "text",
"content": { "content": {
"richText": [ "richText": [
@ -117,21 +95,20 @@
{ "type": "p", "children": [{ "text": "" }] }, { "type": "p", "children": [{ "text": "" }] },
{ "type": "p", "children": [{ "text": "{{Data}}" }] } { "type": "p", "children": [{ "text": "{{Data}}" }] }
] ]
}, }
"outgoingEdgeId": "cl9ipet83000z3b6of6zfqota"
} }
] ]
}, },
{ {
"id": "cl9ipej6b000u3b6oeaz305l6", "id": "cl9ipej6b000u3b6oeaz305l6",
"graphCoordinates": { "x": 1138, "y": 85 },
"title": "Group #2 copy", "title": "Group #2 copy",
"graphCoordinates": { "x": 1138, "y": 85 },
"blocks": [ "blocks": [
{ {
"id": "cl9ipej6c000w3b6otzk247vl", "id": "cl9ipej6c000w3b6otzk247vl",
"groupId": "cl9ipej6b000u3b6oeaz305l6",
"type": "Webhook", "type": "Webhook",
"options": { "options": {
"variablesForTest": [],
"responseVariableMapping": [ "responseVariableMapping": [
{ {
"id": "cl9ipdspg000p3b6ognbfvmdx", "id": "cl9ipdspg000p3b6ognbfvmdx",
@ -139,15 +116,16 @@
"bodyPath": "data" "bodyPath": "data"
} }
], ],
"variablesForTest": [],
"isAdvancedConfig": true, "isAdvancedConfig": true,
"isCustomBody": true "isCustomBody": true,
}, "webhook": {
"webhookId": "partial-body-webhook" "url": "http://localhost:3000/api/mock/webhook-easy-config",
"body": "{{Full body}}"
}
}
}, },
{ {
"id": "cl9ipej6c000y3b6oegzkgloq", "id": "cl9ipej6c000y3b6oegzkgloq",
"groupId": "cl9ipej6b000u3b6oeaz305l6",
"type": "text", "type": "text",
"content": { "content": {
"richText": [ "richText": [
@ -164,97 +142,94 @@
}, },
{ {
"id": "cl9ipkaer00153b6ov230yuv2", "id": "cl9ipkaer00153b6ov230yuv2",
"graphCoordinates": { "x": 333, "y": 26 },
"title": "Group #4", "title": "Group #4",
"graphCoordinates": { "x": 333, "y": 26 },
"blocks": [ "blocks": [
{ {
"id": "cl9ipkaer00163b6o0ohmmscn", "id": "cl9ipkaer00163b6o0ohmmscn",
"groupId": "cl9ipkaer00153b6ov230yuv2",
"type": "choice input", "type": "choice input",
"options": { "buttonLabel": "Send", "isMultipleChoice": false },
"items": [ "items": [
{ {
"id": "cl9ipkaer00173b6oxof4zrqn", "id": "cl9ipkaer00173b6oxof4zrqn",
"blockId": "cl9ipkaer00163b6o0ohmmscn",
"type": 0,
"content": "Send failing webhook" "content": "Send failing webhook"
} }
] ],
"options": { "isMultipleChoice": false, "buttonLabel": "Send" }
}, },
{ {
"id": "cl9ipki9u00193b6okmhudo0f", "id": "cl9ipki9u00193b6okmhudo0f",
"groupId": "cl9ipkaer00153b6ov230yuv2", "outgoingEdgeId": "cl9ipklm0001c3b6oy0a5nbhr",
"type": "Webhook", "type": "Webhook",
"options": { "options": {
"responseVariableMapping": [],
"variablesForTest": [], "variablesForTest": [],
"responseVariableMapping": [],
"isAdvancedConfig": false, "isAdvancedConfig": false,
"isCustomBody": false "isCustomBody": false,
}, "webhook": { "url": "http://localhost:3001/api/mock/fail" }
"webhookId": "failing-webhook", }
"outgoingEdgeId": "cl9ipklm0001c3b6oy0a5nbhr"
} }
] ]
} }
], ],
"variables": [
{ "id": "vcl9ipajth000c3b6okl97r81j", "name": "Name" },
{ "id": "vcl9ipaszl000e3b6ousjxuw7b", "name": "Age" },
{ "id": "vcl9ipbokm000n3b6o06hvarrf", "name": "Full body" },
{ "id": "vcl9ipdxnj000q3b6oy55th4xb", "name": "Data" },
{ "id": "vcl9ipg4tb00103b6oue08w3nm", "name": "Gender" }
],
"edges": [ "edges": [
{ {
"from": { "id": "cl9ipkkb2001b3b6oh3vptq9k",
"groupId": "cl9ipa38j00083b6o69e90m4t", "from": { "eventId": "cl9ip9u0j0000d71a5d98gwni" },
"blockId": "cl9ipb08n000f3b6ok3mi2p48" "to": { "groupId": "cl9ipkaer00153b6ov230yuv2" }
},
"to": { "groupId": "cl9ipbcjy000j3b6oqngo7luv" },
"id": "cl9ipcp83000o3b6odsn0a9a1"
}, },
{ {
"from": { "id": "cl9ipcp83000o3b6odsn0a9a1",
"groupId": "cl9ipbcjy000j3b6oqngo7luv", "from": { "blockId": "cl9ipb08n000f3b6ok3mi2p48" },
"blockId": "cl9ipe5t8000s3b6ocswre500" "to": { "groupId": "cl9ipbcjy000j3b6oqngo7luv" }
},
"to": { "groupId": "cl9ipej6b000u3b6oeaz305l6" },
"id": "cl9ipet83000z3b6of6zfqota"
}, },
{ {
"from": { "id": "cl9ipet83000z3b6of6zfqota",
"groupId": "cl9ip9u0j0000d71a5d98gwni", "from": { "blockId": "cl9ipe5t8000s3b6ocswre500" },
"blockId": "cl9ip9u0j0001d71a44dsd2p1" "to": { "groupId": "cl9ipej6b000u3b6oeaz305l6" }
},
"to": { "groupId": "cl9ipkaer00153b6ov230yuv2" },
"id": "cl9ipkkb2001b3b6oh3vptq9k"
}, },
{ {
"from": { "id": "cl9ipklm0001c3b6oy0a5nbhr",
"groupId": "cl9ipkaer00153b6ov230yuv2", "from": { "blockId": "cl9ipki9u00193b6okmhudo0f" },
"blockId": "cl9ipki9u00193b6okmhudo0f" "to": { "groupId": "cl9ipa38j00083b6o69e90m4t" }
}, }
"to": { "groupId": "cl9ipa38j00083b6o69e90m4t" }, ],
"id": "cl9ipklm0001c3b6oy0a5nbhr" "variables": [
{
"id": "vcl9ipajth000c3b6okl97r81j",
"name": "Name",
"isSessionVariable": true
},
{
"id": "vcl9ipaszl000e3b6ousjxuw7b",
"name": "Age",
"isSessionVariable": true
},
{ "id": "vcl9ipbokm000n3b6o06hvarrf", "name": "Full body" },
{ "id": "vcl9ipdxnj000q3b6oy55th4xb", "name": "Data" },
{
"id": "vcl9ipg4tb00103b6oue08w3nm",
"name": "Gender",
"isSessionVariable": true
} }
], ],
"theme": { "theme": {
"general": { "font": "Open Sans", "background": { "type": "None" } },
"chat": { "chat": {
"inputs": {
"color": "#303235",
"backgroundColor": "#FFFFFF",
"placeholderColor": "#9095A0"
},
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
"hostAvatar": { "hostAvatar": {
"url": "https://avatars.githubusercontent.com/u/16015833?v=4", "isEnabled": true,
"isEnabled": true "url": "https://avatars.githubusercontent.com/u/16015833?v=4"
}, },
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" }, "hostBubbles": { "backgroundColor": "#F7F8FF", "color": "#303235" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" } "guestBubbles": { "backgroundColor": "#FF8E21", "color": "#FFFFFF" },
}, "buttons": { "backgroundColor": "#0042DA", "color": "#FFFFFF" },
"general": { "font": "Open Sans", "background": { "type": "None" } } "inputs": {
"backgroundColor": "#FFFFFF",
"color": "#303235",
"placeholderColor": "#9095A0"
}
}
}, },
"selectedThemeTemplateId": null,
"settings": { "settings": {
"general": { "general": {
"isBrandingEnabled": false, "isBrandingEnabled": false,
@ -262,14 +237,21 @@
"isHideQueryParamsEnabled": true, "isHideQueryParamsEnabled": true,
"isNewResultOnRefreshEnabled": false "isNewResultOnRefreshEnabled": false
}, },
"typingEmulation": { "enabled": true, "speed": 300, "maxDelay": 1.5 },
"metadata": { "metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form." "description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
}, }
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
}, },
"createdAt": "2024-07-16T12:45:19.954Z",
"updatedAt": "2024-07-16T12:46:27.462Z",
"icon": null,
"folderId": null,
"publicId": null, "publicId": null,
"customDomain": null, "customDomain": null,
"workspaceId": "proWorkspace", "workspaceId": "proWorkspace",
"resultsTablePreferences": null,
"isArchived": false, "isArchived": false,
"isClosed": false "isClosed": false,
"whatsAppCredentialsId": null,
"riskLevel": null
} }

View File

@ -2,18 +2,30 @@ import { getTestAsset } from '@/test/utils/playwright'
import test, { expect } from '@playwright/test' import test, { expect } from '@playwright/test'
import { createId } from '@paralleldrive/cuid2' import { createId } from '@paralleldrive/cuid2'
import prisma from '@typebot.io/lib/prisma' import prisma from '@typebot.io/lib/prisma'
import { import { importTypebotInDatabase } from '@typebot.io/playwright/databaseActions'
createWebhook,
deleteTypebots,
deleteWebhooks,
importTypebotInDatabase,
} from '@typebot.io/playwright/databaseActions'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants'
import { StartChatInput, StartPreviewChatInput } from '@typebot.io/schemas' import { StartChatInput, StartPreviewChatInput } from '@typebot.io/schemas'
test.afterEach(async () => { test.describe.configure({ mode: 'parallel' })
await deleteWebhooks(['chat-webhook-id'])
await deleteTypebots(['chat-sub-bot', 'starting-with-input']) test.beforeEach(async () => {
try {
await importTypebotInDatabase(
getTestAsset('typebots/chat/linkedBot.json'),
{
id: 'chat-sub-bot',
publicId: 'chat-sub-bot-public',
}
)
await importTypebotInDatabase(
getTestAsset('typebots/chat/startingWithInput.json'),
{
id: 'starting-with-input',
publicId: 'starting-with-input-public',
}
)
} catch {
/* empty */
}
}) })
test('API chat execution should work on preview bot', async ({ request }) => { test('API chat execution should work on preview bot', async ({ request }) => {
@ -23,22 +35,6 @@ test('API chat execution should work on preview bot', async ({ request }) => {
id: typebotId, id: typebotId,
publicId, publicId,
}) })
await importTypebotInDatabase(getTestAsset('typebots/chat/linkedBot.json'), {
id: 'chat-sub-bot',
publicId: 'chat-sub-bot-public',
})
await importTypebotInDatabase(
getTestAsset('typebots/chat/startingWithInput.json'),
{
id: 'starting-with-input',
publicId: 'starting-with-input-public',
}
)
await createWebhook(typebotId, {
id: 'chat-webhook-id',
method: HttpMethod.GET,
url: 'https://api.chucknorris.io/jokes/random',
})
let chatSessionId: string let chatSessionId: string
@ -104,22 +100,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
id: typebotId, id: typebotId,
publicId, publicId,
}) })
await importTypebotInDatabase(getTestAsset('typebots/chat/linkedBot.json'), {
id: 'chat-sub-bot',
publicId: 'chat-sub-bot-public',
})
await importTypebotInDatabase(
getTestAsset('typebots/chat/startingWithInput.json'),
{
id: 'starting-with-input',
publicId: 'starting-with-input-public',
}
)
await createWebhook(typebotId, {
id: 'chat-webhook-id',
method: HttpMethod.GET,
url: 'https://api.chucknorris.io/jokes/random',
})
let chatSessionId: string let chatSessionId: string
await test.step('Start the chat', async () => { await test.step('Start the chat', async () => {

View File

@ -1,50 +1,14 @@
import test, { expect } from '@playwright/test' import test, { expect } from '@playwright/test'
import { createId } from '@paralleldrive/cuid2' import { createId } from '@paralleldrive/cuid2'
import { import { importTypebotInDatabase } from '@typebot.io/playwright/databaseActions'
createWebhook,
importTypebotInDatabase,
} from '@typebot.io/playwright/databaseActions'
import { getTestAsset } from '@/test/utils/playwright' import { getTestAsset } from '@/test/utils/playwright'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants'
const typebotId = createId() test('should execute webhooks properly', async ({ page }) => {
const typebotId = createId()
test.beforeEach(async () => {
await importTypebotInDatabase(getTestAsset('typebots/webhook.json'), { await importTypebotInDatabase(getTestAsset('typebots/webhook.json'), {
id: typebotId, id: typebotId,
publicId: `${typebotId}-public`, publicId: `${typebotId}-public`,
}) })
try {
await createWebhook(typebotId, {
id: 'failing-webhook',
url: 'http://localhost:3001/api/mock/fail',
method: HttpMethod.POST,
})
await createWebhook(typebotId, {
id: 'partial-body-webhook',
url: 'http://localhost:3000/api/mock/webhook-easy-config',
method: HttpMethod.POST,
body: `{
"name": "{{Name}}",
"age": {{Age}},
"gender": "{{Gender}}"
}`,
})
await createWebhook(typebotId, {
id: 'full-body-webhook',
url: 'http://localhost:3000/api/mock/webhook-easy-config',
method: HttpMethod.POST,
body: `{{Full body}}`,
})
} catch (err) {
console.log(err)
}
})
test('should execute webhooks properly', async ({ page }) => {
await page.goto(`/${typebotId}-public`) await page.goto(`/${typebotId}-public`)
await page.locator('text=Send failing webhook').click() await page.locator('text=Send failing webhook').click()
await page.locator('[placeholder="Type a name..."]').fill('John') await page.locator('[placeholder="Type a name..."]').fill('John')

View File

@ -335,8 +335,8 @@ export const convertKeyValueTableToObject = (
const value = parseVariables(variables)(item.value) const value = parseVariables(variables)(item.value)
if (isEmpty(key) || isEmpty(value)) return object if (isEmpty(key) || isEmpty(value)) return object
if (object[key] && concatDuplicateInArray) { if (object[key] && concatDuplicateInArray) {
if (Array.isArray(object[key])) object[key].push(value) if (Array.isArray(object[key])) (object[key] as string[]).push(value)
else object[key] = [object[key], value] else object[key] = [object[key] as string, value]
} else object[key] = value } else object[key] = value
return object return object
}, {}) }, {})

View File

@ -1,6 +1,10 @@
// Do not edit this file manually // Do not edit this file manually
import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge' import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge'
import { anthropicBlock } from '.' import { anthropicBlock } from '.'
import { auth } from './auth'
export const anthropicBlockSchema = parseBlockSchema(anthropicBlock) export const anthropicBlockSchema = parseBlockSchema(anthropicBlock)
export const anthropicCredentialsSchema = parseBlockCredentials(anthropicBlock) export const anthropicCredentialsSchema = parseBlockCredentials(
anthropicBlock.id,
auth.schema
)

View File

@ -1,6 +1,5 @@
// Do not edit this file manually // Do not edit this file manually
import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge' import { parseBlockSchema } from '@typebot.io/forge'
import { calComBlock } from '.' import { calComBlock } from '.'
export const calComBlockSchema = parseBlockSchema(calComBlock) export const calComBlockSchema = parseBlockSchema(calComBlock)
export const calComCredentialsSchema = parseBlockCredentials(calComBlock)

View File

@ -1,6 +1,10 @@
// Do not edit this file manually // Do not edit this file manually
import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge' import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge'
import { chatNodeBlock } from '.' import { chatNodeBlock } from '.'
import { auth } from './auth'
export const chatNodeBlockSchema = parseBlockSchema(chatNodeBlock) export const chatNodeBlockSchema = parseBlockSchema(chatNodeBlock)
export const chatNodeCredentialsSchema = parseBlockCredentials(chatNodeBlock) export const chatNodeCredentialsSchema = parseBlockCredentials(
chatNodeBlock.id,
auth.schema
)

View File

@ -1,6 +1,10 @@
// Do not edit this file manually // Do not edit this file manually
import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge' import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge'
import { difyAiBlock } from '.' import { difyAiBlock } from '.'
import { auth } from './auth'
export const difyAiBlockSchema = parseBlockSchema(difyAiBlock) export const difyAiBlockSchema = parseBlockSchema(difyAiBlock)
export const difyAiCredentialsSchema = parseBlockCredentials(difyAiBlock) export const difyAiCredentialsSchema = parseBlockCredentials(
difyAiBlock.id,
auth.schema
)

View File

@ -1,7 +1,10 @@
// Do not edit this file manually // Do not edit this file manually
import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge' import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge'
import { elevenlabsBlock } from '.' import { elevenlabsBlock } from '.'
import { auth } from './auth'
export const elevenlabsBlockSchema = parseBlockSchema(elevenlabsBlock) export const elevenlabsBlockSchema = parseBlockSchema(elevenlabsBlock)
export const elevenlabsCredentialsSchema = export const elevenlabsCredentialsSchema = parseBlockCredentials(
parseBlockCredentials(elevenlabsBlock) elevenlabsBlock.id,
auth.schema
)

View File

@ -1,6 +1,10 @@
// Do not edit this file manually // Do not edit this file manually
import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge' import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge'
import { mistralBlock } from '.' import { mistralBlock } from '.'
import { auth } from './auth'
export const mistralBlockSchema = parseBlockSchema(mistralBlock) export const mistralBlockSchema = parseBlockSchema(mistralBlock)
export const mistralCredentialsSchema = parseBlockCredentials(mistralBlock) export const mistralCredentialsSchema = parseBlockCredentials(
mistralBlock.id,
auth.schema
)

View File

@ -1,6 +1,10 @@
// Do not edit this file manually // Do not edit this file manually
import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge' import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge'
import { nocodbBlock } from '.' import { nocodbBlock } from '.'
import { auth } from './auth'
export const nocodbBlockSchema = parseBlockSchema(nocodbBlock) export const nocodbBlockSchema = parseBlockSchema(nocodbBlock)
export const nocodbCredentialsSchema = parseBlockCredentials(nocodbBlock) export const nocodbCredentialsSchema = parseBlockCredentials(
nocodbBlock.id,
auth.schema
)

View File

@ -1,7 +1,10 @@
// Do not edit this file manually // Do not edit this file manually
import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge' import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge'
import { openRouterBlock } from '.' import { openRouterBlock } from '.'
import { auth } from './auth'
export const openRouterBlockSchema = parseBlockSchema(openRouterBlock) export const openRouterBlockSchema = parseBlockSchema(openRouterBlock)
export const openRouterCredentialsSchema = export const openRouterCredentialsSchema = parseBlockCredentials(
parseBlockCredentials(openRouterBlock) openRouterBlock.id,
auth.schema
)

View File

@ -1,6 +1,10 @@
// Do not edit this file manually // Do not edit this file manually
import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge' import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge'
import { openAIBlock } from '.' import { openAIBlock } from '.'
import { auth } from './auth'
export const openAIBlockSchema = parseBlockSchema(openAIBlock) export const openAIBlockSchema = parseBlockSchema(openAIBlock)
export const openAICredentialsSchema = parseBlockCredentials(openAIBlock) export const openAICredentialsSchema = parseBlockCredentials(
openAIBlock.id,
auth.schema
)

View File

@ -1,6 +1,5 @@
// Do not edit this file manually // Do not edit this file manually
import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge' import { parseBlockSchema } from '@typebot.io/forge'
import { qrCodeBlock } from '.' import { qrCodeBlock } from '.'
export const qrCodeBlockSchema = parseBlockSchema(qrCodeBlock) export const qrCodeBlockSchema = parseBlockSchema(qrCodeBlock)
export const qrCodeCredentialsSchema = parseBlockCredentials(qrCodeBlock)

View File

@ -1,7 +1,10 @@
// Do not edit this file manually // Do not edit this file manually
import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge' import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge'
import { togetherAiBlock } from '.' import { togetherAiBlock } from '.'
import { auth } from './auth'
export const togetherAiBlockSchema = parseBlockSchema(togetherAiBlock) export const togetherAiBlockSchema = parseBlockSchema(togetherAiBlock)
export const togetherAiCredentialsSchema = export const togetherAiCredentialsSchema = parseBlockCredentials(
parseBlockCredentials(togetherAiBlock) togetherAiBlock.id,
auth.schema
)

View File

@ -1,7 +1,10 @@
// Do not edit this file manually // Do not edit this file manually
import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge' import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge'
import { zemanticAiBlock } from '.' import { zemanticAiBlock } from '.'
import { auth } from './auth'
export const zemanticAiBlockSchema = parseBlockSchema(zemanticAiBlock) export const zemanticAiBlockSchema = parseBlockSchema(zemanticAiBlock)
export const zemanticAiCredentialsSchema = export const zemanticAiCredentialsSchema = parseBlockCredentials(
parseBlockCredentials(zemanticAiBlock) zemanticAiBlock.id,
auth.schema
)

View File

@ -273,6 +273,7 @@ const createSchemasFile = async (
path: string, path: string,
{ {
id, id,
auth,
}: { id: string; name: string; auth: 'apiKey' | 'encryptedData' | 'none' } }: { id: string; name: string; auth: 'apiKey' | 'encryptedData' | 'none' }
) => { ) => {
const camelCaseName = camelize(id as string) const camelCaseName = camelize(id as string)
@ -280,11 +281,19 @@ const createSchemasFile = async (
join(path, 'schemas.ts'), join(path, 'schemas.ts'),
await prettier.format( await prettier.format(
`// Do not edit this file manually `// Do not edit this file manually
import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge' import { ${
import { ${camelCaseName}Block } from '.' auth !== 'none' ? 'parseBlockCredentials,' : ''
} parseBlockSchema } from '@typebot.io/forge'
import { ${camelCaseName}Block } from '.'${
auth !== 'none' ? `\nimport { auth } from './auth'` : ''
}
export const ${camelCaseName}BlockSchema = parseBlockSchema(${camelCaseName}Block) export const ${camelCaseName}BlockSchema = parseBlockSchema(${camelCaseName}Block)
export const ${camelCaseName}CredentialsSchema = parseBlockCredentials(${camelCaseName}Block)`, ${
auth !== 'none'
? `export const ${camelCaseName}CredentialsSchema = parseBlockCredentials(${camelCaseName}Block.id, auth.schema)`
: ''
}`,
{ parser: 'typescript', ...prettierRc } { parser: 'typescript', ...prettierRc }
) )
) )

View File

@ -82,22 +82,18 @@ export const parseBlockSchema = <
}) })
} }
export const parseBlockCredentials = < export const parseBlockCredentials = <I extends string>(
I extends string, blockId: I,
A extends AuthDefinition, authSchema: z.ZodObject<any>
O extends z.ZodObject<any>
>(
blockDefinition: BlockDefinition<I, A, O>
) => { ) => {
if (!blockDefinition.auth) return null
return z.object({ return z.object({
id: z.string(), id: z.string(),
type: z.literal(blockDefinition.id), type: z.literal(blockId),
createdAt: z.date(), createdAt: z.date(),
workspaceId: z.string(), workspaceId: z.string(),
name: z.string(), name: z.string(),
iv: z.string(), iv: z.string(),
data: blockDefinition.auth.schema, data: authSchema,
}) })
} }

View File

@ -1,7 +1,5 @@
import { anthropicBlock } from '@typebot.io/anthropic-block' import { anthropicBlock } from '@typebot.io/anthropic-block'
import { anthropicCredentialsSchema } from '@typebot.io/anthropic-block/schemas' import { anthropicCredentialsSchema } from '@typebot.io/anthropic-block/schemas'
import { calComBlock } from '@typebot.io/cal-com-block'
import { calComCredentialsSchema } from '@typebot.io/cal-com-block/schemas'
import { chatNodeBlock } from '@typebot.io/chat-node-block' import { chatNodeBlock } from '@typebot.io/chat-node-block'
import { chatNodeCredentialsSchema } from '@typebot.io/chat-node-block/schemas' import { chatNodeCredentialsSchema } from '@typebot.io/chat-node-block/schemas'
import { difyAiBlock } from '@typebot.io/dify-ai-block' import { difyAiBlock } from '@typebot.io/dify-ai-block'
@ -14,8 +12,6 @@ import { openRouterBlock } from '@typebot.io/open-router-block'
import { openRouterCredentialsSchema } from '@typebot.io/open-router-block/schemas' import { openRouterCredentialsSchema } from '@typebot.io/open-router-block/schemas'
import { openAIBlock } from '@typebot.io/openai-block' import { openAIBlock } from '@typebot.io/openai-block'
import { openAICredentialsSchema } from '@typebot.io/openai-block/schemas' import { openAICredentialsSchema } from '@typebot.io/openai-block/schemas'
import { qrCodeBlock } from '@typebot.io/qrcode-block'
import { qrCodeCredentialsSchema } from '@typebot.io/qrcode-block/schemas'
import { togetherAiBlock } from '@typebot.io/together-ai-block' import { togetherAiBlock } from '@typebot.io/together-ai-block'
import { togetherAiCredentialsSchema } from '@typebot.io/together-ai-block/schemas' import { togetherAiCredentialsSchema } from '@typebot.io/together-ai-block/schemas'
import { zemanticAiBlock } from '@typebot.io/zemantic-ai-block' import { zemanticAiBlock } from '@typebot.io/zemantic-ai-block'
@ -26,9 +22,7 @@ import { nocodbCredentialsSchema } from '@typebot.io/nocodb-block/schemas'
export const forgedCredentialsSchemas = { export const forgedCredentialsSchemas = {
[openAIBlock.id]: openAICredentialsSchema, [openAIBlock.id]: openAICredentialsSchema,
[zemanticAiBlock.id]: zemanticAiCredentialsSchema, [zemanticAiBlock.id]: zemanticAiCredentialsSchema,
[calComBlock.id]: calComCredentialsSchema,
[chatNodeBlock.id]: chatNodeCredentialsSchema, [chatNodeBlock.id]: chatNodeCredentialsSchema,
[qrCodeBlock.id]: qrCodeCredentialsSchema,
[difyAiBlock.id]: difyAiCredentialsSchema, [difyAiBlock.id]: difyAiCredentialsSchema,
[mistralBlock.id]: mistralCredentialsSchema, [mistralBlock.id]: mistralCredentialsSchema,
[elevenlabsBlock.id]: elevenlabsCredentialsSchema, [elevenlabsBlock.id]: elevenlabsCredentialsSchema,

View File

@ -7,7 +7,7 @@
"types": "./index.ts", "types": "./index.ts",
"devDependencies": { "devDependencies": {
"@paralleldrive/cuid2": "2.2.1", "@paralleldrive/cuid2": "2.2.1",
"@playwright/test": "1.43.1", "@playwright/test": "1.45.2",
"@typebot.io/env": "workspace:*", "@typebot.io/env": "workspace:*",
"@typebot.io/prisma": "workspace:*", "@typebot.io/prisma": "workspace:*",
"@typebot.io/tsconfig": "workspace:*", "@typebot.io/tsconfig": "workspace:*",
@ -49,4 +49,4 @@
"wildcard-match": "5.1.3", "wildcard-match": "5.1.3",
"zod": "3.22.4" "zod": "3.22.4"
} }
} }

View File

@ -151,25 +151,6 @@ export const updateUser = (data: Partial<User>) =>
}, },
}) })
export const createWebhook = async (
typebotId: string,
webhookProps?: Partial<HttpRequest>
) => {
try {
await prisma.webhook.delete({ where: { id: 'webhook1' } })
} catch {}
return prisma.webhook.create({
data: {
method: 'GET',
typebotId,
id: 'webhook1',
...webhookProps,
queryParams: webhookProps?.queryParams ?? [],
headers: webhookProps?.headers ?? [],
},
})
}
export const createTypebots = async (partialTypebots: Partial<TypebotV6>[]) => { export const createTypebots = async (partialTypebots: Partial<TypebotV6>[]) => {
const typebotsWithId = partialTypebots.map((typebot) => { const typebotsWithId = partialTypebots.map((typebot) => {
const typebotId = typebot.id ?? createId() const typebotId = typebot.id ?? createId()

View File

@ -7,7 +7,7 @@
"author": "Baptiste Arnaud", "author": "Baptiste Arnaud",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@playwright/test": "1.43.1", "@playwright/test": "1.45.2",
"@typebot.io/lib": "workspace:*", "@typebot.io/lib": "workspace:*",
"@typebot.io/prisma": "workspace:*", "@typebot.io/prisma": "workspace:*",
"@typebot.io/schemas": "workspace:*", "@typebot.io/schemas": "workspace:*",

View File

@ -3,16 +3,26 @@ import { stripeCredentialsSchema } from './blocks/inputs/payment/schema'
import { googleSheetsCredentialsSchema } from './blocks/integrations/googleSheets/schema' import { googleSheetsCredentialsSchema } from './blocks/integrations/googleSheets/schema'
import { smtpCredentialsSchema } from './blocks/integrations/sendEmail' import { smtpCredentialsSchema } from './blocks/integrations/sendEmail'
import { whatsAppCredentialsSchema } from './whatsapp' import { whatsAppCredentialsSchema } from './whatsapp'
import { zemanticAiCredentialsSchema } from './blocks' import { forgedCredentialsSchemas } from '@typebot.io/forge-repository/credentials'
import { openAICredentialsSchema } from './blocks/integrations/openai'
export const credentialsSchema = z.discriminatedUnion('type', [ const credentialsSchema = z.discriminatedUnion('type', [
smtpCredentialsSchema, smtpCredentialsSchema,
googleSheetsCredentialsSchema, googleSheetsCredentialsSchema,
stripeCredentialsSchema, stripeCredentialsSchema,
openAICredentialsSchema,
whatsAppCredentialsSchema, whatsAppCredentialsSchema,
zemanticAiCredentialsSchema, ...Object.values(forgedCredentialsSchemas),
]) ])
export type Credentials = z.infer<typeof credentialsSchema> export type Credentials = z.infer<typeof credentialsSchema>
export const credentialsTypes = [
'smtp',
'google sheets',
'stripe',
'whatsApp',
...(Object.keys(forgedCredentialsSchemas) as Array<
keyof typeof forgedCredentialsSchemas
>),
] as const
export const credentialsTypeSchema = z.enum(credentialsTypes)

44
pnpm-lock.yaml generated
View File

@ -288,8 +288,8 @@ importers:
specifier: 2.9.2 specifier: 2.9.2
version: 2.9.2 version: 2.9.2
'@playwright/test': '@playwright/test':
specifier: 1.43.1 specifier: 1.45.2
version: 1.43.1 version: 1.45.2
'@typebot.io/billing': '@typebot.io/billing':
specifier: workspace:* specifier: workspace:*
version: link:../../ee/packages/billing version: link:../../ee/packages/billing
@ -478,8 +478,8 @@ importers:
specifier: 2.2.1 specifier: 2.2.1
version: 2.2.1 version: 2.2.1
'@playwright/test': '@playwright/test':
specifier: 1.43.1 specifier: 1.45.2
version: 1.43.1 version: 1.45.2
'@typebot.io/emails': '@typebot.io/emails':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/emails version: link:../../packages/emails
@ -1785,8 +1785,8 @@ importers:
specifier: 2.2.1 specifier: 2.2.1
version: 2.2.1 version: 2.2.1
'@playwright/test': '@playwright/test':
specifier: 1.43.1 specifier: 1.45.2
version: 1.43.1 version: 1.45.2
'@typebot.io/env': '@typebot.io/env':
specifier: workspace:* specifier: workspace:*
version: link:../env version: link:../env
@ -1856,8 +1856,8 @@ importers:
packages/playwright: packages/playwright:
dependencies: dependencies:
'@playwright/test': '@playwright/test':
specifier: 1.43.1 specifier: 1.45.2
version: 1.43.1 version: 1.45.2
'@typebot.io/env': '@typebot.io/env':
specifier: workspace:* specifier: workspace:*
version: link:../env version: link:../env
@ -4687,9 +4687,9 @@ packages:
resolution: {integrity: sha512-+zk04eXRiaJGaRnJZkCxXbBtBvQDQJXCoxqlXhLY3HzAovXfsBnh6DjXRujPRQQ7GKtT8/tOlyvZ9h6ReM+GLQ==} resolution: {integrity: sha512-+zk04eXRiaJGaRnJZkCxXbBtBvQDQJXCoxqlXhLY3HzAovXfsBnh6DjXRujPRQQ7GKtT8/tOlyvZ9h6ReM+GLQ==}
engines: {node: '>=16'} engines: {node: '>=16'}
'@playwright/test@1.43.1': '@playwright/test@1.45.2':
resolution: {integrity: sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==} resolution: {integrity: sha512-JxG9eq92ET75EbVi3s+4sYbcG7q72ECeZNbdBlaMkGcNbiDQ4cAi8U2QP5oKkOx+1gpaiL1LDStmzCaEM1Z6fQ==}
engines: {node: '>=16'} engines: {node: '>=18'}
hasBin: true hasBin: true
'@popperjs/core@2.11.8': '@popperjs/core@2.11.8':
@ -10645,14 +10645,14 @@ packages:
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
playwright-core@1.43.1: playwright-core@1.45.2:
resolution: {integrity: sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==} resolution: {integrity: sha512-ha175tAWb0dTK0X4orvBIqi3jGEt701SMxMhyujxNrgd8K0Uy5wMSwwcQHtyB4om7INUkfndx02XnQ2p6dvLDw==}
engines: {node: '>=16'} engines: {node: '>=18'}
hasBin: true hasBin: true
playwright@1.43.1: playwright@1.45.2:
resolution: {integrity: sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==} resolution: {integrity: sha512-ReywF2t/0teRvNBpfIgh5e4wnrI/8Su8ssdo5XsQKpjxJj+jspm00jSoz9BTg91TT0c9HRjXO7LBNVrgYj9X0g==}
engines: {node: '>=16'} engines: {node: '>=18'}
hasBin: true hasBin: true
pngjs@5.0.0: pngjs@5.0.0:
@ -16357,9 +16357,9 @@ snapshots:
'@planetscale/database@1.8.0': {} '@planetscale/database@1.8.0': {}
'@playwright/test@1.43.1': '@playwright/test@1.45.2':
dependencies: dependencies:
playwright: 1.43.1 playwright: 1.45.2
'@popperjs/core@2.11.8': {} '@popperjs/core@2.11.8': {}
@ -24207,11 +24207,11 @@ snapshots:
dependencies: dependencies:
find-up: 4.1.0 find-up: 4.1.0
playwright-core@1.43.1: {} playwright-core@1.45.2: {}
playwright@1.43.1: playwright@1.45.2:
dependencies: dependencies:
playwright-core: 1.43.1 playwright-core: 1.45.2
optionalDependencies: optionalDependencies:
fsevents: 2.3.2 fsevents: 2.3.2