2
0

♻️ (builder) Change to features-centric folder structure

This commit is contained in:
Baptiste Arnaud
2022-11-15 09:35:48 +01:00
committed by Baptiste Arnaud
parent 3686465a85
commit 643571fe7d
683 changed files with 3907 additions and 3643 deletions

View File

@ -0,0 +1,26 @@
import test, { expect } from '@playwright/test'
import { typebotViewer } from 'utils/playwright/testHelpers'
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
import cuid from 'cuid'
import { getTestAsset } from '@/test/utils/playwright'
const typebotId = cuid()
test.describe('Code block', () => {
test('code should trigger', async ({ page }) => {
await importTypebotInDatabase(getTestAsset('typebots/logic/code.json'), {
id: typebotId,
})
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Configure...')
await page.fill(
'div[role="textbox"]',
'window.location.href = "https://www.google.com"'
)
await page.click('text=Preview')
await typebotViewer(page).locator('text=Trigger code').click()
await expect(page).toHaveURL('https://www.google.com')
})
})

View File

@ -0,0 +1,7 @@
import { CodeIcon as CodeIco } from '@/components/icons'
import { IconProps } from '@chakra-ui/react'
import React from 'react'
export const CodeIcon = (props: IconProps) => (
<CodeIco color="purple.500" {...props} />
)

View File

@ -0,0 +1,11 @@
import React from 'react'
import { Text } from '@chakra-ui/react'
import { CodeOptions } from 'models'
type Props = CodeOptions
export const CodeNodeContent = ({ name, content }: Props) => (
<Text color={content ? 'currentcolor' : 'gray.500'} noOfLines={1}>
{content ? `Run ${name}` : 'Configure...'}
</Text>
)

View File

@ -0,0 +1,51 @@
import { FormLabel, Stack, Text } from '@chakra-ui/react'
import { CodeEditor } from '@/components/CodeEditor'
import { CodeOptions } from 'models'
import React from 'react'
import { SwitchWithLabel } from '@/components/SwitchWithLabel'
import { Input } from '@/components/inputs'
type Props = {
options: CodeOptions
onOptionsChange: (options: CodeOptions) => void
}
export const CodeSettings = ({ options, onOptionsChange }: Props) => {
const handleNameChange = (name: string) =>
onOptionsChange({ ...options, name })
const handleCodeChange = (content: string) =>
onOptionsChange({ ...options, content })
const handleShouldExecuteInParentContextChange = (
shouldExecuteInParentContext: boolean
) => onOptionsChange({ ...options, shouldExecuteInParentContext })
return (
<Stack spacing={4}>
<Stack>
<FormLabel mb="0" htmlFor="name">
Name:
</FormLabel>
<Input
id="name"
defaultValue={options.name}
onChange={handleNameChange}
withVariableButton={false}
/>
</Stack>
<SwitchWithLabel
label="Execute in parent window"
moreInfoContent="Execute the code in the parent window context (when the bot is embedded). If it isn't detected, the code will be executed in the current window context."
initialValue={options.shouldExecuteInParentContext ?? false}
onCheckChange={handleShouldExecuteInParentContextChange}
/>
<Stack>
<Text>Code:</Text>
<CodeEditor
value={options.content ?? ''}
lang="js"
onChange={handleCodeChange}
/>
</Stack>
</Stack>
)
}

View File

@ -0,0 +1,3 @@
export { CodeSettings } from './components/CodeSettings'
export { CodeNodeContent } from './components/CodeNodeContent'
export { CodeIcon } from './components/CodeIcon'

View File

@ -0,0 +1,7 @@
import { FilterIcon } from '@/components/icons'
import { IconProps } from '@chakra-ui/react'
import React from 'react'
export const ConditionIcon = (props: IconProps) => (
<FilterIcon color="purple.500" {...props} />
)

View File

