@ -56,7 +56,6 @@
|
||||
"@uiw/codemirror-theme-tokyo-night": "4.21.24",
|
||||
"@uiw/react-codemirror": "4.21.24",
|
||||
"@upstash/ratelimit": "0.4.3",
|
||||
"@upstash/redis": "1.22.0",
|
||||
"@use-gesture/react": "10.2.27",
|
||||
"browser-image-compression": "2.0.2",
|
||||
"canvas-confetti": "1.6.0",
|
||||
@ -69,6 +68,7 @@
|
||||
"google-auth-library": "8.9.0",
|
||||
"google-spreadsheet": "4.1.1",
|
||||
"immer": "10.0.2",
|
||||
"ioredis": "^5.4.1",
|
||||
"isolated-vm": "4.7.2",
|
||||
"jsonwebtoken": "9.0.1",
|
||||
"ky": "1.2.4",
|
||||
|
@ -8,15 +8,17 @@ import {
|
||||
useColorModeValue,
|
||||
Portal,
|
||||
} from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
import React, { RefObject } from 'react'
|
||||
import { EmojiOrImageIcon } from './EmojiOrImageIcon'
|
||||
import { ImageUploadContent } from './ImageUploadContent'
|
||||
import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'
|
||||
import { useTranslate } from '@tolgee/react'
|
||||
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
|
||||
|
||||
type Props = {
|
||||
uploadFileProps: FilePathUploadProps
|
||||
icon?: string | null
|
||||
parentModalRef?: RefObject<HTMLElement | null> | undefined
|
||||
onChangeIcon: (icon: string) => void
|
||||
boxSize?: string
|
||||
}
|
||||
@ -28,6 +30,7 @@ export const EditableEmojiOrImageIcon = ({
|
||||
boxSize,
|
||||
}: Props) => {
|
||||
const { t } = useTranslate()
|
||||
const { ref: parentModalRef } = useParentModal()
|
||||
const bg = useColorModeValue('gray.100', 'gray.700')
|
||||
|
||||
return (
|
||||
@ -56,7 +59,7 @@ export const EditableEmojiOrImageIcon = ({
|
||||
</PopoverTrigger>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
<Portal>
|
||||
<Portal containerRef={parentModalRef}>
|
||||
<PopoverContent p="2">
|
||||
<ImageUploadContent
|
||||
uploadFileProps={uploadFileProps}
|
||||
|
@ -24,7 +24,7 @@ test.describe('Date input block', () => {
|
||||
'date'
|
||||
)
|
||||
await page.locator('[data-testid="from-date"]').fill('2021-01-01')
|
||||
await page.locator('form').getByRole('button').click()
|
||||
await page.getByLabel('Send').click()
|
||||
await expect(page.locator('text="01/01/2021"')).toBeVisible()
|
||||
|
||||
await page.click(`text=Pick a date`)
|
||||
|
@ -27,7 +27,9 @@ test('options should work', async ({ page }) => {
|
||||
await page
|
||||
.locator(`input[type="file"]`)
|
||||
.setInputFiles([getTestAsset('avatar.jpg')])
|
||||
await expect(page.locator(`text=File uploaded`)).toBeVisible()
|
||||
await expect(
|
||||
page.getByRole('img', { name: 'Attached image 1' })
|
||||
).toBeVisible()
|
||||
await page.click('text="Collect file"')
|
||||
await page.click('text="Required?"')
|
||||
await page.click('text="Allow multiple files?"')
|
||||
@ -46,9 +48,11 @@ test('options should work', async ({ page }) => {
|
||||
getTestAsset('avatar.jpg'),
|
||||
getTestAsset('avatar.jpg'),
|
||||
])
|
||||
await expect(page.locator(`text="3"`)).toBeVisible()
|
||||
await expect(page.getByRole('img', { name: 'avatar.jpg' })).toHaveCount(3)
|
||||
await page.locator('text="Go"').click()
|
||||
await expect(page.locator(`text="3 files uploaded"`)).toBeVisible()
|
||||
await expect(
|
||||
page.getByRole('img', { name: 'Attached image 1' })
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('Free workspace', () => {
|
||||
|
@ -1,25 +1,48 @@
|
||||
import React from 'react'
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { Stack, Text } from '@chakra-ui/react'
|
||||
import { WithVariableContent } from '@/features/graph/components/nodes/block/WithVariableContent'
|
||||
import { TextInputBlock } from '@typebot.io/schemas'
|
||||
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { SetVariableLabel } from '@/components/SetVariableLabel'
|
||||
|
||||
type Props = {
|
||||
options: TextInputBlock['options']
|
||||
}
|
||||
|
||||
export const TextInputNodeContent = ({ options }: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
const attachmentVariableId =
|
||||
typebot &&
|
||||
options?.attachments?.isEnabled &&
|
||||
options?.attachments.saveVariableId
|
||||
if (options?.variableId)
|
||||
return (
|
||||
<WithVariableContent
|
||||
variableId={options?.variableId}
|
||||
h={options.isLong ? '100px' : 'auto'}
|
||||
/>
|
||||
<Stack w="calc(100% - 25px)">
|
||||
<WithVariableContent
|
||||
variableId={options?.variableId}
|
||||
h={options.isLong ? '100px' : 'auto'}
|
||||
/>
|
||||
{attachmentVariableId && (
|
||||
<SetVariableLabel
|
||||
variables={typebot.variables}
|
||||
variableId={attachmentVariableId}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
return (
|
||||
<Text color={'gray.500'} h={options?.isLong ? '100px' : 'auto'}>
|
||||
{options?.labels?.placeholder ??
|
||||
defaultTextInputOptions.labels.placeholder}
|
||||
</Text>
|
||||
<Stack>
|
||||
<Text color={'gray.500'} h={options?.isLong ? '100px' : 'auto'}>
|
||||
{options?.labels?.placeholder ??
|
||||
defaultTextInputOptions.labels.placeholder}
|
||||
</Text>
|
||||
{attachmentVariableId && (
|
||||
<SetVariableLabel
|
||||
variables={typebot.variables}
|
||||
variableId={attachmentVariableId}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { DropdownList } from '@/components/DropdownList'
|
||||
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
|
||||
import { TextInput } from '@/components/inputs'
|
||||
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
||||
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { useTranslate } from '@tolgee/react'
|
||||
import { TextInputBlock, Variable } from '@typebot.io/schemas'
|
||||
import { fileVisibilityOptions } from '@typebot.io/schemas/features/blocks/inputs/file/constants'
|
||||
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
|
||||
import React from 'react'
|
||||
|
||||
@ -14,21 +17,44 @@ type Props = {
|
||||
|
||||
export const TextInputSettings = ({ options, onOptionsChange }: Props) => {
|
||||
const { t } = useTranslate()
|
||||
const handlePlaceholderChange = (placeholder: string) =>
|
||||
const updatePlaceholder = (placeholder: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } })
|
||||
const handleButtonLabelChange = (button: string) =>
|
||||
|
||||
const updateButtonLabel = (button: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options?.labels, button } })
|
||||
const handleLongChange = (isLong: boolean) =>
|
||||
|
||||
const updateIsLong = (isLong: boolean) =>
|
||||
onOptionsChange({ ...options, isLong })
|
||||
const handleVariableChange = (variable?: Variable) =>
|
||||
|
||||
const updateVariableId = (variable?: Variable) =>
|
||||
onOptionsChange({ ...options, variableId: variable?.id })
|
||||
|
||||
const updateAttachmentsEnabled = (isEnabled: boolean) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
attachments: { ...options?.attachments, isEnabled },
|
||||
})
|
||||
|
||||
const updateAttachmentsSaveVariableId = (variable?: Pick<Variable, 'id'>) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
attachments: { ...options?.attachments, saveVariableId: variable?.id },
|
||||
})
|
||||
|
||||
const updateVisibility = (
|
||||
visibility: (typeof fileVisibilityOptions)[number]
|
||||
) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
attachments: { ...options?.attachments, visibility },
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<SwitchWithLabel
|
||||
label={t('blocks.inputs.text.settings.longText.label')}
|
||||
initialValue={options?.isLong ?? defaultTextInputOptions.isLong}
|
||||
onCheckChange={handleLongChange}
|
||||
onCheckChange={updateIsLong}
|
||||
/>
|
||||
<TextInput
|
||||
label={t('blocks.inputs.settings.placeholder.label')}
|
||||
@ -36,22 +62,50 @@ export const TextInputSettings = ({ options, onOptionsChange }: Props) => {
|
||||
options?.labels?.placeholder ??
|
||||
defaultTextInputOptions.labels.placeholder
|
||||
}
|
||||
onChange={handlePlaceholderChange}
|
||||
onChange={updatePlaceholder}
|
||||
/>
|
||||
<TextInput
|
||||
label={t('blocks.inputs.settings.button.label')}
|
||||
defaultValue={
|
||||
options?.labels?.button ?? defaultTextInputOptions.labels.button
|
||||
}
|
||||
onChange={handleButtonLabelChange}
|
||||
onChange={updateButtonLabel}
|
||||
/>
|
||||
<SwitchWithRelatedSettings
|
||||
label={'Allow attachments'}
|
||||
initialValue={
|
||||
options?.attachments?.isEnabled ??
|
||||
defaultTextInputOptions.attachments.isEnabled
|
||||
}
|
||||
onCheckChange={updateAttachmentsEnabled}
|
||||
>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable">
|
||||
Save the URLs in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options?.attachments?.saveVariableId}
|
||||
onSelectVariable={updateAttachmentsSaveVariableId}
|
||||
/>
|
||||
</Stack>
|
||||
<DropdownList
|
||||
label="Visibility:"
|
||||
moreInfoTooltip='This setting determines who can see the uploaded files. "Public" means that anyone who has the link can see the files. "Private" means that only a members of this workspace can see the files.'
|
||||
currentItem={
|
||||
options?.attachments?.visibility ??
|
||||
defaultTextInputOptions.attachments.visibility
|
||||
}
|
||||
onItemSelect={updateVisibility}
|
||||
items={fileVisibilityOptions}
|
||||
/>
|
||||
</SwitchWithRelatedSettings>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable">
|
||||
{t('blocks.inputs.settings.saveAnswer.label')}
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options?.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
onSelectVariable={updateVariableId}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
@ -4,6 +4,7 @@ import { parseDefaultGroupWithBlock } from '@typebot.io/playwright/databaseHelpe
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
|
||||
test.describe.parallel('Text input block', () => {
|
||||
test('options should work', async ({ page }) => {
|
||||
@ -37,4 +38,48 @@ test.describe.parallel('Text input block', () => {
|
||||
).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Go' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('hey boy', async ({ page }) => {
|
||||
const typebotId = createId()
|
||||
await createTypebots([
|
||||
{
|
||||
id: typebotId,
|
||||
...parseDefaultGroupWithBlock({
|
||||
type: InputBlockType.TEXT,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
|
||||
await page.click(`text=${defaultTextInputOptions.labels.placeholder}`)
|
||||
await page.getByText('Allow attachments').click()
|
||||
await page.locator('[data-testid="variables-input"]').first().click()
|
||||
await page.getByText('var1').click()
|
||||
await page.getByRole('button', { name: 'Test' }).click()
|
||||
await page
|
||||
.getByPlaceholder('Type your answer...')
|
||||
.fill('Help me with these')
|
||||
await page.getByLabel('Add attachments').click()
|
||||
await expect(page.getByRole('menuitem', { name: 'Document' })).toBeVisible()
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Photos & videos' })
|
||||
).toBeVisible()
|
||||
await page
|
||||
.locator('#document-upload')
|
||||
.setInputFiles(getTestAsset('typebots/theme.json'))
|
||||
await expect(page.getByText('theme.json')).toBeVisible()
|
||||
await page
|
||||
.locator('#photos-upload')
|
||||
.setInputFiles([getTestAsset('avatar.jpg'), getTestAsset('avatar.jpg')])
|
||||
await expect(page.getByRole('img', { name: 'avatar.jpg' })).toHaveCount(2)
|
||||
await page.getByRole('img', { name: 'avatar.jpg' }).first().hover()
|
||||
await page.getByLabel('Remove attachment').first().click()
|
||||
await expect(page.getByRole('img', { name: 'avatar.jpg' })).toHaveCount(1)
|
||||
await page.getByLabel('Send').click()
|
||||
await expect(
|
||||
page.getByRole('img', { name: 'Attached image 1' })
|
||||
).toBeVisible()
|
||||
await expect(page.getByText('Help me with these')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
@ -19,9 +19,9 @@ test.describe.parallel('Google sheets integration', () => {
|
||||
|
||||
await page.click('text=Add a value')
|
||||
await page.click('text=Select a column')
|
||||
await page.click('button >> text="Email"')
|
||||
await page.getByRole('menuitem', { name: 'Email' }).click()
|
||||
await page.click('[aria-label="Insert a variable"]')
|
||||
await page.click('button >> text="Email" >> nth=1')
|
||||
await page.getByRole('menuitem', { name: 'Email' }).last().click()
|
||||
|
||||
await page.click('text=Add a value')
|
||||
await page.click('text=Select a column')
|
||||
@ -61,11 +61,11 @@ test.describe.parallel('Google sheets integration', () => {
|
||||
await page.getByRole('button', { name: 'Row(s) to update' }).click()
|
||||
await page.getByRole('button', { name: 'Add filter rule' }).click()
|
||||
await page.click('text=Select a column')
|
||||
await page.click('button >> text="Email"')
|
||||
await page.getByRole('menuitem', { name: 'Email' }).click()
|
||||
await page.getByRole('button', { name: 'Select an operator' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Equal to' }).click()
|
||||
await page.click('[aria-label="Insert a variable"]')
|
||||
await page.click('button >> text="Email" >> nth=1')
|
||||
await page.getByRole('menuitem', { name: 'Email' }).last().click()
|
||||
|
||||
await page.getByRole('button', { name: 'Cells to update' }).click()
|
||||
await page.click('text=Add a value')
|
||||
@ -106,11 +106,11 @@ test.describe.parallel('Google sheets integration', () => {
|
||||
await page.getByRole('button', { name: 'Select row(s)' }).click()
|
||||
await page.getByRole('button', { name: 'Add filter rule' }).click()
|
||||
await page.click('text=Select a column')
|
||||
await page.click('button >> text="Email"')
|
||||
await page.getByRole('menuitem', { name: 'Email' }).click()
|
||||
await page.getByRole('button', { name: 'Select an operator' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Equal to' }).click()
|
||||
await page.click('[aria-label="Insert a variable"]')
|
||||
await page.click('button >> text="Email" >> nth=1')
|
||||
await page.getByRole('menuitem', { name: 'Email' }).last().click()
|
||||
|
||||
await page.getByRole('button', { name: 'Add filter rule' }).click()
|
||||
await page.getByRole('button', { name: 'AND', exact: true }).click()
|
||||
|
@ -20,7 +20,7 @@ test.describe('Condition block', () => {
|
||||
'input[placeholder="Search for a variable"] >> nth=-1',
|
||||
'Age'
|
||||
)
|
||||
await page.click('button:has-text("Age")')
|
||||
await page.getByRole('menuitem', { name: 'Age' }).click()
|
||||
await page.click('button:has-text("Select an operator")')
|
||||
await page.click('button:has-text("Greater than")', { force: true })
|
||||
await page.fill('input[placeholder="Type a number..."]', '80')
|
||||
@ -31,7 +31,7 @@ test.describe('Condition block', () => {
|
||||
':nth-match(input[placeholder="Search for a variable"], 2)',
|
||||
'Age'
|
||||
)
|
||||
await page.click('button:has-text("Age")')
|
||||
await page.getByRole('menuitem', { name: 'Age' }).click()
|
||||
await page.click('button:has-text("Select an operator")')
|
||||
await page.click('button:has-text("Less than")', { force: true })
|
||||
await page.fill(
|
||||
@ -44,7 +44,7 @@ test.describe('Condition block', () => {
|
||||
'input[placeholder="Search for a variable"] >> nth=-1',
|
||||
'Age'
|
||||
)
|
||||
await page.click('button:has-text("Age")')
|
||||
await page.getByRole('menuitem', { name: 'Age' }).click()
|
||||
await page.click('button:has-text("Select an operator")')
|
||||
await page.click('button:has-text("Greater than")', { force: true })
|
||||
await page.fill('input[placeholder="Type a number..."]', '20')
|
||||
|
@ -23,10 +23,7 @@ test.describe('Set variable block', () => {
|
||||
await page.click('text=Click to edit... >> nth = 0')
|
||||
await page.fill('input[placeholder="Select a variable"] >> nth=-1', 'Total')
|
||||
await page.getByRole('menuitem', { name: 'Create Total' }).click()
|
||||
await page
|
||||
.getByTestId('code-editor')
|
||||
.getByRole('textbox')
|
||||
.fill('1000 * {{Num}}')
|
||||
await page.locator('textarea').fill('1000 * {{Num}}')
|
||||
|
||||
await page.click('text=Click to edit...', { force: true })
|
||||
await expect(page.getByText('Save in results?')).toBeHidden()
|
||||
@ -39,10 +36,7 @@ test.describe('Set variable block', () => {
|
||||
await expect(
|
||||
page.getByRole('group').nth(1).locator('.chakra-switch')
|
||||
).not.toHaveAttribute('data-checked')
|
||||
await page
|
||||
.getByTestId('code-editor')
|
||||
.getByRole('textbox')
|
||||
.fill('Custom value')
|
||||
await page.locator('textarea').fill('Custom value')
|
||||
|
||||
await page.click('text=Click to edit...', { force: true })
|
||||
await page.fill(
|
||||
@ -50,10 +44,7 @@ test.describe('Set variable block', () => {
|
||||
'Addition'
|
||||
)
|
||||
await page.getByRole('menuitem', { name: 'Create Addition' }).click()
|
||||
await page
|
||||
.getByTestId('code-editor')
|
||||
.getByRole('textbox')
|
||||
.fill('1000 + {{Total}}')
|
||||
await page.locator('textarea').fill('1000 + {{Total}}')
|
||||
|
||||
await page.click('text=Test')
|
||||
await page
|
||||
@ -94,14 +85,14 @@ test.describe('Set variable block', () => {
|
||||
await page.getByRole('button', { name: 'Test' }).click()
|
||||
await page.getByRole('button', { name: 'There is a bug 🐛' }).click()
|
||||
await page.getByTestId('textarea').fill('Hello!!')
|
||||
await page.getByTestId('input').getByRole('button').click()
|
||||
await page.getByLabel('Send').click()
|
||||
await page
|
||||
.locator('typebot-standard')
|
||||
.getByRole('button', { name: 'Restart' })
|
||||
.click()
|
||||
await page.getByRole('button', { name: 'I have a question 💭' }).click()
|
||||
await page.getByTestId('textarea').fill('How are you?')
|
||||
await page.getByTestId('input').getByRole('button').click()
|
||||
await page.getByLabel('Send').click()
|
||||
await page.getByRole('button', { name: 'Transcription' }).click()
|
||||
|
||||
await expect(
|
||||
|
@ -46,8 +46,8 @@ test('should be configurable', async ({ page }) => {
|
||||
await page.getByLabel('Clear').click()
|
||||
|
||||
await page.click('text=Test')
|
||||
await page.locator('typebot-standard').locator('input').fill('Hello there!')
|
||||
await page.locator('typebot-standard').locator('input').press('Enter')
|
||||
await page.getByPlaceholder('Type your answer...').fill('Hello there!')
|
||||
await page.getByPlaceholder('Type your answer...').press('Enter')
|
||||
await expect(
|
||||
page.locator('typebot-standard').locator('text=Hello there!')
|
||||
).toBeVisible()
|
||||
|
@ -9,6 +9,7 @@ import { useTranslate } from '@tolgee/react'
|
||||
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'
|
||||
|
||||
export const DashboardHeader = () => {
|
||||
const { t } = useTranslate()
|
||||
@ -38,12 +39,14 @@ export const DashboardHeader = () => {
|
||||
</Link>
|
||||
<HStack>
|
||||
{user && workspace && !workspace.isPastDue && (
|
||||
<WorkspaceSettingsModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
user={user}
|
||||
workspace={workspace}
|
||||
/>
|
||||
<ParentModalProvider>
|
||||
<WorkspaceSettingsModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
user={user}
|
||||
workspace={workspace}
|
||||
/>
|
||||
</ParentModalProvider>
|
||||
)}
|
||||
{!workspace?.isPastDue && (
|
||||
<Button
|
||||
|
@ -171,15 +171,10 @@ const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({
|
||||
const newBlock = { ...block }
|
||||
const blockId = createId()
|
||||
oldToNewIdsMapping.set(newBlock.id, blockId)
|
||||
console.log(JSON.stringify(newBlock), blockHasOptions(newBlock))
|
||||
if (blockHasOptions(newBlock) && newBlock.options) {
|
||||
const variableIdsToReplace = extractVariableIdsFromObject(
|
||||
newBlock.options
|
||||
).filter((v) => oldToNewIdsMapping.has(v))
|
||||
console.log(
|
||||
JSON.stringify(newBlock.options),
|
||||
variableIdsToReplace
|
||||
)
|
||||
if (variableIdsToReplace.length > 0) {
|
||||
let optionsStr = JSON.stringify(newBlock.options)
|
||||
variableIdsToReplace.forEach((variableId) => {
|
||||
|
@ -43,7 +43,7 @@ export const parseReactBotProps = ({ typebot, apiHost }: BotProps) => {
|
||||
}
|
||||
|
||||
export const typebotImportCode = isCloudProdInstance()
|
||||
? `import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'`
|
||||
? `import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.3/dist/web.js'`
|
||||
: `import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@${packageJson.version}/dist/web.js'`
|
||||
|
||||
export const parseInlineScript = (script: string) =>
|
||||
|
@ -108,7 +108,6 @@ export const startWhatsAppPreview = authenticatedProcedure
|
||||
setVariableHistory,
|
||||
} = await startSession({
|
||||
version: 2,
|
||||
message: undefined,
|
||||
startParams: {
|
||||
isOnlyRegistering: !canSendDirectMessagesToUser,
|
||||
type: 'preview',
|
||||
|
@ -25,6 +25,7 @@ import { UserPreferencesForm } from '@/features/account/components/UserPreferenc
|
||||
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'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
@ -47,6 +48,7 @@ export const WorkspaceSettingsModal = ({
|
||||
onClose,
|
||||
}: Props) => {
|
||||
const { t } = useTranslate()
|
||||
const { ref } = useParentModal()
|
||||
const { currentRole } = useWorkspace()
|
||||
const [selectedTab, setSelectedTab] = useState<SettingsTab>('my-account')
|
||||
|
||||
@ -55,7 +57,7 @@ export const WorkspaceSettingsModal = ({
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="4xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent minH="600px" flexDir="row">
|
||||
<ModalContent minH="600px" flexDir="row" ref={ref}>
|
||||
<Stack
|
||||
spacing={8}
|
||||
w="180px"
|
||||
|
@ -16,20 +16,36 @@ import { mockedUser } from '@typebot.io/lib/mockedUser'
|
||||
import { getNewUserInvitations } from '@/features/auth/helpers/getNewUserInvitations'
|
||||
import { sendVerificationRequest } from '@/features/auth/helpers/sendVerificationRequest'
|
||||
import { Ratelimit } from '@upstash/ratelimit'
|
||||
import { Redis } from '@upstash/redis/nodejs'
|
||||
import ky from 'ky'
|
||||
import { env } from '@typebot.io/env'
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
import { getIp } from '@typebot.io/lib/getIp'
|
||||
import { trackEvents } from '@typebot.io/telemetry/trackEvents'
|
||||
import Redis from 'ioredis'
|
||||
|
||||
const providers: Provider[] = []
|
||||
|
||||
let rateLimit: Ratelimit | undefined
|
||||
let emailSignInRateLimiter: Ratelimit | undefined
|
||||
|
||||
if (env.UPSTASH_REDIS_REST_URL && env.UPSTASH_REDIS_REST_TOKEN) {
|
||||
rateLimit = new Ratelimit({
|
||||
redis: Redis.fromEnv(),
|
||||
if (env.REDIS_URL) {
|
||||
const redis = new Redis(env.REDIS_URL)
|
||||
const rateLimitCompatibleRedis = {
|
||||
sadd: <TData>(key: string, ...members: TData[]) =>
|
||||
redis.sadd(key, ...members.map((m) => String(m))),
|
||||
eval: async <TArgs extends unknown[], TData = unknown>(
|
||||
script: string,
|
||||
keys: string[],
|
||||
args: TArgs
|
||||
) =>
|
||||
redis.eval(
|
||||
script,
|
||||
keys.length,
|
||||
...keys,
|
||||
...(args ?? []).map((a) => String(a))
|
||||
) as Promise<TData>,
|
||||
}
|
||||
emailSignInRateLimiter = new Ratelimit({
|
||||
redis: rateLimitCompatibleRedis,
|
||||
limiter: Ratelimit.slidingWindow(1, '60 s'),
|
||||
})
|
||||
}
|
||||
@ -229,13 +245,13 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
let restricted: 'rate-limited' | undefined
|
||||
|
||||
if (
|
||||
rateLimit &&
|
||||
emailSignInRateLimiter &&
|
||||
req.url?.startsWith('/api/auth/signin/email') &&
|
||||
req.method === 'POST'
|
||||
) {
|
||||
const ip = getIp(req)
|
||||
if (ip) {
|
||||
const { success } = await rateLimit.limit(ip)
|
||||
const { success } = await emailSignInRateLimiter.limit(ip)
|
||||
if (!success) restricted = 'rate-limited'
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,62 @@
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
|
||||
import {
|
||||
methodNotAllowed,
|
||||
notAuthenticated,
|
||||
notFound,
|
||||
} from '@typebot.io/lib/api'
|
||||
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
|
||||
import { downloadMedia } from '@typebot.io/bot-engine/whatsapp/downloadMedia'
|
||||
import { env } from '@typebot.io/env'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'GET') {
|
||||
if (!env.META_SYSTEM_USER_TOKEN)
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Meta system user token is not set' })
|
||||
const user = await getAuthenticatedUser(req, res)
|
||||
if (!user) return notAuthenticated(res)
|
||||
|
||||
const typebotId = req.query.typebotId as string
|
||||
|
||||
const typebot = await prisma.typebot.findFirst({
|
||||
where: {
|
||||
id: typebotId,
|
||||
},
|
||||
select: {
|
||||
whatsAppCredentialsId: true,
|
||||
workspace: {
|
||||
select: {
|
||||
members: {
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!typebot?.workspace || isReadWorkspaceFobidden(typebot.workspace, user))
|
||||
return notFound(res, 'Workspace not found')
|
||||
|
||||
if (!typebot) return notFound(res, 'Typebot not found')
|
||||
|
||||
const mediaId = req.query.mediaId as string
|
||||
|
||||
const { file, mimeType } = await downloadMedia({
|
||||
mediaId,
|
||||
systemUserAccessToken: env.META_SYSTEM_USER_TOKEN,
|
||||
})
|
||||
|
||||
res.setHeader('Content-Type', mimeType)
|
||||
res.setHeader('Cache-Control', 'public, max-age=86400')
|
||||
|
||||
return res.send(file)
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default handler
|
@ -10,7 +10,7 @@ There, you can change the container dimensions. Here is a code example:
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
|
||||
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.3/dist/web.js'
|
||||
|
||||
Typebot.initStandard({
|
||||
typebot: 'my-typebot',
|
||||
@ -28,7 +28,7 @@ If you have different bots on the same page you will have to make them distinct
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
|
||||
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.3/dist/web.js'
|
||||
|
||||
Typebot.initStandard({
|
||||
id: 'bot1'
|
||||
@ -60,7 +60,7 @@ Here is an example:
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
|
||||
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.3/dist/web.js'
|
||||
|
||||
Typebot.initPopup({
|
||||
typebot: 'my-typebot',
|
||||
@ -80,7 +80,7 @@ Here is an example:
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
|
||||
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.3/dist/web.js'
|
||||
|
||||
Typebot.initBubble({
|
||||
typebot: 'my-typebot',
|
||||
|
@ -24,7 +24,7 @@ It should look like:
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
|
||||
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.3/dist/web.js'
|
||||
|
||||
Typebot.initPopup({
|
||||
typebot: 'my-typebot',
|
||||
|
@ -11157,6 +11157,12 @@
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"attachedFileUrls": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -11452,6 +11458,12 @@
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"attachedFileUrls": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -14664,6 +14676,9 @@
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"caption": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -14698,6 +14713,9 @@
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"caption": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -14766,6 +14784,9 @@
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"caption": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -15605,6 +15626,25 @@
|
||||
},
|
||||
"isLong": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"attachments": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"isEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"saveVariableId": {
|
||||
"type": "string"
|
||||
},
|
||||
"visibility": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Auto",
|
||||
"Public",
|
||||
"Private"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -174,14 +174,12 @@ The Authorization callback URL should be `$NEXTAUTH_URL/api/auth/callback/azure-
|
||||
Used for authenticating with Keycloak.
|
||||
Follow the official Keycloak guide for creating OAuth2 applications [here](https://www.keycloak.org/).
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ------------------------ | ------------------ | ------------------------------------------------------------------------------------ |
|
||||
| KEYCLOAK_CLIENT_ID | | Application client ID. |
|
||||
| KEYCLOAK_CLIENT_SECRET | | Application secret |
|
||||
| KEYCLOAK_REALM | | Your Keycloak Realm |
|
||||
| KEYCLOAK_BASE_URL | | Base URL of the Keycloak instance |
|
||||
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ---------------------- | ------- | --------------------------------- |
|
||||
| KEYCLOAK_CLIENT_ID | | Application client ID. |
|
||||
| KEYCLOAK_CLIENT_SECRET | | Application secret |
|
||||
| KEYCLOAK_REALM | | Your Keycloak Realm |
|
||||
| KEYCLOAK_BASE_URL | | Base URL of the Keycloak instance |
|
||||
|
||||
## Custom OAuth Provider (Auth)
|
||||
|
||||
@ -306,6 +304,17 @@ In order to be able to test your bot on WhatsApp from the Preview drawer, you ne
|
||||
| WHATSAPP_CLOUD_API_URL | https://graph.facebook.com | The WhatsApp Cloud API base URL |
|
||||
| WHATSAPP_INTERACTIVE_GROUP_SIZE | 3 | The array size of items to send to API on choice input. You can't choose a number higher than 3 if you are using the official cloud API URL. |
|
||||
|
||||
## Redis
|
||||
|
||||
In Typebot, Redis is optional and is used to:
|
||||
|
||||
- Rate limit the sign in requests based on user IP
|
||||
- Enable multiple media upload on WhatsApp
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| --------- | ------- | -------------------------------------------------------------------- |
|
||||
| REDIS_URL | | The database URL. i.e. `redis://<username>:<password>@<host>:<port>` |
|
||||
|
||||
## Others
|
||||
|
||||
The [official Typebot managed service](https://app.typebot.io/) uses other services such as [Stripe](https://stripe.com/) for processing payments, [Sentry](https://sentry.io/) for tracking bugs and [Sleekplan](https://sleekplan.com/) for user feedbacks.
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { publicProcedure } from '@/helpers/server/trpc'
|
||||
import { continueChatResponseSchema } from '@typebot.io/schemas/features/chat/schema'
|
||||
import {
|
||||
continueChatResponseSchema,
|
||||
messageSchema,
|
||||
} from '@typebot.io/schemas/features/chat/schema'
|
||||
import { z } from 'zod'
|
||||
import { continueChat as continueChatFn } from '@typebot.io/bot-engine/apiHandlers/continueChat'
|
||||
|
||||
@ -13,7 +16,7 @@ export const continueChat = publicProcedure
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
message: z.string().optional(),
|
||||
message: messageSchema.optional(),
|
||||
sessionId: z
|
||||
.string()
|
||||
.describe(
|
||||
|
@ -91,7 +91,9 @@ export const sendMessageV1 = publicProcedure
|
||||
typeof startParams.typebot === 'string'
|
||||
? undefined
|
||||
: startParams.typebot,
|
||||
message,
|
||||
message: message
|
||||
? { type: 'text', text: message }
|
||||
: undefined,
|
||||
userId: user?.id,
|
||||
textBubbleContentFormat: 'richText',
|
||||
}
|
||||
@ -102,10 +104,11 @@ export const sendMessageV1 = publicProcedure
|
||||
publicId: startParams.typebot,
|
||||
prefilledVariables: startParams.prefilledVariables,
|
||||
resultId: startParams.resultId,
|
||||
message,
|
||||
message: message
|
||||
? { type: 'text', text: message }
|
||||
: undefined,
|
||||
textBubbleContentFormat: 'richText',
|
||||
},
|
||||
message,
|
||||
})
|
||||
|
||||
if (startParams.isPreview || typeof startParams.typebot !== 'string') {
|
||||
@ -185,11 +188,14 @@ export const sendMessageV1 = publicProcedure
|
||||
lastMessageNewFormat,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
} = await continueBotFlow(message, {
|
||||
version: 1,
|
||||
state: session.state,
|
||||
textBubbleContentFormat: 'richText',
|
||||
})
|
||||
} = await continueBotFlow(
|
||||
message ? { type: 'text', text: message } : undefined,
|
||||
{
|
||||
version: 1,
|
||||
state: session.state,
|
||||
textBubbleContentFormat: 'richText',
|
||||
}
|
||||
)
|
||||
|
||||
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
|
||||
|
||||
|
@ -91,7 +91,9 @@ export const sendMessageV2 = publicProcedure
|
||||
typeof startParams.typebot === 'string'
|
||||
? undefined
|
||||
: startParams.typebot,
|
||||
message,
|
||||
message: message
|
||||
? { type: 'text', text: message }
|
||||
: undefined,
|
||||
userId: user?.id,
|
||||
textBubbleContentFormat: 'richText',
|
||||
}
|
||||
@ -102,10 +104,11 @@ export const sendMessageV2 = publicProcedure
|
||||
publicId: startParams.typebot,
|
||||
prefilledVariables: startParams.prefilledVariables,
|
||||
resultId: startParams.resultId,
|
||||
message,
|
||||
message: message
|
||||
? { type: 'text', text: message }
|
||||
: undefined,
|
||||
textBubbleContentFormat: 'richText',
|
||||
},
|
||||
message,
|
||||
})
|
||||
|
||||
if (startParams.isPreview || typeof startParams.typebot !== 'string') {
|
||||
@ -184,11 +187,14 @@ export const sendMessageV2 = publicProcedure
|
||||
lastMessageNewFormat,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
} = await continueBotFlow(message, {
|
||||
version: 2,
|
||||
state: session.state,
|
||||
textBubbleContentFormat: 'richText',
|
||||
})
|
||||
} = await continueBotFlow(
|
||||
message ? { type: 'text', text: message } : undefined,
|
||||
{
|
||||
version: 2,
|
||||
state: session.state,
|
||||
textBubbleContentFormat: 'richText',
|
||||
}
|
||||
)
|
||||
|
||||
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
|
||||
|
||||
|
@ -5,9 +5,14 @@ import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresigne
|
||||
import { env } from '@typebot.io/env'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
|
||||
import { parseGroups } from '@typebot.io/schemas'
|
||||
import {
|
||||
FileInputBlock,
|
||||
parseGroups,
|
||||
TextInputBlock,
|
||||
} from '@typebot.io/schemas'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
import { getBlockById } from '@typebot.io/schemas/helpers'
|
||||
import { PublicTypebot } from '@typebot.io/prisma'
|
||||
|
||||
export const generateUploadUrl = publicProcedure
|
||||
.meta({
|
||||
@ -50,27 +55,18 @@ export const generateUploadUrl = publicProcedure
|
||||
|
||||
const typebotId = session.state.typebotsQueue[0].typebot.id
|
||||
|
||||
const publicTypebot = await prisma.publicTypebot.findFirst({
|
||||
where: {
|
||||
typebotId,
|
||||
},
|
||||
select: {
|
||||
version: true,
|
||||
groups: true,
|
||||
typebot: {
|
||||
select: {
|
||||
workspaceId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
const isPreview = session.state.typebotsQueue[0].resultId
|
||||
|
||||
const workspaceId = publicTypebot?.typebot.workspaceId
|
||||
const typebot = session.state.typebotsQueue[0].resultId
|
||||
? await getAndParsePublicTypebot(
|
||||
session.state.typebotsQueue[0].typebot.id
|
||||
)
|
||||
: session.state.typebotsQueue[0].typebot
|
||||
|
||||
if (!workspaceId)
|
||||
if (!typebot?.version)
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: "Can't find workspaceId",
|
||||
message: "Can't find typebot",
|
||||
})
|
||||
|
||||
if (session.state.currentBlockId === undefined)
|
||||
@ -79,42 +75,93 @@ export const generateUploadUrl = publicProcedure
|
||||
message: "Can't find currentBlockId in session state",
|
||||
})
|
||||
|
||||
const { block: fileUploadBlock } = getBlockById(
|
||||
const { block } = getBlockById(
|
||||
session.state.currentBlockId,
|
||||
parseGroups(publicTypebot.groups, {
|
||||
typebotVersion: publicTypebot.version,
|
||||
parseGroups(typebot.groups, {
|
||||
typebotVersion: typebot.version,
|
||||
})
|
||||
)
|
||||
|
||||
if (fileUploadBlock?.type !== InputBlockType.FILE)
|
||||
if (
|
||||
block?.type !== InputBlockType.FILE &&
|
||||
(block.type !== InputBlockType.TEXT ||
|
||||
!block.options?.attachments?.isEnabled)
|
||||
)
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: "Can't find file upload block",
|
||||
message: 'Current block does not expect file upload',
|
||||
})
|
||||
|
||||
const { visibility, maxFileSize } = parseFileUploadParams(block)
|
||||
|
||||
const resultId = session.state.typebotsQueue[0].resultId
|
||||
|
||||
const filePath = `${
|
||||
fileUploadBlock.options?.visibility === 'Private' ? 'private' : 'public'
|
||||
}/workspaces/${workspaceId}/typebots/${typebotId}/results/${resultId}/${fileName}`
|
||||
const filePath =
|
||||
'workspaceId' in typebot && typebot.workspaceId
|
||||
? `${visibility === 'Private' ? 'private' : 'public'}/workspaces/${
|
||||
typebot.workspaceId
|
||||
}/typebots/${typebotId}/results/${resultId}/${fileName}`
|
||||
: `/public/tmp/${typebotId}/${fileName}`
|
||||
|
||||
const presignedPostPolicy = await generatePresignedPostPolicy({
|
||||
fileType,
|
||||
filePath,
|
||||
maxFileSize:
|
||||
fileUploadBlock.options && 'sizeLimit' in fileUploadBlock.options
|
||||
? (fileUploadBlock.options.sizeLimit as number)
|
||||
: env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE,
|
||||
maxFileSize,
|
||||
})
|
||||
|
||||
return {
|
||||
presignedUrl: presignedPostPolicy.postURL,
|
||||
formData: presignedPostPolicy.formData,
|
||||
fileUrl:
|
||||
fileUploadBlock.options?.visibility === 'Private'
|
||||
visibility === 'Private' && !isPreview
|
||||
? `${env.NEXTAUTH_URL}/api/typebots/${typebotId}/results/${resultId}/${fileName}`
|
||||
: env.S3_PUBLIC_CUSTOM_DOMAIN
|
||||
? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}`
|
||||
: `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`,
|
||||
}
|
||||
})
|
||||
|
||||
const getAndParsePublicTypebot = async (typebotId: string) => {
|
||||
const publicTypebot = (await prisma.publicTypebot.findFirst({
|
||||
where: {
|
||||
typebotId,
|
||||
},
|
||||
select: {
|
||||
version: true,
|
||||
groups: true,
|
||||
typebot: {
|
||||
select: {
|
||||
workspaceId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})) as (PublicTypebot & { typebot: { workspaceId: string } }) | null
|
||||
|
||||
return {
|
||||
...publicTypebot,
|
||||
workspaceId: publicTypebot?.typebot.workspaceId,
|
||||
}
|
||||
}
|
||||
|
||||
const parseFileUploadParams = (
|
||||
block: FileInputBlock | TextInputBlock
|
||||
): { visibility: 'Public' | 'Private'; maxFileSize: number | undefined } => {
|
||||
if (block.type === InputBlockType.FILE) {
|
||||
return {
|
||||
visibility:
|
||||
block.options?.visibility === 'Private' ? 'Private' : 'Public',
|
||||
maxFileSize:
|
||||
block.options && 'sizeLimit' in block.options
|
||||
? (block.options.sizeLimit as number)
|
||||
: env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
visibility:
|
||||
block.options?.attachments?.visibility === 'Private'
|
||||
? 'Private'
|
||||
: 'Public',
|
||||
maxFileSize: env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE,
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import { StartChatInput, StartPreviewChatInput } from '@typebot.io/schemas'
|
||||
|
||||
test.afterEach(async () => {
|
||||
await deleteWebhooks(['chat-webhook-id'])
|
||||
await deleteTypebots(['chat-sub-bot'])
|
||||
await deleteTypebots(['chat-sub-bot', 'starting-with-input'])
|
||||
})
|
||||
|
||||
test('API chat execution should work on preview bot', async ({ request }) => {
|
||||
@ -108,6 +108,13 @@ test('API chat execution should work on published bot', async ({ request }) => {
|
||||
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,
|
||||
@ -296,11 +303,12 @@ test('API chat execution should work on published bot', async ({ request }) => {
|
||||
expect(messages[2].content.richText.length).toBeGreaterThan(0)
|
||||
})
|
||||
await test.step('Starting with a message when typebot starts with input should proceed', async () => {
|
||||
const { messages } = await (
|
||||
const response = await (
|
||||
await request.post(
|
||||
`/api/v1/typebots/starting-with-input-public/startChat`,
|
||||
{
|
||||
data: {
|
||||
//@ts-expect-error We want to test if message is correctly preprocessed by zod
|
||||
message: 'Hey',
|
||||
isStreamEnabled: false,
|
||||
isOnlyRegistering: false,
|
||||
@ -309,7 +317,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
|
||||
}
|
||||
)
|
||||
).json()
|
||||
expect(messages[0].content.richText).toStrictEqual([
|
||||
expect(response.messages[0].content.richText).toStrictEqual([
|
||||
{
|
||||
children: [
|
||||
{
|
||||
|
@ -14,11 +14,11 @@ test('Transcript set variable should be correctly computed', async ({
|
||||
|
||||
await page.goto(`/${typebotId}-public`)
|
||||
await page.getByPlaceholder('Type your answer...').fill('hey')
|
||||
await page.getByRole('button').click()
|
||||
await page.getByLabel('Send').click()
|
||||
await page.getByPlaceholder('Type your answer...').fill('hey 2')
|
||||
await page.getByRole('button').click()
|
||||
await page.getByLabel('Send').click()
|
||||
await page.getByPlaceholder('Type your answer...').fill('hey 3')
|
||||
await page.getByRole('button').click()
|
||||
await page.getByLabel('Send').click()
|
||||
await expect(
|
||||
page.getByText('Assistant: "How are you? You said "')
|
||||
).toBeVisible()
|
||||
|
@ -31,8 +31,8 @@ test.beforeAll(async () => {
|
||||
|
||||
test('should work as expected', async ({ page }) => {
|
||||
await page.goto(`/${typebotId}-public`)
|
||||
await page.locator('input').fill('Hello there!')
|
||||
await page.locator('input').press('Enter')
|
||||
await page.getByPlaceholder('Type your answer...').fill('Hello there!')
|
||||
await page.getByPlaceholder('Type your answer...').press('Enter')
|
||||
await expect(page.getByText('Cheers!')).toBeVisible()
|
||||
await page.goto(`${env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
|
||||
await expect(page.locator('text=Hello there!')).toBeVisible()
|
||||
@ -41,8 +41,8 @@ test('should work as expected', async ({ page }) => {
|
||||
test.describe('Merge disabled', () => {
|
||||
test('should work as expected', async ({ page }) => {
|
||||
await page.goto(`/${typebotWithMergeDisabledId}-public`)
|
||||
await page.locator('input').fill('Hello there!')
|
||||
await page.locator('input').press('Enter')
|
||||
await page.getByPlaceholder('Type your answer...').fill('Hello there!')
|
||||
await page.getByPlaceholder('Type your answer...').press('Enter')
|
||||
await expect(page.getByText('Cheers!')).toBeVisible()
|
||||
await page.goto(
|
||||
`${process.env.NEXTAUTH_URL}/typebots/${typebotWithMergeDisabledId}/results`
|
||||
|
Reference in New Issue
Block a user