♻️ (builder) Change to features-centric folder structure
This commit is contained in:
committed by
Baptiste Arnaud
parent
3686465a85
commit
643571fe7d
26
apps/builder/src/features/blocks/logic/code/code.spec.ts
Normal file
26
apps/builder/src/features/blocks/logic/code/code.spec.ts
Normal 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')
|
||||
})
|
||||
})
|
@ -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} />
|
||||
)
|
@ -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>
|
||||
)
|
@ -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>
|
||||
)
|
||||
}
|
3
apps/builder/src/features/blocks/logic/code/index.ts
Normal file
3
apps/builder/src/features/blocks/logic/code/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { CodeSettings } from './components/CodeSettings'
|
||||
export { CodeNodeContent } from './components/CodeNodeContent'
|
||||
export { CodeIcon } from './components/CodeIcon'
|
@ -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} />
|
||||
)
|
@ -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 '!='
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { ConditionSettingsBody } from './ConditonSettingsBody'
|
@ -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()
|
||||
})
|
||||
})
|
@ -0,0 +1,3 @@
|
||||
export { ConditionSettingsBody } from './components/ConditionSettingsBody'
|
||||
export { ConditionNodeContent } from './components/ConditionNodeContent'
|
||||
export { ConditionIcon } from './components/ConditionIcon'
|
@ -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} />
|
||||
)
|
@ -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>
|
||||
)
|
@ -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>
|
||||
)
|
||||
}
|
3
apps/builder/src/features/blocks/logic/redirect/index.ts
Normal file
3
apps/builder/src/features/blocks/logic/redirect/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { RedirectSettings } from './components/RedirectSettings'
|
||||
export { RedirectNodeContent } from './components/RedirectNodeContent'
|
||||
export { RedirectIcon } from './components/RedirectIcon'
|
@ -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')
|
||||
})
|
||||
})
|
@ -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>
|
||||
)
|
||||
}
|
@ -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} />
|
||||
)
|
@ -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>
|
||||
)
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export { SetVariableSettings } from './components/SetVariableSettings'
|
||||
export { SetVariableContent } from './components/SetVariableContent'
|
||||
export { SetVariableIcon } from './components/SetVariableIcon'
|
@ -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()
|
||||
})
|
||||
})
|
@ -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>
|
||||
)
|
||||
}
|
@ -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} />
|
||||
)
|
@ -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'}
|
||||
/>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { TypebotLinkSettingsForm } from './TypebotLinkSettingsForm'
|
@ -0,0 +1,3 @@
|
||||
export { TypebotLinkSettingsForm } from './components/TypebotLinkSettingsForm'
|
||||
export { TypebotLinkContent } from './components/TypebotLinkContent'
|
||||
export { TypebotLinkIcon } from './components/TypebotLinkIcon'
|
@ -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()
|
||||
})
|
Reference in New Issue
Block a user