@ -0,0 +1,73 @@
import { Stack, Tag, Text, Flex, Wrap } from '@chakra-ui/react'
import { useTypebot } from '@/features/editor'
import { Comparison, ConditionItem, ComparisonOperators } from 'models'
import React from 'react'
import { byId, isNotDefined } from 'utils'
type Props = {
item: ConditionItem
}
export const ConditionNodeContent = ({ item }: Props) => {
const { typebot } = useTypebot()
return (
<Flex px={2} py={2}>
{item.content.comparisons.length === 0 ||
comparisonIsEmpty(item.content.comparisons[0]) ? (
<Text color={'gray.500'}>Configure...</Text>
) : (
<Stack maxW="170px">
{item.content.comparisons.map((comparison, idx) => {
const variable = typebot?.variables.find(
byId(comparison.variableId)
)
return (
<Wrap key={comparison.id} spacing={1} noOfLines={1}>
{idx > 0 && <Text>{item.content.logicalOperator ?? ''}</Text>}
{variable?.name && (
<Tag bgColor="orange.400" color="white">
{variable.name}
</Tag>
)}
{comparison.comparisonOperator && (
<Text>
{parseComparisonOperatorSymbol(
comparison.comparisonOperator
)}
</Text>
)}
{comparison?.value && (
<Tag bgColor={'gray.200'}>
<Text noOfLines={1}>{comparison.value}</Text>
</Tag>
)}
</Wrap>
)
})}
</Stack>
)}
</Flex>
)
}
const comparisonIsEmpty = (comparison: Comparison) =>
isNotDefined(comparison.comparisonOperator) &&
isNotDefined(comparison.value) &&
isNotDefined(comparison.variableId)
const parseComparisonOperatorSymbol = (operator: ComparisonOperators) => {
switch (operator) {
case ComparisonOperators.CONTAINS:
return 'contains'
case ComparisonOperators.EQUAL:
return '='
case ComparisonOperators.GREATER:
return '>'
case ComparisonOperators.IS_SET:
return 'is set'
case ComparisonOperators.LESS:
return '<'
case ComparisonOperators.NOT_EQUAL:
return '!='
}
}

View File

@ -0,0 +1,50 @@
import { Stack } from '@chakra-ui/react'
import { DropdownList } from '@/components/DropdownList'
import { Comparison, Variable, ComparisonOperators } from 'models'
import { TableListItemProps } from '@/components/TableList'
import { VariableSearchInput } from '@/components/VariableSearchInput'
import { Input } from '@/components/inputs'
export const ComparisonItem = ({
item,
onItemChange,
}: TableListItemProps<Comparison>) => {
const handleSelectVariable = (variable?: Variable) => {
if (variable?.id === item.variableId) return
onItemChange({ ...item, variableId: variable?.id })
}
const handleSelectComparisonOperator = (
comparisonOperator: ComparisonOperators
) => {
if (comparisonOperator === item.comparisonOperator) return
onItemChange({ ...item, comparisonOperator })
}
const handleChangeValue = (value: string) => {
if (value === item.value) return
onItemChange({ ...item, value })
}
return (
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
<VariableSearchInput
initialVariableId={item.variableId}
onSelectVariable={handleSelectVariable}
placeholder="Search for a variable"
/>
<DropdownList<ComparisonOperators>
currentItem={item.comparisonOperator}
onItemSelect={handleSelectComparisonOperator}
items={Object.values(ComparisonOperators)}
placeholder="Select an operator"
/>
{item.comparisonOperator !== ComparisonOperators.IS_SET && (
<Input
defaultValue={item.value ?? ''}
onChange={handleChangeValue}
placeholder="Type a value..."
/>
)}
</Stack>
)
}

View File

