✨ (credentials) Add credentials management menu in workspace settings
Closes #1567
This commit is contained in:
@ -1,3 +1,4 @@
|
||||
emojiList.json
|
||||
iconNames.ts
|
||||
reporters
|
||||
.last-run.json
|
||||
|
1
apps/builder/.eslintignore
Normal file
1
apps/builder/.eslintignore
Normal file
@ -0,0 +1 @@
|
||||
src/test/reporters
|
@ -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:*",
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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']
|
||||
|
16
apps/builder/src/components/logos/StripeLogo.tsx
Normal file
16
apps/builder/src/components/logos/StripeLogo.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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 ?? '')
|
||||
|
@ -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
|
||||
"read" or "write" access. That's why it
|
||||
states that Typebot can also delete your spreadsheets which it
|
||||
won'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
|
||||
"read" or "write" access. That's why it
|
||||
states that Typebot can also delete your spreadsheets which it
|
||||
won'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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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({
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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',
|
||||
})
|
||||
})
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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
|
||||
|
@ -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' })
|
||||
|
@ -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({
|
||||
|
66
apps/builder/src/features/credentials/api/getCredentials.ts
Normal file
66
apps/builder/src/features/credentials/api/getCredentials.ts
Normal 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,
|
||||
}
|
||||
})
|
@ -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))
|
||||
),
|
||||
}
|
||||
})
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -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 }
|
||||
}
|
||||
)
|
@ -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'
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
39
apps/builder/src/features/credentials/credentials.spec.ts
Normal file
39
apps/builder/src/features/credentials/credentials.spec.ts
Normal 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()
|
||||
})
|
@ -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>
|
||||
)}
|
||||
|
@ -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} />
|
||||
}
|
||||
}
|
||||
|
@ -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} />
|
||||
}
|
||||
}
|
||||
|
@ -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() }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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 }
|
||||
})
|
@ -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 }
|
||||
}
|
||||
)
|
@ -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 }
|
||||
})
|
@ -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,
|
||||
})
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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({
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -118,8 +118,7 @@
|
||||
"variablesForTest": [],
|
||||
"isAdvancedConfig": false,
|
||||
"isCustomBody": false
|
||||
},
|
||||
"webhookId": "webhook1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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
1
apps/viewer/.eslintignore
Normal file
1
apps/viewer/.eslintignore
Normal file
@ -0,0 +1 @@
|
||||
src/test/reporters
|
@ -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:*",
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 () => {
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
}, {})
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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 }
|
||||
)
|
||||
)
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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:*",
|
||||
|
@ -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()
|
||||
|
@ -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:*",
|
||||
|
@ -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
44
pnpm-lock.yaml
generated
@ -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
|
||||
|
||||
|
Reference in New Issue
Block a user