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
iconNames.ts
reporters
.last-run.json

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ import { isDefined } from '@typebot.io/lib'
export type SwitchWithLabelProps = {
label: string
initialValue: boolean
initialValue: boolean | undefined
moreInfoContent?: string
onCheckChange?: (isChecked: boolean) => void
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,
onClose,
}: 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 { user } = useUser()
const { workspace } = useWorkspace()
@ -99,7 +114,8 @@ export const StripeConfigModal = ({
test: { ...stripeConfig.test, secretKey },
})
const createCredentials = async () => {
const createCredentials = async (e: React.FormEvent) => {
e.preventDefault()
if (!user?.email || !workspace?.id) return
mutate({
credentials: {
@ -120,16 +136,16 @@ export const StripeConfigModal = ({
},
})
}
return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>
{t('blocks.inputs.payment.settings.stripeConfig.title.label')}
</ModalHeader>
<ModalCloseButton />
<ModalContent>
<ModalHeader>
{t('blocks.inputs.payment.settings.stripeConfig.title.label')}
</ModalHeader>
<ModalCloseButton />
<form onSubmit={createCredentials}>
<ModalBody>
<Stack as="form" spacing={4}>
<Stack spacing={4}>
<TextInput
isRequired
label={t(
@ -208,8 +224,8 @@ export const StripeConfigModal = ({
<ModalFooter>
<Button
type="submit"
colorScheme="blue"
onClick={createCredentials}
isDisabled={
stripeConfig.live.publicKey === '' ||
stripeConfig.name === '' ||
@ -220,7 +236,7 @@ export const StripeConfigModal = ({
{t('connect')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</form>
</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.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="sk_test_..."]', 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 = {
isOpen: boolean
typebotId: string
blockId: string
typebotId?: string
blockId?: string
onClose: () => void
}
@ -32,30 +32,45 @@ export const GoogleSheetConnectModal = ({
isOpen,
onClose,
}: Props) => {
const { workspace } = useWorkspace()
return (
<Modal isOpen={isOpen} onClose={onClose} size="lg">
<ModalOverlay />
<ModalContent>
<ModalHeader>Connect Spreadsheets</ModalHeader>
<ModalCloseButton />
<ModalBody as={Stack} spacing="6">
<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>
<GoogleSheetConnectModalContent typebotId={typebotId} blockId={blockId} />
</Modal>
)
}
export const GoogleSheetConnectModalContent = ({
typebotId,
blockId,
}: {
typebotId?: string
blockId?: string
}) => {
const { workspace } = useWorkspace()
return (
<ModalContent>
<ModalHeader>Connect Spreadsheets</ModalHeader>
<ModalCloseButton />
<ModalBody as={Stack} spacing="6">
<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
as={Link}
leftIcon={<GoogleLogo />}
@ -64,19 +79,18 @@ export const GoogleSheetConnectModal = ({
variant="outline"
href={getGoogleSheetsConsentScreenUrlQuery(
window.location.href,
workspace.id,
blockId,
workspace?.id,
typebotId
)}
mx="auto"
>
Continue with Google
</Button>
</Flex>
</ModalBody>
<ModalFooter />
</ModalContent>
</Modal>
)}
</Flex>
</ModalBody>
<ModalFooter />
</ModalContent>
)
}

View File

@ -5,44 +5,22 @@ export const GoogleSheetsLogo = (props: IconProps) => (
<title>Sheets-icon</title>
<desc>Created with Sketch.</desc>
<defs>
<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-1"
></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>
<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"></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>
<linearGradient
x1="50.0053945%"
y1="8.58610612%"
x2="50.0053945%"
y2="100.013939%"
id="linearGradient-7"
>
<stop stopColor="#263238" stopOpacity="0.2" offset="0%"></stop>
<stop stopColor="#263238" stopOpacity="0.02" offset="100%"></stop>
</linearGradient>
<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-8"
></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>
<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"></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"></path>
<radialGradient
cx="3.16804688%"
cy="2.71744318%"
@ -50,112 +28,101 @@ export const GoogleSheetsLogo = (props: IconProps) => (
fy="2.71744318%"
r="161.248516%"
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" offset="100%"></stop>
</radialGradient>
</defs>
<g id="Page-1" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g
id="Consumer-Apps-Sheets-Large-VD-R8-"
transform="translate(-451.000000, -451.000000)"
>
<g id="Hero" transform="translate(0.000000, 63.000000)">
<g id="Personal" transform="translate(277.000000, 299.000000)">
<g id="Sheets-icon" transform="translate(174.833333, 89.958333)">
<g id="Group">
<g id="Clipped">
<mask id="mask-2" fill="white">
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g transform="translate(-451.000000, -451.000000)">
<g transform="translate(0.000000, 63.000000)">
<g transform="translate(277.000000, 299.000000)">
<g transform="translate(174.833333, 89.958333)">
<g>
<g>
<mask fill="white">
<use xlinkHref="#path-1"></use>
</mask>
<g id="SVGID_1_"></g>
<g></g>
<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"
id="Path"
fill="#0F9D58"
fillRule="nonzero"
mask="url(#mask-2)"
></path>
</g>
<g id="Clipped">
<mask id="mask-4" fill="white">
<g>
<mask fill="white">
<use xlinkHref="#path-3"></use>
</mask>
<g id="SVGID_1_"></g>
<g></g>
<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"
id="Shape"
fill="#F1F1F1"
fillRule="nonzero"
mask="url(#mask-4)"
></path>
</g>
<g id="Clipped">
<mask id="mask-6" fill="white">
<g>
<mask fill="white">
<use xlinkHref="#path-5"></use>
</mask>
<g id="SVGID_1_"></g>
<g></g>
<polygon
id="Path"
fill="url(#linearGradient-7)"
fillRule="nonzero"
mask="url(#mask-6)"
points="30.8813021 16.4520313 47.3333333 32.9003646 47.3333333 17.75"
></polygon>
</g>
<g id="Clipped">
<mask id="mask-9" fill="white">
<g>
<mask fill="white">
<use xlinkHref="#path-8"></use>
</mask>
<g id="SVGID_1_"></g>
<g id="Group" mask="url(#mask-9)">
<g></g>
<g mask="url(#mask-9)">
<g transform="translate(26.625000, -2.958333)">
<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"
id="Path"
fill="#87CEAC"
fillRule="nonzero"
></path>
</g>
</g>
</g>
<g id="Clipped">
<mask id="mask-11" fill="white">
<g>
<mask fill="white">
<use xlinkHref="#path-10"></use>
</mask>
<g id="SVGID_1_"></g>
<g></g>
<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"
id="Path"
fillOpacity="0.2"
fill="#FFFFFF"
fillRule="nonzero"
mask="url(#mask-11)"
></path>
</g>
<g id="Clipped">
<mask id="mask-13" fill="white">
<g>
<mask fill="white">
<use xlinkHref="#path-12"></use>
</mask>
<g id="SVGID_1_"></g>
<g></g>
<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"
id="Path"
fillOpacity="0.2"
fill="#263238"
fillRule="nonzero"
mask="url(#mask-13)"
></path>
</g>
<g id="Clipped">
<mask id="mask-15" fill="white">
<g>
<mask fill="white">
<use xlinkHref="#path-14"></use>
</mask>
<g id="SVGID_1_"></g>
<g></g>
<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"
id="Path"
fillOpacity="0.1"
fill="#263238"
fillRule="nonzero"
@ -165,7 +132,6 @@ export const GoogleSheetsLogo = (props: IconProps) => (
</g>
<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"
fill="url(#radialGradient-16)"
fillRule="nonzero"
></path>

View File

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

View File

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

View File

@ -26,9 +26,25 @@ type Props = {
export const SmtpConfigModal = ({
isOpen,
onNewCredentials,
onClose,
onNewCredentials,
}: 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 { workspace } = useWorkspace()
const [isCreating, setIsCreating] = useState(false)
@ -53,11 +69,11 @@ export const SmtpConfigModal = ({
onSuccess: (data) => {
refetchCredentials()
onNewCredentials(data.credentialsId)
onClose()
},
})
const handleCreateClick = async () => {
const handleCreateClick = async (e: React.FormEvent) => {
e.preventDefault()
if (!user?.email || !workspace?.id) return
setIsCreating(true)
const { error: testSmtpError } = await testSmtpConfig(
@ -82,19 +98,18 @@ export const SmtpConfigModal = ({
})
}
return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Create SMTP config</ModalHeader>
<ModalCloseButton />
<ModalContent>
<ModalHeader>Create SMTP config</ModalHeader>
<ModalCloseButton />
<form onSubmit={handleCreateClick}>
<ModalBody>
<SmtpConfigForm config={smtpConfig} onConfigChange={setSmtpConfig} />
</ModalBody>
<ModalFooter>
<Button
type="submit"
colorScheme="blue"
onClick={handleCreateClick}
isDisabled={
isNotDefined(smtpConfig.from.email) ||
isNotDefined(smtpConfig.host) ||
@ -107,7 +122,7 @@ export const SmtpConfigModal = ({
Create
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</form>
</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 {
createWebhook,
importTypebotInDatabase,
} from '@typebot.io/playwright/databaseActions'
import { importTypebotInDatabase } from '@typebot.io/playwright/databaseActions'
import { createId } from '@paralleldrive/cuid2'
import { getTestAsset } from '@/test/utils/playwright'
import { apiToken } from '@typebot.io/playwright/databaseSetup'
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('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.describe.configure({ mode: 'parallel' })
test('its configuration should work', async ({ page }) => {
const typebotId = createId()
await importTypebotInDatabase(
getTestAsset('typebots/integrations/webhook.json'),
{
id: typebotId,
}
)
await createWebhook(typebotId)
test('editor configuration should work', async ({ page }) => {
const typebotId = createId()
await importTypebotInDatabase(
getTestAsset('typebots/integrations/webhook.json'),
{
id: typebotId,
}
)
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Configure...')
await page.fill(
'input[placeholder="Paste URL..."]',
`${env.NEXTAUTH_URL}/api/mock/webhook`
)
await page.click('text=Advanced configuration')
await page.getByRole('button', { name: 'GET' }).click()
await page.click('text=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": "Go", "secret 1": "content"`,
{ timeout: 10000 }
)
await page.click('text=Query params')
await page.click('text=Add a param')
await page.fill('input[placeholder="e.g. email"]', 'firstParam')
await page.fill('input[placeholder="e.g. {{Email}}"]', '{{secret 1}}')
await page.fill(
'input[placeholder="Paste URL..."]',
`${env.NEXTAUTH_URL}/api/mock/webhook`
)
await page.click('text=Advanced configuration')
await page.click('text=Add a param')
await page.fill('input[placeholder="e.g. email"] >> nth=1', 'secondParam')
await page.fill(
'input[placeholder="e.g. {{Email}}"] >> nth=1',
'{{secret 2}}'
)
await page.click('text=Query params')
await page.click('text=Add a param')
await page.fill('input[placeholder="e.g. email"]', 'firstParam')
await page.fill('input[placeholder="e.g. {{Email}}"]', '{{secret 1}}')
await page.click('text=Headers')
await page.waitForTimeout(200)
await page.getByRole('button', { name: 'Add a value' }).click()
await page.fill('input[placeholder="e.g. Content-Type"]', 'Custom-Typebot')
await page.fill(
'input[placeholder="e.g. application/json"]',
'{{secret 3}}'
)
await page.click('text=Add a param')
await page.fill('input[placeholder="e.g. email"] >> nth=1', 'secondParam')
await page.fill(
'input[placeholder="e.g. {{Email}}"] >> nth=1',
'{{secret 2}}'
)
await page.click('text=Body')
await page.click('text=Custom body')
await page.fill('div[role="textbox"]', '{ "customField": "{{secret 4}}" }')
await page.click('text=Headers')
await page.waitForTimeout(200)
await page.getByRole('button', { name: 'Add a value' }).click()
await page.fill('input[placeholder="e.g. Content-Type"]', 'Custom-Typebot')
await page.fill('input[placeholder="e.g. application/json"]', '{{secret 3}}')
await page.click('text=Variable values for test')
await addTestVariable(page, 'secret 1', 'secret1')
await addTestVariable(page, 'secret 2', 'secret2')
await addTestVariable(page, 'secret 3', 'secret3')
await addTestVariable(page, 'secret 4', 'secret4')
await page.click('text=Body')
await page.click('text=Custom body')
await page.fill('div[role="textbox"]', '{ "customField": "{{secret 4}}" }')
await page.click('text=Test the request')
await expect(page.locator('div[role="textbox"] >> nth=-1')).toContainText(
'"statusCode": 200'
)
await page.click('text=Variable values for test')
await addTestVariable(page, 'secret 1', 'secret1')
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=Add an entry >> nth=-1')
await page.click('input[placeholder="Select the data"]')
await page.click('text=data.flatMap(item => item.name)')
})
await page.click('text=Test the request')
await expect(page.locator('div[role="textbox"] >> nth=-1')).toContainText(
'"statusCode": 200'
)
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) => {
@ -102,83 +81,70 @@ const addTestVariable = async (page: Page, name: string, value: string) => {
await page.fill('input >> nth=-1', value)
}
test.describe('API', () => {
const typebotId = 'webhook-flow'
test('Webhook API endpoints should work', async ({ request }) => {
const typebotId = createId()
await importTypebotInDatabase(getTestAsset('typebots/api.json'), {
id: typebotId,
})
test.beforeAll(async () => {
try {
await importTypebotInDatabase(getTestAsset('typebots/api.json'), {
id: typebotId,
})
await createWebhook(typebotId)
} catch (err) {
console.log(err)
// GET webhook blocks
const getResponse = await request.get(
`/api/v1/typebots/${typebotId}/webhookBlocks`,
{
headers: { Authorization: `Bearer ${apiToken}` },
}
)
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 }) => {
const response = await request.get(
`/api/v1/typebots/${typebotId}/webhookBlocks`,
{
headers: { Authorization: `Bearer ${apiToken}` },
}
)
const { webhookBlocks } = await response.json()
expect(webhookBlocks).toHaveLength(1)
expect(webhookBlocks[0]).toEqual({
id: 'webhookBlock',
label: 'Webhook > webhookBlock',
type: 'Webhook',
})
// Subscribe webhook
const url = 'https://test.com'
const subscribeResponse = await request.post(
`/api/v1/typebots/${typebotId}/webhookBlocks/webhookBlock/subscribe`,
{
headers: {
Authorization: `Bearer ${apiToken}`,
},
data: { url },
}
)
expect(await subscribeResponse.json()).toEqual({
id: 'webhookBlock',
url,
})
test('can subscribe webhook', async ({ request }) => {
const url = 'https://test.com'
const response = await request.post(
`/api/v1/typebots/${typebotId}/webhookBlocks/webhookBlock/subscribe`,
{
headers: {
Authorization: `Bearer ${apiToken}`,
},
data: { url },
}
)
const body = await response.json()
expect(body).toEqual({
id: 'webhookBlock',
url,
})
// Unsubscribe webhook
const unsubResponse = await request.post(
`/api/v1/typebots/${typebotId}/webhookBlocks/webhookBlock/unsubscribe`,
{
headers: { Authorization: `Bearer ${apiToken}` },
}
)
expect(await unsubResponse.json()).toEqual({
id: 'webhookBlock',
url: null,
})
test('can unsubscribe webhook', async ({ request }) => {
const response = await request.post(
`/api/v1/typebots/${typebotId}/webhookBlocks/webhookBlock/unsubscribe`,
{
headers: { Authorization: `Bearer ${apiToken}` },
}
)
const body = await response.json()
expect(body).toEqual({
id: 'webhookBlock',
url: null,
})
})
// Get sample result
const sampleResponse = await request.get(
`/api/v1/typebots/${typebotId}/webhookBlocks/webhookBlock/getResultExample`,
{
headers: { Authorization: `Bearer ${apiToken}` },
}
)
const sample = await sampleResponse.json()
test('can get a sample result', async ({ request }) => {
const response = await request.get(
`/api/v1/typebots/${typebotId}/webhookBlocks/webhookBlock/getResultExample`,
{
headers: { Authorization: `Bearer ${apiToken}` },
}
)
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',
})
expect(omit(sample.resultExample, 'submittedAt')).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 { CredentialsDropdown } from '@/features/credentials/components/CredentialsDropdown'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import {
Accordion,
AccordionButton,
@ -9,15 +7,12 @@ import {
AccordionPanel,
Stack,
Text,
useDisclosure,
} from '@chakra-ui/react'
import { isEmpty } from '@typebot.io/lib'
import { ZemanticAiBlock } from '@typebot.io/schemas'
import { ZemanticAiCredentialsModal } from './ZemanticAiCredentialsModal'
import { ProjectsDropdown } from './ProjectsDropdown'
import { SearchResponseItem } from './SearchResponseItem'
import { TableList } from '@/components/TableList'
import { createId } from '@paralleldrive/cuid2'
type Props = {
block: ZemanticAiBlock
@ -28,22 +23,6 @@ export const ZemanticAiSettings = ({
block: { id: blockId, options },
onOptionsChange,
}: 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) => {
onOptionsChange({
...options,
@ -92,23 +71,6 @@ export const ZemanticAiSettings = ({
return (
<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 && (
<>
<ProjectsDropdown

View File

@ -1,7 +1,6 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
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 { encrypt } from '@typebot.io/lib/api/encryption/encrypt'
import { z } from 'zod'
@ -10,11 +9,11 @@ import {
Credentials,
googleSheetsCredentialsSchema,
stripeCredentialsSchema,
zemanticAiCredentialsSchema,
} from '@typebot.io/schemas'
import { isDefined } from '@typebot.io/lib/utils'
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden'
import { trackEvents } from '@typebot.io/telemetry/trackEvents'
import { forgedCredentialsSchemas } from '@typebot.io/forge-repository/credentials'
const inputShape = {
data: true,
@ -40,9 +39,10 @@ export const createCredentials = authenticatedProcedure
stripeCredentialsSchema.pick(inputShape),
smtpCredentialsSchema.pick(inputShape),
googleSheetsCredentialsSchema.pick(inputShape),
openAICredentialsSchema.pick(inputShape),
whatsAppCredentialsSchema.pick(inputShape),
zemanticAiCredentialsSchema.pick(inputShape),
...Object.values(forgedCredentialsSchemas).map((i) =>
i.pick(inputShape)
),
])
.and(z.object({ id: z.string().cuid2().optional() })),
})
@ -53,7 +53,12 @@ export const createCredentials = authenticatedProcedure
})
)
.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({
code: 'CONFLICT',
message: 'Credentials already exist.',
@ -62,7 +67,7 @@ export const createCredentials = authenticatedProcedure
where: {
id: credentials.workspaceId,
},
select: { id: true, members: true },
select: { id: true, members: { select: { userId: true, role: true } } },
})
if (!workspace || isWriteWorkspaceForbidden(workspace, user))
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({
where: {
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))
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 { authenticatedProcedure } from '@/helpers/server/trpc'
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 { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
import { whatsAppCredentialsSchema } from '@typebot.io/schemas/features/whatsapp'
import { zemanticAiCredentialsSchema } from '@typebot.io/schemas/features/blocks/integrations/zemanticAi'
import {
googleSheetsCredentialsSchema,
stripeCredentialsSchema,
} from '@typebot.io/schemas'
import { credentialsTypeSchema } from '@typebot.io/schemas'
import { isDefined } from '@udecode/plate-common'
const outputCredentialsSchema = z.array(
z.object({
id: z.string(),
type: credentialsTypeSchema,
name: z.string(),
})
)
export const listCredentials = authenticatedProcedure
.meta({
@ -25,17 +27,12 @@ export const listCredentials = authenticatedProcedure
.input(
z.object({
workspaceId: z.string(),
type: stripeCredentialsSchema.shape.type
.or(smtpCredentialsSchema.shape.type)
.or(googleSheetsCredentialsSchema.shape.type)
.or(openAICredentialsSchema.shape.type)
.or(whatsAppCredentialsSchema.shape.type)
.or(zemanticAiCredentialsSchema.shape.type),
type: credentialsTypeSchema.optional(),
})
)
.output(
z.object({
credentials: z.array(z.object({ id: z.string(), name: z.string() })),
credentials: outputCredentialsSchema,
})
)
.query(async ({ input: { workspaceId, type }, ctx: { user } }) => {
@ -52,6 +49,7 @@ export const listCredentials = authenticatedProcedure
},
select: {
id: true,
type: true,
name: true,
},
},
@ -60,5 +58,11 @@ export const listCredentials = authenticatedProcedure
if (!workspace || isReadWorkspaceFobidden(workspace, user))
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 { deleteCredentials } from './deleteCredentials'
import { listCredentials } from './listCredentials'
import { updateCredentials } from './updateCredentials'
import { getCredentials } from './getCredentials'
export const credentialsRouter = router({
createCredentials,
listCredentials,
getCredentials,
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 { WorkspaceSettingsModal } from '@/features/workspace/components/WorkspaceSettingsModal'
import { ParentModalProvider } from '@/features/graph/providers/ParentModalProvider'
import { useRouter } from 'next/router'
export const DashboardHeader = () => {
const { t } = useTranslate()
const { user, logOut } = useUser()
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 = () =>
createWorkspace(user?.name ?? undefined)
@ -45,6 +51,9 @@ export const DashboardHeader = () => {
onClose={onClose}
user={user}
workspace={workspace}
defaultTab={
isRedirectFromCredentialsCreation ? 'credentials' : undefined
}
/>
</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 { FlagIcon, SendEmailIcon, ThunderIcon } from '@/components/icons'
import { WaitIcon } from '@/features/blocks/logic/wait/components/WaitIcon'
import { ScriptIcon } from '@/features/blocks/logic/script/components/ScriptIcon'
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 { OpenAILogo } from '@/features/blocks/integrations/openai/components/OpenAILogo'
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 orange = useColorModeValue('orange.500', 'orange.300')
const purple = useColorModeValue('purple.500', 'purple.300')
@ -51,78 +52,78 @@ export const BlockIcon = ({ type, mt }: BlockIconProps): JSX.Element => {
switch (type) {
case BubbleBlockType.TEXT:
return <TextBubbleIcon color={blue} mt={mt} />
return <TextBubbleIcon color={blue} {...props} />
case BubbleBlockType.IMAGE:
return <ImageBubbleIcon color={blue} mt={mt} />
return <ImageBubbleIcon color={blue} {...props} />
case BubbleBlockType.VIDEO:
return <VideoBubbleIcon color={blue} mt={mt} />
return <VideoBubbleIcon color={blue} {...props} />
case BubbleBlockType.EMBED:
return <EmbedBubbleIcon color={blue} mt={mt} />
return <EmbedBubbleIcon color={blue} {...props} />
case BubbleBlockType.AUDIO:
return <AudioBubbleIcon color={blue} mt={mt} />
return <AudioBubbleIcon color={blue} {...props} />
case InputBlockType.TEXT:
return <TextInputIcon color={orange} mt={mt} />
return <TextInputIcon color={orange} {...props} />
case InputBlockType.NUMBER:
return <NumberInputIcon color={orange} mt={mt} />
return <NumberInputIcon color={orange} {...props} />
case InputBlockType.EMAIL:
return <EmailInputIcon color={orange} mt={mt} />
return <EmailInputIcon color={orange} {...props} />
case InputBlockType.URL:
return <UrlInputIcon color={orange} mt={mt} />
return <UrlInputIcon color={orange} {...props} />
case InputBlockType.DATE:
return <DateInputIcon color={orange} mt={mt} />
return <DateInputIcon color={orange} {...props} />
case InputBlockType.PHONE:
return <PhoneInputIcon color={orange} mt={mt} />
return <PhoneInputIcon color={orange} {...props} />
case InputBlockType.CHOICE:
return <ButtonsInputIcon color={orange} mt={mt} />
return <ButtonsInputIcon color={orange} {...props} />
case InputBlockType.PICTURE_CHOICE:
return <PictureChoiceIcon color={orange} mt={mt} />
return <PictureChoiceIcon color={orange} {...props} />
case InputBlockType.PAYMENT:
return <PaymentInputIcon color={orange} mt={mt} />
return <PaymentInputIcon color={orange} {...props} />
case InputBlockType.RATING:
return <RatingInputIcon color={orange} mt={mt} />
return <RatingInputIcon color={orange} {...props} />
case InputBlockType.FILE:
return <FileInputIcon color={orange} mt={mt} />
return <FileInputIcon color={orange} {...props} />
case LogicBlockType.SET_VARIABLE:
return <SetVariableIcon color={purple} mt={mt} />
return <SetVariableIcon color={purple} {...props} />
case LogicBlockType.CONDITION:
return <ConditionIcon color={purple} mt={mt} />
return <ConditionIcon color={purple} {...props} />
case LogicBlockType.REDIRECT:
return <RedirectIcon color={purple} mt={mt} />
return <RedirectIcon color={purple} {...props} />
case LogicBlockType.SCRIPT:
return <ScriptIcon mt={mt} />
return <ScriptIcon {...props} />
case LogicBlockType.WAIT:
return <WaitIcon color={purple} mt={mt} />
return <WaitIcon color={purple} {...props} />
case LogicBlockType.JUMP:
return <JumpIcon color={purple} mt={mt} />
return <JumpIcon color={purple} {...props} />
case LogicBlockType.TYPEBOT_LINK:
return <TypebotLinkIcon color={purple} mt={mt} />
return <TypebotLinkIcon color={purple} {...props} />
case LogicBlockType.AB_TEST:
return <AbTestIcon color={purple} mt={mt} />
return <AbTestIcon color={purple} {...props} />
case IntegrationBlockType.GOOGLE_SHEETS:
return <GoogleSheetsLogo mt={mt} />
return <GoogleSheetsLogo {...props} />
case IntegrationBlockType.GOOGLE_ANALYTICS:
return <GoogleAnalyticsLogo mt={mt} />
return <GoogleAnalyticsLogo {...props} />
case IntegrationBlockType.WEBHOOK:
return <ThunderIcon mt={mt} />
return <ThunderIcon {...props} />
case IntegrationBlockType.ZAPIER:
return <ZapierLogo mt={mt} />
return <ZapierLogo {...props} />
case IntegrationBlockType.MAKE_COM:
return <MakeComLogo mt={mt} />
return <MakeComLogo {...props} />
case IntegrationBlockType.PABBLY_CONNECT:
return <PabblyConnectLogo mt={mt} />
return <PabblyConnectLogo {...props} />
case IntegrationBlockType.EMAIL:
return <SendEmailIcon mt={mt} />
return <SendEmailIcon {...props} />
case IntegrationBlockType.CHATWOOT:
return <ChatwootLogo mt={mt} />
return <ChatwootLogo {...props} />
case IntegrationBlockType.PIXEL:
return <PixelLogo mt={mt} />
return <PixelLogo {...props} />
case IntegrationBlockType.ZEMANTIC_AI:
return <ZemanticAiLogo mt={mt} />
return <ZemanticAiLogo {...props} />
case 'start':
return <FlagIcon mt={mt} />
return <FlagIcon {...props} />
case IntegrationBlockType.OPEN_AI:
return <OpenAILogo mt={mt} fill={openAIColor} />
return <OpenAILogo {...props} fill={openAIColor} />
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 { useTranslate } from '@tolgee/react'
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 { 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()
switch (type) {
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 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:
return <Text fontSize="sm">{t('editor.sidebarBlock.image.label')}</Text>
return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.image.label')}
</Text>
)
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:
return <Text fontSize="sm">{t('editor.sidebarBlock.embed.label')}</Text>
return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.embed.label')}
</Text>
)
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:
return <Text fontSize="sm">{t('editor.sidebarBlock.number.label')}</Text>
return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.number.label')}
</Text>
)
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:
return <Text fontSize="sm">{t('editor.sidebarBlock.website.label')}</Text>
return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.website.label')}
</Text>
)
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:
return <Text fontSize="sm">{t('editor.sidebarBlock.phone.label')}</Text>
return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.phone.label')}
</Text>
)
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:
return (
<Text fontSize="sm">{t('editor.sidebarBlock.picChoice.label')}</Text>
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.picChoice.label')}
</Text>
)
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:
return <Text fontSize="sm">{t('editor.sidebarBlock.rating.label')}</Text>
return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.rating.label')}
</Text>
)
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:
return (
<Text fontSize="sm">{t('editor.sidebarBlock.setVariable.label')}</Text>
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.setVariable.label')}
</Text>
)
case LogicBlockType.CONDITION:
return (
<Text fontSize="sm">{t('editor.sidebarBlock.condition.label')}</Text>
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.condition.label')}
</Text>
)
case LogicBlockType.REDIRECT:
return (
<Text fontSize="sm">{t('editor.sidebarBlock.redirect.label')}</Text>
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.redirect.label')}
</Text>
)
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:
return <Text fontSize="sm">{t('editor.sidebarBlock.typebot.label')}</Text>
return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.typebot.label')}
</Text>
)
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:
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:
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:
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:
return (
<Text fontSize="sm">{t('editor.sidebarBlock.analytics.label')}</Text>
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.analytics.label')}
</Text>
)
case IntegrationBlockType.WEBHOOK:
return <Text fontSize="sm">HTTP request</Text>
return (
<Text fontSize="sm" {...props}>
HTTP request
</Text>
)
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:
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:
return <Text fontSize="sm">{t('editor.sidebarBlock.pabbly.label')}</Text>
return (
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.pabbly.label')}
</Text>
)
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:
return (
<Text fontSize="sm">{t('editor.sidebarBlock.chatwoot.label')}</Text>
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.chatwoot.label')}
</Text>
)
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:
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:
return (
<Text fontSize="sm">{t('editor.sidebarBlock.zemanticAi.label')}</Text>
<Text fontSize="sm" {...props}>
{t('editor.sidebarBlock.zemanticAi.label')}
</Text>
)
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 { useForgedBlock } from './hooks/useForgedBlock'
export const ForgedBlockIcon = ({
type,
mt,
...props
}: {
type: ForgedBlock['type']
mt?: string
}): JSX.Element => {
} & IconProps): JSX.Element => {
const { colorMode } = useColorMode()
const { blockDef } = useForgedBlock(type)
if (!blockDef) return <></>
if (colorMode === 'dark' && blockDef.DarkLogo)
return <blockDef.DarkLogo width="1rem" style={{ marginTop: mt }} />
return <blockDef.LightLogo width="1rem" style={{ marginTop: mt }} />
return (
<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 { 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)
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 { fetchSelectItems } from './fetchSelectItems'
import { createCredentials } from './credentials/createCredentials'
import { deleteCredentials } from './credentials/deleteCredentials'
import { listCredentials } from './credentials/listCredentials'
export const forgeRouter = router({
fetchSelectItems,
createCredentials,
listCredentials,
deleteCredentials,
})

View File

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

View File

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

View File

@ -16,6 +16,7 @@ import { trpc } from '@/lib/trpc'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { ForgedBlockDefinition } from '@typebot.io/forge-repository/types'
import { useToast } from '@/hooks/useToast'
import { Credentials } from '@typebot.io/schemas/features/credentials'
type Props = Omit<ButtonProps, 'type'> & {
blockDef: ForgedBlockDefinition
@ -34,13 +35,14 @@ export const ForgedCredentialsDropdown = ({
const router = useRouter()
const { showToast } = useToast()
const { workspace, currentRole } = useWorkspace()
const { data, refetch, isLoading } = trpc.forge.listCredentials.useQuery(
{
workspaceId: workspace?.id as string,
type: blockDef.id,
},
{ enabled: !!workspace?.id }
)
const { data, refetch, isLoading } =
trpc.credentials.listCredentials.useQuery(
{
workspaceId: workspace?.id as string,
type: blockDef.id as Credentials['type'],
},
{ enabled: !!workspace?.id }
)
const [isDeleting, setIsDeleting] = useState<string>()
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,
onNewCredentials,
}: 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 { showToast } = useToast()
const { activeStep, goToNext, goToPrevious, setActiveStep } = useSteps({
@ -226,82 +241,78 @@ export const WhatsAppCredentialsModal = ({
goToNext()
}
return (
<Modal isOpen={isOpen} onClose={onClose} size="3xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>
<HStack h="40px">
{activeStep > 0 && (
<IconButton
icon={<ChevronLeftIcon />}
aria-label={'Go back'}
variant="ghost"
onClick={goToPrevious}
/>
)}
<Heading size="md">Add a WhatsApp phone number</Heading>
</HStack>
</ModalHeader>
<ModalCloseButton />
<ModalBody as={Stack} spacing="10">
<Stepper index={activeStep} size="sm" pt="4">
{steps.map((step, index) => (
<Step key={index}>
<StepIndicator>
<StepStatus
complete={<StepIcon />}
incomplete={<StepNumber />}
active={<StepNumber />}
/>
</StepIndicator>
<ModalContent>
<ModalHeader>
<HStack h="40px">
{activeStep > 0 && (
<IconButton
icon={<ChevronLeftIcon />}
aria-label={'Go back'}
variant="ghost"
onClick={goToPrevious}
/>
)}
<Heading size="md">Add a WhatsApp phone number</Heading>
</HStack>
</ModalHeader>
<ModalCloseButton />
<ModalBody as={Stack} spacing="10">
<Stepper index={activeStep} size="sm" pt="4">
{steps.map((step, index) => (
<Step key={index}>
<StepIndicator>
<StepStatus
complete={<StepIcon />}
incomplete={<StepNumber />}
active={<StepNumber />}
/>
</StepIndicator>
<Box flexShrink="0">
<StepTitle>{step.title}</StepTitle>
</Box>
<Box flexShrink="0">
<StepTitle>{step.title}</StepTitle>
</Box>
<StepSeparator />
</Step>
))}
</Stepper>
{activeStep === 0 && <Requirements />}
{activeStep === 1 && (
<SystemUserToken
initialToken={systemUserAccessToken}
setToken={setSystemUserAccessToken}
/>
)}
{activeStep === 2 && (
<PhoneNumber
appId={tokenInfoData?.appId}
initialPhoneNumberId={phoneNumberId}
setPhoneNumberId={setPhoneNumberId}
/>
)}
{activeStep === 3 && (
<Webhook
appId={tokenInfoData?.appId}
verificationToken={verificationToken}
credentialsId={credentialsId}
/>
)}
</ModalBody>
<ModalFooter>
<Button
onClick={goToNextStep}
colorScheme="blue"
isDisabled={
(activeStep === 1 && isEmpty(systemUserAccessToken)) ||
(activeStep === 2 && isEmpty(phoneNumberId))
}
isLoading={isVerifying || isCreating}
>
{activeStep === steps.length - 1 ? 'Submit' : 'Continue'}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
<StepSeparator />
</Step>
))}
</Stepper>
{activeStep === 0 && <Requirements />}
{activeStep === 1 && (
<SystemUserToken
initialToken={systemUserAccessToken}
setToken={setSystemUserAccessToken}
/>
)}
{activeStep === 2 && (
<PhoneNumber
appId={tokenInfoData?.appId}
initialPhoneNumberId={phoneNumberId}
setPhoneNumberId={setPhoneNumberId}
/>
)}
{activeStep === 3 && (
<Webhook
appId={tokenInfoData?.appId}
verificationToken={verificationToken}
credentialsId={credentialsId}
/>
)}
</ModalBody>
<ModalFooter>
<Button
onClick={goToNextStep}
colorScheme="blue"
isDisabled={
(activeStep === 1 && isEmpty(systemUserAccessToken)) ||
(activeStep === 2 && isEmpty(phoneNumberId))
}
isLoading={isVerifying || isCreating}
>
{activeStep === steps.length - 1 ? 'Submit' : 'Continue'}
</Button>
</ModalFooter>
</ModalContent>
)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,32 +1,23 @@
{
"id": "ckz8gli9e9842no1afuppdn0z",
"createdAt": "2022-02-04T13:44:30.386Z",
"updatedAt": "2022-02-04T13:44:30.386Z",
"version": "6",
"id": "clyoe6owl0003grw9k9hzc9qs",
"name": "My typebot",
"folderId": null,
"groups": [
"events": [
{
"id": "p6GeeRXHgwiJeoJRBkKaMJ",
"blocks": [
{
"id": "iDS7jFemUsQ7Sp3eu3xg3w",
"type": "start",
"label": "Start",
"groupId": "p6GeeRXHgwiJeoJRBkKaMJ",
"outgoingEdgeId": "cyEJPaLU7AchnBSaeWoyiS"
}
],
"title": "Start",
"graphCoordinates": { "x": 0, "y": 0 }
},
"outgoingEdgeId": "cyEJPaLU7AchnBSaeWoyiS",
"graphCoordinates": { "x": 0, "y": 0 },
"type": "start"
}
],
"groups": [
{
"id": "kBneEpKdMYrF65XxUQ5GS7",
"graphCoordinates": { "x": 260, "y": 186 },
"title": "Group #1",
"graphCoordinates": { "x": 260, "y": 186 },
"blocks": [
{
"id": "skSkZ4PNP7m1gYvu9Ew6ngM",
"groupId": "kBneEpKdMYrF65XxUQ5GS7",
"type": "text",
"content": {
"richText": [{ "type": "p", "children": [{ "text": "Ready?" }] }]
@ -34,85 +25,89 @@
},
{
"id": "sh6ZVRA3o72y6BEiNKVcoma",
"groupId": "kBneEpKdMYrF65XxUQ5GS7",
"type": "choice input",
"options": { "buttonLabel": "Send", "isMultipleChoice": false },
"items": [
{
"id": "rr5mKKBPq73ZrfXZ3uuupz",
"blockId": "sh6ZVRA3o72y6BEiNKVcoma",
"type": 0,
"content": "Go",
"outgoingEdgeId": "1sLicz8gq2QxytFTwBd8ac"
"outgoingEdgeId": "1sLicz8gq2QxytFTwBd8ac",
"content": "Go"
}
]
],
"options": { "isMultipleChoice": false, "buttonLabel": "Send" }
}
]
},
{
"id": "8XnDM1QsqPms4LQHh8q3Jo",
"graphCoordinates": { "x": 646, "y": 511 },
"title": "Group #2",
"graphCoordinates": { "x": 646, "y": 511 },
"blocks": [
{
"id": "soSmiE7zyb3WF77GxFxAjYX",
"groupId": "8XnDM1QsqPms4LQHh8q3Jo",
"type": "Webhook",
"options": {
"responseVariableMapping": [],
"variablesForTest": [],
"responseVariableMapping": [],
"isAdvancedConfig": false,
"isCustomBody": false
},
"webhookId": "webhook1"
"isCustomBody": false,
"webhook": { "method": "POST" }
}
}
]
}
],
"edges": [
{
"id": "cyEJPaLU7AchnBSaeWoyiS",
"from": { "eventId": "p6GeeRXHgwiJeoJRBkKaMJ" },
"to": { "groupId": "kBneEpKdMYrF65XxUQ5GS7" }
},
{
"id": "1sLicz8gq2QxytFTwBd8ac",
"from": {
"blockId": "sh6ZVRA3o72y6BEiNKVcoma",
"itemId": "rr5mKKBPq73ZrfXZ3uuupz"
},
"to": { "groupId": "8XnDM1QsqPms4LQHh8q3Jo" }
}
],
"variables": [
{ "id": "var1", "name": "secret 1" },
{ "id": "var2", "name": "secret 2" },
{ "id": "var3", "name": "secret 3" },
{ "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": {
"general": { "font": "Open Sans", "background": { "type": "None" } },
"chat": {
"hostBubbles": { "backgroundColor": "#F7F8FF", "color": "#303235" },
"guestBubbles": { "backgroundColor": "#FF8E21", "color": "#FFFFFF" },
"buttons": { "backgroundColor": "#0042DA", "color": "#FFFFFF" },
"inputs": {
"color": "#303235",
"backgroundColor": "#FFFFFF",
"color": "#303235",
"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": {
"general": { "isBrandingEnabled": true },
"typingEmulation": { "enabled": true, "speed": 300, "maxDelay": 1.5 },
"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."
},
"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": {
"@faire/mjml-react": "3.3.0",
"@paralleldrive/cuid2": "2.2.1",
"@playwright/test": "1.43.1",
"@playwright/test": "1.45.2",
"@typebot.io/emails": "workspace:*",
"@typebot.io/env": "workspace:*",
"@typebot.io/forge": "workspace:*",

View File

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

View File

@ -1,28 +1,20 @@
{
"id": "chat-sub-bot",
"createdAt": "2022-11-24T09:06:52.903Z",
"updatedAt": "2022-11-24T09:13:16.782Z",
"icon": "👶",
"version": "6",
"id": "clyoehfmp0007grw9ubdop6u0",
"name": "Sub bot",
"folderId": null,
"groups": [
"events": [
{
"id": "clauup2lh0002vs1a5ei32mmi",
"title": "Start",
"blocks": [
{
"id": "clauup2li0003vs1aas14fwpc",
"type": "start",
"label": "Start",
"groupId": "clauup2lh0002vs1a5ei32mmi",
"outgoingEdgeId": "clauupl9n001b3b6qdk4czgom"
}
],
"graphCoordinates": { "x": 0, "y": 0 }
},
"outgoingEdgeId": "clauupl9n001b3b6qdk4czgom",
"graphCoordinates": { "x": 0, "y": 0 },
"type": "start"
}
],
"groups": [
{
"id": "clauupd6q00183b6qcm8qbz62",
"title": "Group #1",
"graphCoordinates": { "x": 375.36328125, "y": 167.2578125 },
"blocks": [
{
"id": "clauupd6q00193b6qhegmlnxj",
@ -36,69 +28,69 @@
]
}
]
},
"groupId": "clauupd6q00183b6qcm8qbz62"
}
},
{
"id": "clauupk97001a3b6q2w9qqkec",
"type": "rating input",
"groupId": "clauupd6q00183b6qcm8qbz62",
"options": {
"labels": { "button": "Send" },
"length": 10,
"buttonType": "Numbers",
"length": 10,
"labels": { "button": "Send" },
"customIcon": { "isEnabled": false }
}
}
],
"graphCoordinates": { "x": 375.36328125, "y": 167.2578125 }
]
}
],
"variables": [],
"edges": [
{
"id": "clauupl9n001b3b6qdk4czgom",
"to": { "groupId": "clauupd6q00183b6qcm8qbz62" },
"from": {
"blockId": "clauup2li0003vs1aas14fwpc",
"groupId": "clauup2lh0002vs1a5ei32mmi"
}
"from": { "eventId": "clauup2lh0002vs1a5ei32mmi" },
"to": { "groupId": "clauupd6q00183b6qcm8qbz62" }
}
],
"variables": [],
"theme": {
"general": { "font": "Open Sans", "background": { "type": "None" } },
"chat": {
"inputs": {
"color": "#303235",
"backgroundColor": "#FFFFFF",
"placeholderColor": "#9095A0"
},
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
"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" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
},
"general": { "font": "Open Sans", "background": { "type": "None" } }
"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": {
"general": {
"isBrandingEnabled": false,
"isInputPrefillEnabled": true,
"isResultSavingEnabled": true,
"isHideQueryParamsEnabled": true,
"isNewResultOnRefreshEnabled": false
},
"typingEmulation": { "enabled": true, "speed": 300, "maxDelay": 1.5 },
"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."
},
"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,
"customDomain": null,
"workspaceId": "proWorkspace",
"resultsTablePreferences": null,
"isArchived": false,
"isClosed": false
"isClosed": false,
"whatsAppCredentialsId": null,
"riskLevel": null
}

View File

@ -1,28 +1,20 @@
{
"id": "clauujawp00011avs2vj97zma",
"createdAt": "2022-11-24T09:02:23.737Z",
"updatedAt": "2022-11-24T09:12:57.036Z",
"icon": "🤖",
"version": "6",
"id": "clyoegjca0005grw9ek6h984v",
"name": "Complete bot",
"folderId": null,
"groups": [
"events": [
{
"id": "clauujawn0000vs1a8z6k2k7d",
"title": "Start",
"blocks": [
{
"id": "clauujawn0001vs1a0mk8docp",
"type": "start",
"label": "Start",
"groupId": "clauujawn0000vs1a8z6k2k7d",
"outgoingEdgeId": "clauuk4o300083b6q7b2iowv3"
}
],
"graphCoordinates": { "x": 0, "y": 0 }
},
"outgoingEdgeId": "clauuk4o300083b6q7b2iowv3",
"graphCoordinates": { "x": 0, "y": 0 },
"type": "start"
}
],
"groups": [
{
"id": "clauujxdc00063b6q42ca20gj",
"title": "Welcome",
"graphCoordinates": { "x": 5.81640625, "y": 172.359375 },
"blocks": [
{
"id": "clauujxdd00073b6qpejnkzcy",
@ -31,8 +23,7 @@
"richText": [
{ "type": "p", "children": [{ "text": "Hi there! 👋" }] }
]
},
"groupId": "clauujxdc00063b6q42ca20gj"
}
},
{
"id": "clauukaad00093b6q07av51yc",
@ -44,29 +35,27 @@
"children": [{ "text": "Welcome. What's your name?" }]
}
]
},
"groupId": "clauujxdc00063b6q42ca20gj"
}
},
{
"id": "clauukip8000a3b6qtzl288tu",
"outgoingEdgeId": "clauul0sk000f3b6q2tvy5wfi",
"type": "text input",
"groupId": "clauujxdc00063b6q42ca20gj",
"options": {
"isLong": false,
"labels": {
"button": "Send",
"placeholder": "Type your answer..."
"placeholder": "Type your answer...",
"button": "Send"
},
"variableId": "vclauuklnc000b3b6q7xchq4yf"
},
"outgoingEdgeId": "clauul0sk000f3b6q2tvy5wfi"
"variableId": "vclauuklnc000b3b6q7xchq4yf",
"isLong": false
}
}
],
"graphCoordinates": { "x": 5.81640625, "y": 172.359375 }
]
},
{
"id": "clauukoka000c3b6qe6chawis",
"title": "Age",
"graphCoordinates": { "x": 361.17578125, "y": 170.10546875 },
"blocks": [
{
"id": "clauukoka000d3b6qxqi38cmk",
@ -78,16 +67,14 @@
"children": [{ "text": "Nice to meet you {{Name}}" }]
}
]
},
"groupId": "clauukoka000c3b6qe6chawis"
}
},
{
"id": "clauuku5o000e3b6q90rm30p1",
"type": "image",
"content": {
"url": "https://media2.giphy.com/media/l0MYGb1LuZ3n7dRnO/giphy-downsized.gif?cid=fe3852a3yd2leg4yi8iual3wgyw893zzocuuqlp3wytt802h&rid=giphy-downsized.gif&ct=g"
},
"groupId": "clauukoka000c3b6qe6chawis"
}
},
{
"id": "clauul4vg000g3b6qr0q2h0uy",
@ -96,60 +83,56 @@
"richText": [
{ "type": "p", "children": [{ "text": "How old are you?" }] }
]
},
"groupId": "clauukoka000c3b6qe6chawis"
}
},
{
"id": "clauul90j000h3b6qjfrw9js4",
"outgoingEdgeId": "clauum41j000n3b6qpqu12icm",
"type": "number input",
"groupId": "clauukoka000c3b6qe6chawis",
"options": {
"labels": { "button": "Send", "placeholder": "Type a number..." },
"variableId": "vclauulfjk000i3b6qmujooweu"
},
"outgoingEdgeId": "clauum41j000n3b6qpqu12icm"
"variableId": "vclauulfjk000i3b6qmujooweu",
"labels": { "placeholder": "Type a number...", "button": "Send" }
}
}
],
"graphCoordinates": { "x": 361.17578125, "y": 170.10546875 }
]
},
{
"id": "clauulhqf000j3b6qm8y5oifc",
"title": "Is major?",
"graphCoordinates": { "x": 726.2265625, "y": 240.80078125 },
"blocks": [
{
"id": "clauulhqf000k3b6qsrc1hd74",
"outgoingEdgeId": "clauumm5v000t3b6qu62qcft8",
"type": "Condition",
"items": [
{
"id": "clauulhqg000l3b6qaxn4qli5",
"type": 1,
"blockId": "clauulhqf000k3b6qsrc1hd74",
"outgoingEdgeId": "clauumi0x000q3b6q9bwkqnmr",
"content": {
"logicalOperator": "AND",
"comparisons": [
{
"id": "clauuliyn000m3b6q10gwx8ii",
"value": "21",
"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",
"title": "Group #4",
"graphCoordinates": { "x": 1073.38671875, "y": 232.25 },
"blocks": [
{
"id": "clauum8x7000p3b6qxjud5hdc",
"outgoingEdgeId": "clauuom2y000y3b6qkcjy2ri7",
"type": "text",
"content": {
"richText": [
@ -158,39 +141,35 @@
"children": [{ "text": "Ok, you are an adult then 😁" }]
}
]
},
"groupId": "clauum8x7000o3b6qx8hqduf8",
"outgoingEdgeId": "clauuom2y000y3b6qkcjy2ri7"
}
}
],
"graphCoordinates": { "x": 1073.38671875, "y": 232.25 }
]
},
{
"id": "clauumjq4000r3b6q8l6bi9ra",
"title": "Group #4 copy",
"graphCoordinates": { "x": 1073.984375, "y": 408.6875 },
"blocks": [
{
"id": "clauumjq5000s3b6qqjhrklv4",
"outgoingEdgeId": "clauuol8t000x3b6qcw1few70",
"type": "text",
"content": {
"richText": [
{ "type": "p", "children": [{ "text": "Oh, you are a kid 😁" }] }
]
},
"groupId": "clauumjq4000r3b6q8l6bi9ra",
"outgoingEdgeId": "clauuol8t000x3b6qcw1few70"
}
}
],
"graphCoordinates": { "x": 1073.984375, "y": 408.6875 }
]
},
{
"id": "clauuoekh000u3b6q6zmlx7f9",
"title": "Magic number",
"graphCoordinates": { "x": 1465.359375, "y": 299.25390625 },
"blocks": [
{
"id": "clauuoeki000v3b6qvsh7kde1",
"type": "Set variable",
"groupId": "clauuoekh000u3b6q6zmlx7f9",
"options": {
"variableId": "vclauuohyp000w3b6qbqrs6c6w",
"expressionToEvaluate": "42"
@ -198,6 +177,7 @@
},
{
"id": "clauuontu000z3b6q3ydx6ao1",
"outgoingEdgeId": "clauuq8je001e3b6qksm4j11g",
"type": "text",
"content": {
"richText": [
@ -206,38 +186,33 @@
"children": [{ "text": "My magic number is {{Magic number}}" }]
}
]
},
"groupId": "clauuoekh000u3b6q6zmlx7f9",
"outgoingEdgeId": "clauuq8je001e3b6qksm4j11g"
}
}
],
"graphCoordinates": { "x": 1465.359375, "y": 299.25390625 }
]
},
{
"id": "clauuq2l6001c3b6qpmq3ivwk",
"graphCoordinates": { "x": 1836.43359375, "y": 295.39453125 },
"title": "Rate the experience",
"graphCoordinates": { "x": 1836.43359375, "y": 295.39453125 },
"blocks": [
{
"id": "clauuq2l6001d3b6qyltfcvgb",
"groupId": "clauuq2l6001c3b6qpmq3ivwk",
"outgoingEdgeId": "clauureo3001h3b6qk6epabxq",
"type": "Typebot link",
"options": {
"typebotId": "chat-sub-bot",
"groupId": "clauupd6q00183b6qcm8qbz62"
},
"outgoingEdgeId": "clauureo3001h3b6qk6epabxq"
}
}
]
},
{
"id": "clauur7od001f3b6qq140oe55",
"graphCoordinates": { "x": 2201.45703125, "y": 299.1328125 },
"title": "Multiple input in group",
"graphCoordinates": { "x": 2201.45703125, "y": 299.1328125 },
"blocks": [
{
"id": "clauur7od001g3b6qkoeij3f7",
"groupId": "clauur7od001f3b6qq140oe55",
"type": "text",
"content": {
"richText": [
@ -252,53 +227,39 @@
},
{
"id": "clauurluf001i3b6qjf78puug",
"groupId": "clauur7od001f3b6qq140oe55",
"type": "email input",
"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?"
}
},
{
"id": "clauurokp001j3b6qyrw7boca",
"groupId": "clauur7od001f3b6qq140oe55",
"type": "url input",
"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?"
}
},
{
"id": "clauurs1o001k3b6qgrj0xf59",
"groupId": "clauur7od001f3b6qq140oe55",
"outgoingEdgeId": "clauushy3001p3b6qqnyrxgtb",
"type": "choice input",
"options": { "buttonLabel": "Send", "isMultipleChoice": false },
"items": [
{
"id": "clauurs1o001l3b6qu9hr712h",
"blockId": "clauurs1o001k3b6qgrj0xf59",
"type": 0,
"content": "Yes"
},
{
"id": "clauuru6t001m3b6qp8vkt23l",
"content": "No",
"blockId": "clauurs1o001k3b6qgrj0xf59",
"type": 0
}
{ "id": "clauurs1o001l3b6qu9hr712h", "content": "Yes" },
{ "id": "clauuru6t001m3b6qp8vkt23l", "content": "No" }
],
"outgoingEdgeId": "clauushy3001p3b6qqnyrxgtb"
"options": { "isMultipleChoice": false, "buttonLabel": "Send" }
}
]
},
{
"id": "clauusa9z001n3b6qys3xvz1l",
"graphCoordinates": { "x": 2558.609375, "y": 297.078125 },
"title": "Get Chuck Norris joke",
"graphCoordinates": { "x": 2558.609375, "y": 297.078125 },
"blocks": [
{
"id": "clauusaa0001o3b6qgddldaen",
"groupId": "clauusa9z001n3b6qys3xvz1l",
"type": "text",
"content": {
"richText": [
@ -308,7 +269,6 @@
},
{
"id": "clauusrfh001q3b6q7xaapi4h",
"groupId": "clauusa9z001n3b6qys3xvz1l",
"type": "text",
"content": {
"richText": [
@ -321,33 +281,31 @@
},
{
"id": "clauut2nq001r3b6qi437ixc7",
"groupId": "clauusa9z001n3b6qys3xvz1l",
"outgoingEdgeId": "clauuwjq2001x3b6qciu53855",
"type": "Webhook",
"options": {
"variablesForTest": [],
"responseVariableMapping": [
{
"id": "clauuvvdr001t3b6qqdxzc057",
"bodyPath": "data.value",
"variableId": "vclauuwchv001u3b6qepx6e0a9"
"variableId": "vclauuwchv001u3b6qepx6e0a9",
"bodyPath": "data.value"
}
],
"variablesForTest": [],
"isAdvancedConfig": true,
"isCustomBody": false
},
"webhookId": "chat-webhook-id",
"outgoingEdgeId": "clauuwjq2001x3b6qciu53855"
"isCustomBody": false,
"webhook": { "method": "POST" }
}
}
]
},
{
"id": "clauuwhyl001v3b6qarbpiqbv",
"graphCoordinates": { "x": 2900.9609375, "y": 288.29296875 },
"title": "Display joke",
"graphCoordinates": { "x": 2900.9609375, "y": 288.29296875 },
"blocks": [
{
"id": "clauuwhyl001w3b6q7ai0zeyt",
"groupId": "clauuwhyl001v3b6qarbpiqbv",
"type": "text",
"content": {
"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": [
{
"id": "clauuk4o300083b6q7b2iowv3",
"to": { "groupId": "clauujxdc00063b6q42ca20gj" },
"from": {
"blockId": "clauujawn0001vs1a0mk8docp",
"groupId": "clauujawn0000vs1a8z6k2k7d"
}
"from": { "eventId": "clauujawn0000vs1a8z6k2k7d" },
"to": { "groupId": "clauujxdc00063b6q42ca20gj" }
},
{
"id": "clauul0sk000f3b6q2tvy5wfi",
"to": { "groupId": "clauukoka000c3b6qe6chawis" },
"from": {
"blockId": "clauukip8000a3b6qtzl288tu",
"groupId": "clauujxdc00063b6q42ca20gj"
}
"from": { "blockId": "clauukip8000a3b6qtzl288tu" },
"to": { "groupId": "clauukoka000c3b6qe6chawis" }
},
{
"id": "clauum41j000n3b6qpqu12icm",
"to": { "groupId": "clauulhqf000j3b6qm8y5oifc" },
"from": {
"blockId": "clauul90j000h3b6qjfrw9js4",
"groupId": "clauukoka000c3b6qe6chawis"
}
"from": { "blockId": "clauul90j000h3b6qjfrw9js4" },
"to": { "groupId": "clauulhqf000j3b6qm8y5oifc" }
},
{
"id": "clauumi0x000q3b6q9bwkqnmr",
"to": { "groupId": "clauum8x7000o3b6qx8hqduf8" },
"from": {
"itemId": "clauulhqg000l3b6qaxn4qli5",
"blockId": "clauulhqf000k3b6qsrc1hd74",
"groupId": "clauulhqf000j3b6qm8y5oifc"
}
"itemId": "clauulhqg000l3b6qaxn4qli5"
},
"to": { "groupId": "clauum8x7000o3b6qx8hqduf8" }
},
{
"id": "clauumm5v000t3b6qu62qcft8",
"to": { "groupId": "clauumjq4000r3b6q8l6bi9ra" },
"from": {
"blockId": "clauulhqf000k3b6qsrc1hd74",
"groupId": "clauulhqf000j3b6qm8y5oifc"
}
"from": { "blockId": "clauulhqf000k3b6qsrc1hd74" },
"to": { "groupId": "clauumjq4000r3b6q8l6bi9ra" }
},
{
"id": "clauuol8t000x3b6qcw1few70",
"to": { "groupId": "clauuoekh000u3b6q6zmlx7f9" },
"from": {
"blockId": "clauumjq5000s3b6qqjhrklv4",
"groupId": "clauumjq4000r3b6q8l6bi9ra"
}
"from": { "blockId": "clauumjq5000s3b6qqjhrklv4" },
"to": { "groupId": "clauuoekh000u3b6q6zmlx7f9" }
},
{
"id": "clauuom2y000y3b6qkcjy2ri7",
"to": { "groupId": "clauuoekh000u3b6q6zmlx7f9" },
"from": {
"blockId": "clauum8x7000p3b6qxjud5hdc",
"groupId": "clauum8x7000o3b6qx8hqduf8"
}
"from": { "blockId": "clauum8x7000p3b6qxjud5hdc" },
"to": { "groupId": "clauuoekh000u3b6q6zmlx7f9" }
},
{
"from": {
"groupId": "clauuoekh000u3b6q6zmlx7f9",
"blockId": "clauuontu000z3b6q3ydx6ao1"
},
"to": { "groupId": "clauuq2l6001c3b6qpmq3ivwk" },
"id": "clauuq8je001e3b6qksm4j11g"
"id": "clauuq8je001e3b6qksm4j11g",
"from": { "blockId": "clauuontu000z3b6q3ydx6ao1" },
"to": { "groupId": "clauuq2l6001c3b6qpmq3ivwk" }
},
{
"from": {
"groupId": "clauuq2l6001c3b6qpmq3ivwk",
"blockId": "clauuq2l6001d3b6qyltfcvgb"
},
"to": { "groupId": "clauur7od001f3b6qq140oe55" },
"id": "clauureo3001h3b6qk6epabxq"
"id": "clauureo3001h3b6qk6epabxq",
"from": { "blockId": "clauuq2l6001d3b6qyltfcvgb" },
"to": { "groupId": "clauur7od001f3b6qq140oe55" }
},
{
"from": {
"groupId": "clauur7od001f3b6qq140oe55",
"blockId": "clauurs1o001k3b6qgrj0xf59"
},
"to": { "groupId": "clauusa9z001n3b6qys3xvz1l" },
"id": "clauushy3001p3b6qqnyrxgtb"
"id": "clauushy3001p3b6qqnyrxgtb",
"from": { "blockId": "clauurs1o001k3b6qgrj0xf59" },
"to": { "groupId": "clauusa9z001n3b6qys3xvz1l" }
},
{
"from": {
"groupId": "clauusa9z001n3b6qys3xvz1l",
"blockId": "clauut2nq001r3b6qi437ixc7"
},
"to": { "groupId": "clauuwhyl001v3b6qarbpiqbv" },
"id": "clauuwjq2001x3b6qciu53855"
"id": "clauuwjq2001x3b6qciu53855",
"from": { "blockId": "clauut2nq001r3b6qi437ixc7" },
"to": { "groupId": "clauuwhyl001v3b6qarbpiqbv" }
}
],
"theme": {
"chat": {
"inputs": {
"color": "#303235",
"backgroundColor": "#FFFFFF",
"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" }
"variables": [
{
"id": "vclauuklnc000b3b6q7xchq4yf",
"name": "Name",
"isSessionVariable": true
},
"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": {
"general": {
"isBrandingEnabled": false,
"isInputPrefillEnabled": true,
"isResultSavingEnabled": true,
"isHideQueryParamsEnabled": true,
"isNewResultOnRefreshEnabled": false
},
"typingEmulation": { "enabled": true, "speed": 300, "maxDelay": 1.5 },
"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."
},
"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,
"customDomain": null,
"workspaceId": "proWorkspace",
"resultsTablePreferences": null,
"isArchived": false,
"isClosed": false
"isClosed": false,
"whatsAppCredentialsId": null,
"riskLevel": null
}

View File

@ -1,22 +1,16 @@
{
"version": "5",
"id": "clnbugp6a00011ackz0k3zfkp",
"version": "6",
"id": "clyoehs240009grw9vcxfw1ku",
"name": "My typebot",
"groups": [
"events": [
{
"id": "k2nokn9v0zyhae0wqcxsbqa7",
"title": "Start",
"outgoingEdgeId": "fj2ga89lctnuwcdsshwtxmhp",
"graphCoordinates": { "x": 0, "y": 0 },
"blocks": [
{
"id": "sx4xmdbosubnxkhcg6x521p1",
"groupId": "k2nokn9v0zyhae0wqcxsbqa7",
"outgoingEdgeId": "fj2ga89lctnuwcdsshwtxmhp",
"type": "start",
"label": "Start"
}
]
},
"type": "start"
}
],
"groups": [
{
"id": "g8kdars2ahr3cyz2qf1f7w4i",
"title": "Group #1",
@ -24,7 +18,6 @@
"blocks": [
{
"id": "prh6snup7cbmoxtf5vox8kjw",
"groupId": "g8kdars2ahr3cyz2qf1f7w4i",
"type": "text input",
"options": {
"labels": {
@ -36,7 +29,6 @@
},
{
"id": "dpyyb38amnwwl4q461el2uf6",
"groupId": "g8kdars2ahr3cyz2qf1f7w4i",
"type": "text",
"content": {
"richText": [
@ -50,10 +42,7 @@
"edges": [
{
"id": "fj2ga89lctnuwcdsshwtxmhp",
"from": {
"groupId": "k2nokn9v0zyhae0wqcxsbqa7",
"blockId": "sx4xmdbosubnxkhcg6x521p1"
},
"from": { "eventId": "k2nokn9v0zyhae0wqcxsbqa7" },
"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."
}
},
"createdAt": "2023-10-04T14:28:55.282Z",
"updatedAt": "2023-10-04T14:29:11.949Z",
"createdAt": "2024-07-16T12:39:37.804Z",
"updatedAt": "2024-07-16T12:39:37.804Z",
"icon": null,
"folderId": null,
"publicId": null,
@ -98,5 +87,6 @@
"resultsTablePreferences": null,
"isArchived": false,
"isClosed": false,
"whatsAppCredentialsId": null
"whatsAppCredentialsId": null,
"riskLevel": null
}

View File

@ -1,85 +1,61 @@
{
"id": "cl9ip9u0l00001ad79a2lzm55",
"createdAt": "2022-10-21T16:22:07.414Z",
"updatedAt": "2022-10-21T16:30:57.642Z",
"icon": null,
"version": "6",
"id": "clyoep429000dgrw904vfzaez",
"name": "My typebot",
"folderId": null,
"version": "4",
"groups": [
"events": [
{
"id": "cl9ip9u0j0000d71a5d98gwni",
"title": "Start",
"blocks": [
{
"id": "cl9ip9u0j0001d71a44dsd2p1",
"type": "start",
"label": "Start",
"groupId": "cl9ip9u0j0000d71a5d98gwni",
"outgoingEdgeId": "cl9ipkkb2001b3b6oh3vptq9k"
}
],
"graphCoordinates": { "x": 0, "y": 0 }
},
"outgoingEdgeId": "cl9ipkkb2001b3b6oh3vptq9k",
"graphCoordinates": { "x": 0, "y": 0 },
"type": "start"
}
],
"groups": [
{
"id": "cl9ipa38j00083b6o69e90m4t",
"graphCoordinates": { "x": 340, "y": 341 },
"title": "Group #1",
"graphCoordinates": { "x": 340, "y": 341 },
"blocks": [
{
"id": "cl9ipaaut000a3b6ovrqlec3x",
"groupId": "cl9ipa38j00083b6o69e90m4t",
"type": "text input",
"options": {
"isLong": false,
"labels": { "button": "Send", "placeholder": "Type a name..." },
"variableId": "vcl9ipajth000c3b6okl97r81j"
"labels": { "placeholder": "Type a name...", "button": "Send" },
"variableId": "vcl9ipajth000c3b6okl97r81j",
"isLong": false
}
},
{
"id": "cl9ipan8f000d3b6oo2ovi3ac",
"groupId": "cl9ipa38j00083b6o69e90m4t",
"type": "number input",
"options": {
"labels": { "button": "Send", "placeholder": "Type an age..." },
"variableId": "vcl9ipaszl000e3b6ousjxuw7b"
"variableId": "vcl9ipaszl000e3b6ousjxuw7b",
"labels": { "placeholder": "Type an age...", "button": "Send" }
}
},
{
"id": "cl9ipb08n000f3b6ok3mi2p48",
"groupId": "cl9ipa38j00083b6o69e90m4t",
"outgoingEdgeId": "cl9ipcp83000o3b6odsn0a9a1",
"type": "choice input",
"options": {
"buttonLabel": "Send",
"isMultipleChoice": false,
"variableId": "vcl9ipg4tb00103b6oue08w3nm"
},
"items": [
{
"id": "cl9ipb08n000g3b6okr691uad",
"blockId": "cl9ipb08n000f3b6ok3mi2p48",
"type": 0,
"content": "Male"
},
{
"blockId": "cl9ipb08n000f3b6ok3mi2p48",
"type": 0,
"id": "cl9ipb2kk000h3b6oadwtonnz",
"content": "Female"
}
{ "id": "cl9ipb08n000g3b6okr691uad", "content": "Male" },
{ "id": "cl9ipb2kk000h3b6oadwtonnz", "content": "Female" }
],
"outgoingEdgeId": "cl9ipcp83000o3b6odsn0a9a1"
"options": {
"variableId": "vcl9ipg4tb00103b6oue08w3nm",
"isMultipleChoice": false,
"buttonLabel": "Send"
}
}
]
},
{
"id": "cl9ipbcjy000j3b6oqngo7luv",
"graphCoordinates": { "x": 781, "y": 91 },
"title": "Group #2",
"graphCoordinates": { "x": 781, "y": 91 },
"blocks": [
{
"id": "cl9ipbl6l000m3b6o3evn41kv",
"groupId": "cl9ipbcjy000j3b6oqngo7luv",
"type": "Set variable",
"options": {
"variableId": "vcl9ipbokm000n3b6o06hvarrf",
@ -88,9 +64,9 @@
},
{
"id": "cl9ipbcjy000k3b6oe8lta5c1",
"groupId": "cl9ipbcjy000j3b6oqngo7luv",
"type": "Webhook",
"options": {
"variablesForTest": [],
"responseVariableMapping": [
{
"id": "cl9ipdspg000p3b6ognbfvmdx",
@ -98,15 +74,17 @@
"bodyPath": "data"
}
],
"variablesForTest": [],
"isAdvancedConfig": true,
"isCustomBody": true
},
"webhookId": "full-body-webhook"
"isCustomBody": true,
"webhook": {
"url": "http://localhost:3000/api/mock/webhook-easy-config",
"body": "{\n \"name\": \"{{Name}}\",\n \"age\": {{Age}},\n \"gender\": \"{{Gender}}\"\n }"
}
}
},
{
"id": "cl9ipe5t8000s3b6ocswre500",
"groupId": "cl9ipbcjy000j3b6oqngo7luv",
"outgoingEdgeId": "cl9ipet83000z3b6of6zfqota",
"type": "text",
"content": {
"richText": [
@ -117,21 +95,20 @@
{ "type": "p", "children": [{ "text": "" }] },
{ "type": "p", "children": [{ "text": "{{Data}}" }] }
]
},
"outgoingEdgeId": "cl9ipet83000z3b6of6zfqota"
}
}
]
},
{
"id": "cl9ipej6b000u3b6oeaz305l6",
"graphCoordinates": { "x": 1138, "y": 85 },
"title": "Group #2 copy",
"graphCoordinates": { "x": 1138, "y": 85 },
"blocks": [
{
"id": "cl9ipej6c000w3b6otzk247vl",
"groupId": "cl9ipej6b000u3b6oeaz305l6",
"type": "Webhook",
"options": {
"variablesForTest": [],
"responseVariableMapping": [
{
"id": "cl9ipdspg000p3b6ognbfvmdx",
@ -139,15 +116,16 @@
"bodyPath": "data"
}
],
"variablesForTest": [],
"isAdvancedConfig": true,
"isCustomBody": true
},
"webhookId": "partial-body-webhook"
"isCustomBody": true,
"webhook": {
"url": "http://localhost:3000/api/mock/webhook-easy-config",
"body": "{{Full body}}"
}
}
},
{
"id": "cl9ipej6c000y3b6oegzkgloq",
"groupId": "cl9ipej6b000u3b6oeaz305l6",
"type": "text",
"content": {
"richText": [
@ -164,97 +142,94 @@
},
{
"id": "cl9ipkaer00153b6ov230yuv2",
"graphCoordinates": { "x": 333, "y": 26 },
"title": "Group #4",
"graphCoordinates": { "x": 333, "y": 26 },
"blocks": [
{
"id": "cl9ipkaer00163b6o0ohmmscn",
"groupId": "cl9ipkaer00153b6ov230yuv2",
"type": "choice input",
"options": { "buttonLabel": "Send", "isMultipleChoice": false },
"items": [
{
"id": "cl9ipkaer00173b6oxof4zrqn",
"blockId": "cl9ipkaer00163b6o0ohmmscn",
"type": 0,
"content": "Send failing webhook"
}
]
],
"options": { "isMultipleChoice": false, "buttonLabel": "Send" }
},
{
"id": "cl9ipki9u00193b6okmhudo0f",
"groupId": "cl9ipkaer00153b6ov230yuv2",
"outgoingEdgeId": "cl9ipklm0001c3b6oy0a5nbhr",
"type": "Webhook",
"options": {
"responseVariableMapping": [],
"variablesForTest": [],
"responseVariableMapping": [],
"isAdvancedConfig": false,
"isCustomBody": false
},
"webhookId": "failing-webhook",
"outgoingEdgeId": "cl9ipklm0001c3b6oy0a5nbhr"
"isCustomBody": false,
"webhook": { "url": "http://localhost:3001/api/mock/fail" }
}
}
]
}
],
"variables": [
{ "id": "vcl9ipajth000c3b6okl97r81j", "name": "Name" },
{ "id": "vcl9ipaszl000e3b6ousjxuw7b", "name": "Age" },
{ "id": "vcl9ipbokm000n3b6o06hvarrf", "name": "Full body" },
{ "id": "vcl9ipdxnj000q3b6oy55th4xb", "name": "Data" },
{ "id": "vcl9ipg4tb00103b6oue08w3nm", "name": "Gender" }
],
"edges": [
{
"from": {
"groupId": "cl9ipa38j00083b6o69e90m4t",
"blockId": "cl9ipb08n000f3b6ok3mi2p48"
},
"to": { "groupId": "cl9ipbcjy000j3b6oqngo7luv" },
"id": "cl9ipcp83000o3b6odsn0a9a1"
"id": "cl9ipkkb2001b3b6oh3vptq9k",
"from": { "eventId": "cl9ip9u0j0000d71a5d98gwni" },
"to": { "groupId": "cl9ipkaer00153b6ov230yuv2" }
},
{
"from": {
"groupId": "cl9ipbcjy000j3b6oqngo7luv",
"blockId": "cl9ipe5t8000s3b6ocswre500"
},
"to": { "groupId": "cl9ipej6b000u3b6oeaz305l6" },
"id": "cl9ipet83000z3b6of6zfqota"
"id": "cl9ipcp83000o3b6odsn0a9a1",
"from": { "blockId": "cl9ipb08n000f3b6ok3mi2p48" },
"to": { "groupId": "cl9ipbcjy000j3b6oqngo7luv" }
},
{
"from": {
"groupId": "cl9ip9u0j0000d71a5d98gwni",
"blockId": "cl9ip9u0j0001d71a44dsd2p1"
},
"to": { "groupId": "cl9ipkaer00153b6ov230yuv2" },
"id": "cl9ipkkb2001b3b6oh3vptq9k"
"id": "cl9ipet83000z3b6of6zfqota",
"from": { "blockId": "cl9ipe5t8000s3b6ocswre500" },
"to": { "groupId": "cl9ipej6b000u3b6oeaz305l6" }
},
{
"from": {
"groupId": "cl9ipkaer00153b6ov230yuv2",
"blockId": "cl9ipki9u00193b6okmhudo0f"
},
"to": { "groupId": "cl9ipa38j00083b6o69e90m4t" },
"id": "cl9ipklm0001c3b6oy0a5nbhr"
"id": "cl9ipklm0001c3b6oy0a5nbhr",
"from": { "blockId": "cl9ipki9u00193b6okmhudo0f" },
"to": { "groupId": "cl9ipa38j00083b6o69e90m4t" }
}
],
"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": {
"general": { "font": "Open Sans", "background": { "type": "None" } },
"chat": {
"inputs": {
"color": "#303235",
"backgroundColor": "#FFFFFF",
"placeholderColor": "#9095A0"
},
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
"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" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
},
"general": { "font": "Open Sans", "background": { "type": "None" } }
"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": {
"general": {
"isBrandingEnabled": false,
@ -262,14 +237,21 @@
"isHideQueryParamsEnabled": true,
"isNewResultOnRefreshEnabled": false
},
"typingEmulation": { "enabled": true, "speed": 300, "maxDelay": 1.5 },
"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."
},
"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,
"customDomain": null,
"workspaceId": "proWorkspace",
"resultsTablePreferences": null,
"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 { createId } from '@paralleldrive/cuid2'
import prisma from '@typebot.io/lib/prisma'
import {
createWebhook,
deleteTypebots,
deleteWebhooks,
importTypebotInDatabase,
} from '@typebot.io/playwright/databaseActions'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants'
import { importTypebotInDatabase } from '@typebot.io/playwright/databaseActions'
import { StartChatInput, StartPreviewChatInput } from '@typebot.io/schemas'
test.afterEach(async () => {
await deleteWebhooks(['chat-webhook-id'])
await deleteTypebots(['chat-sub-bot', 'starting-with-input'])
test.describe.configure({ mode: 'parallel' })
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 }) => {
@ -23,22 +35,6 @@ test('API chat execution should work on preview bot', async ({ request }) => {
id: typebotId,
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
@ -104,22 +100,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
id: typebotId,
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
await test.step('Start the chat', async () => {

View File

@ -1,50 +1,14 @@
import test, { expect } from '@playwright/test'
import { createId } from '@paralleldrive/cuid2'
import {
createWebhook,
importTypebotInDatabase,
} from '@typebot.io/playwright/databaseActions'
import { importTypebotInDatabase } from '@typebot.io/playwright/databaseActions'
import { getTestAsset } from '@/test/utils/playwright'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants'
const typebotId = createId()
test.beforeEach(async () => {
test('should execute webhooks properly', async ({ page }) => {
const typebotId = createId()
await importTypebotInDatabase(getTestAsset('typebots/webhook.json'), {
id: typebotId,
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.locator('text=Send failing webhook').click()
await page.locator('[placeholder="Type a name..."]').fill('John')

View File

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

View File

@ -1,6 +1,10 @@
// Do not edit this file manually
import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge'
import { anthropicBlock } from '.'
import { auth } from './auth'
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
import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge'
import { parseBlockSchema } from '@typebot.io/forge'
import { calComBlock } from '.'
export const calComBlockSchema = parseBlockSchema(calComBlock)
export const calComCredentialsSchema = parseBlockCredentials(calComBlock)

View File

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

View File

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

View File

@ -1,6 +1,10 @@
// Do not edit this file manually
import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge'
import { openAIBlock } from '.'
import { auth } from './auth'
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
import { parseBlockCredentials, parseBlockSchema } from '@typebot.io/forge'
import { parseBlockSchema } from '@typebot.io/forge'
import { qrCodeBlock } from '.'
export const qrCodeBlockSchema = parseBlockSchema(qrCodeBlock)
export const qrCodeCredentialsSchema = parseBlockCredentials(qrCodeBlock)

View File

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

View File

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

View File

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

View File

@ -82,22 +82,18 @@ export const parseBlockSchema = <
})
}
export const parseBlockCredentials = <
I extends string,
A extends AuthDefinition,
O extends z.ZodObject<any>
>(
blockDefinition: BlockDefinition<I, A, O>
export const parseBlockCredentials = <I extends string>(
blockId: I,
authSchema: z.ZodObject<any>
) => {
if (!blockDefinition.auth) return null
return z.object({
id: z.string(),
type: z.literal(blockDefinition.id),
type: z.literal(blockId),
createdAt: z.date(),
workspaceId: z.string(),
name: 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 { 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 { chatNodeCredentialsSchema } from '@typebot.io/chat-node-block/schemas'
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 { openAIBlock } from '@typebot.io/openai-block'
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 { togetherAiCredentialsSchema } from '@typebot.io/together-ai-block/schemas'
import { zemanticAiBlock } from '@typebot.io/zemantic-ai-block'
@ -26,9 +22,7 @@ import { nocodbCredentialsSchema } from '@typebot.io/nocodb-block/schemas'
export const forgedCredentialsSchemas = {
[openAIBlock.id]: openAICredentialsSchema,
[zemanticAiBlock.id]: zemanticAiCredentialsSchema,
[calComBlock.id]: calComCredentialsSchema,
[chatNodeBlock.id]: chatNodeCredentialsSchema,
[qrCodeBlock.id]: qrCodeCredentialsSchema,
[difyAiBlock.id]: difyAiCredentialsSchema,
[mistralBlock.id]: mistralCredentialsSchema,
[elevenlabsBlock.id]: elevenlabsCredentialsSchema,

View File

@ -7,7 +7,7 @@
"types": "./index.ts",
"devDependencies": {
"@paralleldrive/cuid2": "2.2.1",
"@playwright/test": "1.43.1",
"@playwright/test": "1.45.2",
"@typebot.io/env": "workspace:*",
"@typebot.io/prisma": "workspace:*",
"@typebot.io/tsconfig": "workspace:*",
@ -49,4 +49,4 @@
"wildcard-match": "5.1.3",
"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>[]) => {
const typebotsWithId = partialTypebots.map((typebot) => {
const typebotId = typebot.id ?? createId()

View File

@ -7,7 +7,7 @@
"author": "Baptiste Arnaud",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@playwright/test": "1.43.1",
"@playwright/test": "1.45.2",
"@typebot.io/lib": "workspace:*",
"@typebot.io/prisma": "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 { smtpCredentialsSchema } from './blocks/integrations/sendEmail'
import { whatsAppCredentialsSchema } from './whatsapp'
import { zemanticAiCredentialsSchema } from './blocks'
import { openAICredentialsSchema } from './blocks/integrations/openai'
import { forgedCredentialsSchemas } from '@typebot.io/forge-repository/credentials'
export const credentialsSchema = z.discriminatedUnion('type', [
const credentialsSchema = z.discriminatedUnion('type', [
smtpCredentialsSchema,
googleSheetsCredentialsSchema,
stripeCredentialsSchema,
openAICredentialsSchema,
whatsAppCredentialsSchema,
zemanticAiCredentialsSchema,
...Object.values(forgedCredentialsSchemas),
])
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
version: 2.9.2
'@playwright/test':
specifier: 1.43.1
version: 1.43.1
specifier: 1.45.2
version: 1.45.2
'@typebot.io/billing':
specifier: workspace:*
version: link:../../ee/packages/billing
@ -478,8 +478,8 @@ importers:
specifier: 2.2.1
version: 2.2.1
'@playwright/test':
specifier: 1.43.1
version: 1.43.1
specifier: 1.45.2
version: 1.45.2
'@typebot.io/emails':
specifier: workspace:*
version: link:../../packages/emails
@ -1785,8 +1785,8 @@ importers:
specifier: 2.2.1
version: 2.2.1
'@playwright/test':
specifier: 1.43.1
version: 1.43.1
specifier: 1.45.2
version: 1.45.2
'@typebot.io/env':
specifier: workspace:*
version: link:../env
@ -1856,8 +1856,8 @@ importers:
packages/playwright:
dependencies:
'@playwright/test':
specifier: 1.43.1
version: 1.43.1
specifier: 1.45.2
version: 1.45.2
'@typebot.io/env':
specifier: workspace:*
version: link:../env
@ -4687,9 +4687,9 @@ packages:
resolution: {integrity: sha512-+zk04eXRiaJGaRnJZkCxXbBtBvQDQJXCoxqlXhLY3HzAovXfsBnh6DjXRujPRQQ7GKtT8/tOlyvZ9h6ReM+GLQ==}
engines: {node: '>=16'}
'@playwright/test@1.43.1':
resolution: {integrity: sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==}
engines: {node: '>=16'}
'@playwright/test@1.45.2':
resolution: {integrity: sha512-JxG9eq92ET75EbVi3s+4sYbcG7q72ECeZNbdBlaMkGcNbiDQ4cAi8U2QP5oKkOx+1gpaiL1LDStmzCaEM1Z6fQ==}
engines: {node: '>=18'}
hasBin: true
'@popperjs/core@2.11.8':
@ -10645,14 +10645,14 @@ packages:
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
engines: {node: '>=8'}
playwright-core@1.43.1:
resolution: {integrity: sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==}
engines: {node: '>=16'}
playwright-core@1.45.2:
resolution: {integrity: sha512-ha175tAWb0dTK0X4orvBIqi3jGEt701SMxMhyujxNrgd8K0Uy5wMSwwcQHtyB4om7INUkfndx02XnQ2p6dvLDw==}
engines: {node: '>=18'}
hasBin: true
playwright@1.43.1:
resolution: {integrity: sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==}
engines: {node: '>=16'}
playwright@1.45.2:
resolution: {integrity: sha512-ReywF2t/0teRvNBpfIgh5e4wnrI/8Su8ssdo5XsQKpjxJj+jspm00jSoz9BTg91TT0c9HRjXO7LBNVrgYj9X0g==}
engines: {node: '>=18'}
hasBin: true
pngjs@5.0.0:
@ -16357,9 +16357,9 @@ snapshots:
'@planetscale/database@1.8.0': {}
'@playwright/test@1.43.1':
'@playwright/test@1.45.2':
dependencies:
playwright: 1.43.1
playwright: 1.45.2
'@popperjs/core@2.11.8': {}
@ -24207,11 +24207,11 @@ snapshots:
dependencies:
find-up: 4.1.0
playwright-core@1.43.1: {}
playwright-core@1.45.2: {}
playwright@1.43.1:
playwright@1.45.2:
dependencies:
playwright-core: 1.43.1
playwright-core: 1.45.2
optionalDependencies:
fsevents: 2.3.2