@ -0,0 +1,46 @@
import { Flex } from '@chakra-ui/react'
import { DropdownList } from '@/components/DropdownList'
import {
Comparison,
ConditionItem,
ConditionBlock,
LogicalOperator,
} from 'models'
import React from 'react'
import { ComparisonItem } from './ComparisonsItem'
import { TableList } from '@/components/TableList'
type ConditionSettingsBodyProps = {
block: ConditionBlock
onItemChange: (updates: Partial<ConditionItem>) => void
}
export const ConditionSettingsBody = ({
block,
onItemChange,
}: ConditionSettingsBodyProps) => {
const itemContent = block.items[0].content
const handleComparisonsChange = (comparisons: Comparison[]) =>
onItemChange({ content: { ...itemContent, comparisons } })
const handleLogicalOperatorChange = (logicalOperator: LogicalOperator) =>
onItemChange({ content: { ...itemContent, logicalOperator } })
return (
<TableList<Comparison>
initialItems={itemContent.comparisons}
onItemsChange={handleComparisonsChange}
Item={ComparisonItem}
ComponentBetweenItems={() => (
<Flex justify="center">
<DropdownList<LogicalOperator>
currentItem={itemContent.logicalOperator}
onItemSelect={handleLogicalOperatorChange}
items={Object.values(LogicalOperator)}
/>
</Flex>
)}
addLabel="Add a comparison"
/>
)
}

View File

@ -0,0 +1 @@
export { ConditionSettingsBody } from './ConditonSettingsBody'

View File

@ -0,0 +1,80 @@
import test, { expect } from '@playwright/test'
import { typebotViewer } from 'utils/playwright/testHelpers'
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
import cuid from 'cuid'
import { getTestAsset } from '@/test/utils/playwright'
const typebotId = cuid()
test.describe('Condition block', () => {
test('its configuration should work', async ({ page }) => {
await importTypebotInDatabase(
getTestAsset('typebots/logic/condition.json'),
{
id: typebotId,
}
)
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Configure... >> nth=0', { force: true })
await page.fill(
'input[placeholder="Search for a variable"] >> nth=-1',
'Age'
)
await page.click('button:has-text("Age")')
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 value..."]', '80')
await page.click('button:has-text("Add a comparison")')
await page.fill(
':nth-match(input[placeholder="Search for a variable"], 2)',
'Age'
)
await page.click('button:has-text("Age")')
await page.click('button:has-text("Select an operator")')
await page.click('button:has-text("Less than")', { force: true })
await page.fill(
':nth-match(input[placeholder="Type a value..."], 2)',
'100'
)
await page.click('text=Configure...', { force: true })
await page.fill(
'input[placeholder="Search for a variable"] >> nth=-1',
'Age'
)
await page.click('button:has-text("Age")')
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 value..."]', '20')
await page.click('text=Preview')
await typebotViewer(page)
.locator('input[placeholder="Type a number..."]')
.fill('15')
await typebotViewer(page).locator('text=Send').click()
await expect(
typebotViewer(page).locator('text=You are younger than 20')
).toBeVisible()
await page.click('text=Restart')
await typebotViewer(page)
.locator('input[placeholder="Type a number..."]')
.fill('45')
await typebotViewer(page).locator('text=Send').click()
await expect(
typebotViewer(page).locator('text=You are older than 20')
).toBeVisible()
await page.click('text=Restart')
await typebotViewer(page)
.locator('input[placeholder="Type a number..."]')
.fill('90')
await typebotViewer(page).locator('text=Send').click()
await expect(
typebotViewer(page).locator('text=You are older than 80')
).toBeVisible()
})
})

View File

@ -0,0 +1,3 @@
export { ConditionSettingsBody } from './components/ConditionSettingsBody'
export { ConditionNodeContent } from './components/ConditionNodeContent'
export { ConditionIcon } from './components/ConditionIcon'

View File

@ -0,0 +1,7 @@
import { ExternalLinkIcon } from '@/components/icons'
import { IconProps } from '@chakra-ui/react'
import React from 'react'
export const RedirectIcon = (props: IconProps) => (
<ExternalLinkIcon color="purple.500" {...props} />
)

View File

@ -0,0 +1,11 @@
import React from 'react'
import { Text } from '@chakra-ui/react'
import { RedirectOptions } from 'models'
type Props = { url: RedirectOptions['url'] }
export const RedirectNodeContent = ({ url }: Props) => (
<Text color={url ? 'currentcolor' : 'gray.500'} noOfLines={1}>
{url ? `Redirect to ${url}` : 'Configure...'}
</Text>
)

View File

@ -0,0 +1,38 @@
import { Input } from '@/components/inputs'
import { SwitchWithLabel } from '@/components/SwitchWithLabel'
import { FormLabel, Stack } from '@chakra-ui/react'
import { RedirectOptions } from 'models'
import React from 'react'
type Props = {
options: RedirectOptions
onOptionsChange: (options: RedirectOptions) => void
}
export const RedirectSettings = ({ options, onOptionsChange }: Props) => {
const handleUrlChange = (url?: string) => onOptionsChange({ ...options, url })
const handleIsNewTabChange = (isNewTab: boolean) =>
onOptionsChange({ ...options, isNewTab })
return (
<Stack spacing={4}>
<Stack>
<FormLabel mb="0" htmlFor="tracking-id">
Url:
</FormLabel>
<Input
id="tracking-id"
defaultValue={options.url ?? ''}
placeholder="Type a URL..."
onChange={handleUrlChange}
/>
</Stack>
<SwitchWithLabel
label="Open in new tab?"
initialValue={options.isNewTab}
onCheckChange={handleIsNewTabChange}
/>
</Stack>
)
}

View File

@ -0,0 +1,3 @@
export { RedirectSettings } from './components/RedirectSettings'
export { RedirectNodeContent } from './components/RedirectNodeContent'
export { RedirectIcon } from './components/RedirectIcon'

View File

@ -0,0 +1,38 @@
import test, { expect } from '@playwright/test'
import { typebotViewer } from 'utils/playwright/testHelpers'
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
import cuid from 'cuid'
import { getTestAsset } from '@/test/utils/playwright'
const typebotId = cuid()
test.describe('Redirect block', () => {
test('its configuration should work', async ({ page, context }) => {
await importTypebotInDatabase(
getTestAsset('typebots/logic/redirect.json'),
{
id: typebotId,
}
)
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Configure...')
await page.fill('input[placeholder="Type a URL..."]', 'google.com')
await page.click('text=Preview')
await typebotViewer(page).locator('text=Go to URL').click()
await expect(page).toHaveURL('https://www.google.com')
await page.goBack()
await page.click('text=Redirect to google.com')
await page.click('text=Open in new tab')
await page.click('text=Preview')
const [newPage] = await Promise.all([
context.waitForEvent('page'),
typebotViewer(page).locator('text=Go to URL').click(),
])
await newPage.waitForLoadState()
await expect(newPage).toHaveURL('https://www.google.com')
})
})

View File

@ -0,0 +1,18 @@
import { Text } from '@chakra-ui/react'
import { useTypebot } from '@/features/editor'
import { SetVariableBlock } from 'models'
import { byId } from 'utils'
export const SetVariableContent = ({ block }: { block: SetVariableBlock }) => {
const { typebot } = useTypebot()
const variableName =
typebot?.variables.find(byId(block.options.variableId))?.name ?? ''
const expression = block.options.expressionToEvaluate ?? ''
return (
<Text color={'gray.500'} noOfLines={2}>
{variableName === '' && expression === ''
? 'Click to edit...'
: `${variableName} ${expression ? `= ${expression}` : ``}`}
</Text>
)
}

View File

@ -0,0 +1,7 @@
import { EditIcon } from '@/components/icons'
import { IconProps } from '@chakra-ui/react'
import React from 'react'
export const SetVariableIcon = (props: IconProps) => (
<EditIcon color="purple.500" {...props} />
)

View File

@ -0,0 +1,68 @@
import { FormLabel, HStack, Stack, Switch, Text } from '@chakra-ui/react'
import { CodeEditor } from '@/components/CodeEditor'
import { SetVariableOptions, Variable } from 'models'
import React from 'react'
import { VariableSearchInput } from '@/components/VariableSearchInput'
import { Textarea } from '@/components/inputs'
type Props = {
options: SetVariableOptions
onOptionsChange: (options: SetVariableOptions) => void
}
export const SetVariableSettings = ({ options, onOptionsChange }: Props) => {
const handleVariableChange = (variable?: Variable) =>
onOptionsChange({ ...options, variableId: variable?.id })
const handleExpressionChange = (expressionToEvaluate: string) =>
onOptionsChange({ ...options, expressionToEvaluate })
const handleValueTypeChange = () =>
onOptionsChange({
...options,
isCode: options.isCode ? !options.isCode : true,
})
return (
<Stack spacing={4}>
<Stack>
<FormLabel mb="0" htmlFor="variable-search">
Search or create variable:
</FormLabel>
<VariableSearchInput
onSelectVariable={handleVariableChange}
initialVariableId={options.variableId}
id="variable-search"
/>
</Stack>
<Stack>
<HStack justify="space-between">
<FormLabel mb="0" htmlFor="expression">
Value:
</FormLabel>
<HStack>
<Text fontSize="sm">Text</Text>
<Switch
size="sm"
isChecked={options.isCode ?? false}
onChange={handleValueTypeChange}
/>
<Text fontSize="sm">Code</Text>
</HStack>
</HStack>
{options.isCode ?? false ? (
<CodeEditor
value={options.expressionToEvaluate ?? ''}
onChange={handleExpressionChange}
lang="js"
/>
) : (
<Textarea
id="expression"
defaultValue={options.expressionToEvaluate ?? ''}
onChange={handleExpressionChange}
/>
)}
</Stack>
</Stack>
)
}

View File

@ -0,0 +1,3 @@
export { SetVariableSettings } from './components/SetVariableSettings'
export { SetVariableContent } from './components/SetVariableContent'
export { SetVariableIcon } from './components/SetVariableIcon'

View File

@ -0,0 +1,59 @@
import test, { expect } from '@playwright/test'
import { typebotViewer } from 'utils/playwright/testHelpers'
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
import cuid from 'cuid'
import { getTestAsset } from '@/test/utils/playwright'
const typebotId = cuid()
test.describe('Set variable block', () => {
test('its configuration should work', async ({ page }) => {
await importTypebotInDatabase(
getTestAsset('typebots/logic/setVariable.json'),
{
id: typebotId,
}
)
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Type a number...')
await page.fill('input[placeholder="Select a variable"] >> nth=-1', 'Num')
await page.click('text=Create "Num"')
await page.click('text=Click to edit... >> nth = 0')
await page.fill('input[placeholder="Select a variable"] >> nth=-1', 'Total')
await page.click('text=Create "Total"')
await page.fill('textarea', '1000 * {{Num}}')
await page.click('text=Click to edit...', { force: true })
await page.fill(
'input[placeholder="Select a variable"] >> nth=-1',
'Custom var'
)
await page.click('text=Create "Custom var"')
await page.fill('textarea', 'Custom value')
await page.click('text=Click to edit...', { force: true })
await page.fill(
'input[placeholder="Select a variable"] >> nth=-1',
'Addition'
)
await page.click('text=Create "Addition"')
await page.fill('textarea', '1000 + {{Total}}')
await page.click('text=Preview')
await typebotViewer(page)
.locator('input[placeholder="Type a number..."]')
.fill('365')
await typebotViewer(page).locator('text=Send').click()
await expect(
typebotViewer(page).locator('text=Multiplication: 365000')
).toBeVisible()
await expect(
typebotViewer(page).locator('text=Custom var: Custom value')
).toBeVisible()
await expect(
typebotViewer(page).locator('text=Addition: 366000')
).toBeVisible()
})
})

View File

@ -0,0 +1,44 @@
import { TypebotLinkBlock } from 'models'
import React from 'react'
import { Tag, Text } from '@chakra-ui/react'
import { useTypebot } from '@/features/editor'
import { byId } from 'utils'
type Props = {
block: TypebotLinkBlock
}
export const TypebotLinkContent = ({ block }: Props) => {
const { linkedTypebots, typebot } = useTypebot()
const isCurrentTypebot =
typebot &&
(block.options.typebotId === typebot.id ||
block.options.typebotId === 'current')
const linkedTypebot = isCurrentTypebot
? typebot
: linkedTypebots?.find(byId(block.options.typebotId))
const blockTitle = linkedTypebot?.groups.find(
byId(block.options.groupId)
)?.title
if (!block.options.typebotId)
return <Text color="gray.500">Configure...</Text>
return (
<Text>
Jump{' '}
{blockTitle ? (
<>
to <Tag>{blockTitle}</Tag>
</>
) : (
<></>
)}{' '}
{!isCurrentTypebot ? (
<>
in <Tag colorScheme="blue">{linkedTypebot?.name}</Tag>
</>
) : (
<></>
)}
</Text>
)
}

View File

@ -0,0 +1,7 @@
import { BoxIcon } from '@/components/icons'
import { IconProps } from '@chakra-ui/react'
import React from 'react'
export const TypebotLinkIcon = (props: IconProps) => (
<BoxIcon color="purple.500" {...props} />
)

View File

@ -0,0 +1,41 @@
import { SearchableDropdown } from '@/components/SearchableDropdown'
import { Input } from '@chakra-ui/react'
import { Group } from 'models'
import { useMemo } from 'react'
import { byId } from 'utils'
type Props = {
groups: Group[]
groupId?: string
onGroupIdSelected: (groupId: string) => void
isLoading?: boolean
}
export const GroupsDropdown = ({
groups,
groupId,
onGroupIdSelected,
isLoading,
}: Props) => {
const currentGroup = useMemo(
() => groups?.find(byId(groupId)),
[groupId, groups]
)
const handleGroupSelect = (title: string) => {
const id = groups?.find((b) => b.title === title)?.id
if (id) onGroupIdSelected(id)
}
if (isLoading) return <Input value="Loading..." isDisabled />
if (!groups || groups.length === 0)
return <Input value="No groups found" isDisabled />
return (
<SearchableDropdown
selectedItem={currentGroup?.title}
items={(groups ?? []).map((b) => b.title)}
onValueChange={handleGroupSelect}
placeholder={'Select a block'}
/>
)
}

View File

@ -0,0 +1,50 @@
import { Stack } from '@chakra-ui/react'
import { useTypebot } from '@/features/editor'
import { TypebotLinkOptions } from 'models'
import { byId } from 'utils'
import { GroupsDropdown } from './GroupsDropdown'
import { TypebotsDropdown } from './TypebotsDropdown'
type Props = {
options: TypebotLinkOptions
onOptionsChange: (options: TypebotLinkOptions) => void
}
export const TypebotLinkSettingsForm = ({
options,
onOptionsChange,
}: Props) => {
const { linkedTypebots, typebot } = useTypebot()
const handleTypebotIdChange = (typebotId: string | 'current') =>
onOptionsChange({ ...options, typebotId })
const handleGroupIdChange = (groupId: string) =>
onOptionsChange({ ...options, groupId })
return (
<Stack>
{typebot && (
<TypebotsDropdown
typebotId={options.typebotId}
onSelectTypebotId={handleTypebotIdChange}
currentWorkspaceId={typebot.workspaceId as string}
/>
)}
<GroupsDropdown
groups={
typebot &&
(options.typebotId === typebot.id || options.typebotId === 'current')
? typebot.groups
: linkedTypebots?.find(byId(options.typebotId))?.groups ?? []
}
groupId={options.groupId}
onGroupIdSelected={handleGroupIdChange}
isLoading={
linkedTypebots === undefined &&
typebot &&
typebot.id !== options.typebotId
}
/>
</Stack>
)
}

View File

@ -0,0 +1,63 @@
import { HStack, IconButton, Input } from '@chakra-ui/react'
import { ExternalLinkIcon } from '@/components/icons'
import { useToast } from '@/hooks/useToast'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useMemo } from 'react'
import { byId } from 'utils'
import { useTypebots } from '@/features/dashboard'
import { SearchableDropdown } from '@/components/SearchableDropdown'
type Props = {
typebotId?: string | 'current'
currentWorkspaceId: string
onSelectTypebotId: (typebotId: string | 'current') => void
}
export const TypebotsDropdown = ({
typebotId,
onSelectTypebotId,
currentWorkspaceId,
}: Props) => {
const { query } = useRouter()
const { showToast } = useToast()
const { typebots, isLoading } = useTypebots({
workspaceId: currentWorkspaceId,
allFolders: true,
onError: (e) => showToast({ title: e.name, description: e.message }),
})
const currentTypebot = useMemo(
() => typebots?.find(byId(typebotId)),
[typebotId, typebots]
)
const handleTypebotSelect = (name: string) => {
if (name === 'Current typebot') return onSelectTypebotId('current')
const id = typebots?.find((s) => s.name === name)?.id
if (id) onSelectTypebotId(id)
}
if (isLoading) return <Input value="Loading..." isDisabled />
if (!typebots || typebots.length === 0)
return <Input value="No typebots found" isDisabled />
return (
<HStack>
<SearchableDropdown
selectedItem={
typebotId === 'current' ? 'Current typebot' : currentTypebot?.name
}
items={['Current typebot', ...(typebots ?? []).map((t) => t.name)]}
onValueChange={handleTypebotSelect}
placeholder={'Select a typebot'}
/>
{currentTypebot?.id && (
<IconButton
aria-label="Navigate to typebot"
icon={<ExternalLinkIcon />}
as={Link}
href={`/typebots/${currentTypebot?.id}/edit?parentId=${query.typebotId}`}
/>
)}
</HStack>
)
}

View File

@ -0,0 +1 @@
export { TypebotLinkSettingsForm } from './TypebotLinkSettingsForm'

View File

@ -0,0 +1,3 @@
export { TypebotLinkSettingsForm } from './components/TypebotLinkSettingsForm'
export { TypebotLinkContent } from './components/TypebotLinkContent'
export { TypebotLinkIcon } from './components/TypebotLinkIcon'

View File

@ -0,0 +1,63 @@
import test, { expect } from '@playwright/test'
import { typebotViewer } from 'utils/playwright/testHelpers'
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
import cuid from 'cuid'
import { getTestAsset } from '@/test/utils/playwright'
test('should be configurable', async ({ page }) => {
const typebotId = cuid()
const linkedTypebotId = cuid()
await importTypebotInDatabase(
getTestAsset('typebots/logic/linkTypebots/1.json'),
{ id: typebotId, name: 'My link typebot 1' }
)
await importTypebotInDatabase(
getTestAsset('typebots/logic/linkTypebots/2.json'),
{ id: linkedTypebotId, name: 'My link typebot 2' }
)
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Configure...')
await page.click('input[placeholder="Select a typebot"]')
await page.click('text=My link typebot 2')
await expect(page.locator('input[value="My link typebot 2"]')).toBeVisible()
await expect(page.getByText('Jump in My link typebot 2')).toBeVisible()
await page.click('[aria-label="Navigate to typebot"]')
await expect(page).toHaveURL(
`/typebots/${linkedTypebotId}/edit?parentId=${typebotId}`
)
await page.click('[aria-label="Navigate back"]')
await expect(page).toHaveURL(`/typebots/${typebotId}/edit`)
await page.click('text=Jump in My link typebot 2')
await expect(page.locator('input[value="My link typebot 2"]')).toBeVisible()
await page.click('input[placeholder="Select a block"]')
await page.click('text=Group #2')
await page.click('text=Preview')
await expect(typebotViewer(page).locator('text=Second block')).toBeVisible()
await page.click('[aria-label="Close"]')
await page.click('text=Jump to Group #2 in My link typebot 2')
await page.click('input[value="Group #2"]', { clickCount: 3 })
await page.press('input[value="Group #2"]', 'Backspace')
await page.click('button >> text=Start')
await page.click('text=Preview')
await typebotViewer(page).locator('input').fill('Hello there!')
await typebotViewer(page).locator('input').press('Enter')
await expect(typebotViewer(page).locator('text=Hello there!')).toBeVisible()
await page.click('[aria-label="Close"]')
await page.click('text=Jump to Start in My link typebot 2')
await page.click('input[value="My link typebot 2"]', { clickCount: 3 })
await page.press('input[value="My link typebot 2"]', 'Backspace')
await page.click('button >> text=My link typebot 1')
await page.click('input[placeholder="Select a block"]', {
clickCount: 3,
})
await page.press('input[placeholder="Select a block"]', 'Backspace')
await page.click('button >> text=Hello')
await page.click('text=Preview')
await expect(typebotViewer(page).locator('text=Hello world')).toBeVisible()
})