@@ -17,6 +17,7 @@ import { useDebouncedCallback } from 'use-debounce'
|
|||||||
import { env, isDefined } from 'utils'
|
import { env, isDefined } from 'utils'
|
||||||
import { VariablesButton } from '@/features/variables'
|
import { VariablesButton } from '@/features/variables'
|
||||||
import { useOutsideClick } from '@/hooks/useOutsideClick'
|
import { useOutsideClick } from '@/hooks/useOutsideClick'
|
||||||
|
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
selectedItem?: string
|
selectedItem?: string
|
||||||
@@ -58,6 +59,7 @@ export const SearchableDropdown = ({
|
|||||||
const dropdownRef = useRef(null)
|
const dropdownRef = useRef(null)
|
||||||
const itemsRef = useRef<(HTMLButtonElement | null)[]>([])
|
const itemsRef = useRef<(HTMLButtonElement | null)[]>([])
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const { ref: parentModalRef } = useParentModal()
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => () => {
|
() => () => {
|
||||||
@@ -195,7 +197,7 @@ export const SearchableDropdown = ({
|
|||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
</PopoverAnchor>
|
</PopoverAnchor>
|
||||||
<Portal>
|
<Portal containerRef={parentModalRef}>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
maxH="35vh"
|
maxH="35vh"
|
||||||
overflowY="scroll"
|
overflowY="scroll"
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ test.describe.parallel('Buttons input block', () => {
|
|||||||
await page.click('[data-testid="block2-icon"]')
|
await page.click('[data-testid="block2-icon"]')
|
||||||
await page.click('text=Multiple choice?')
|
await page.click('text=Multiple choice?')
|
||||||
await page.fill('#button', 'Go')
|
await page.fill('#button', 'Go')
|
||||||
await page.getByPlaceholder('Select a variable').click()
|
await page.getByPlaceholder('Select a variable').nth(1).click()
|
||||||
await page.getByText('var1').click()
|
await page.getByText('var1').click()
|
||||||
await expect(page.getByText('Collectsvar1')).toBeVisible()
|
await expect(page.getByText('Collectsvar1')).toBeVisible()
|
||||||
await page.click('[data-testid="block2-icon"]')
|
await page.click('[data-testid="block2-icon"]')
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { BlockIndices, ChoiceInputBlock, Variable } from 'models'
|
||||||
|
import React from 'react'
|
||||||
|
import { ItemNodesList } from '@/features/graph/components/Nodes/ItemNode'
|
||||||
|
import {
|
||||||
|
HStack,
|
||||||
|
Stack,
|
||||||
|
Tag,
|
||||||
|
Text,
|
||||||
|
useColorModeValue,
|
||||||
|
Wrap,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { useTypebot } from '@/features/editor'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
block: ChoiceInputBlock
|
||||||
|
indices: BlockIndices
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ButtonsBlockNode = ({ block, indices }: Props) => {
|
||||||
|
const { typebot } = useTypebot()
|
||||||
|
const dynamicVariableName = typebot?.variables.find(
|
||||||
|
(variable) => variable.id === block.options.dynamicVariableId
|
||||||
|
)?.name
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack w="full">
|
||||||
|
{block.options.variableId ? (
|
||||||
|
<CollectVariableLabel
|
||||||
|
variableId={block.options.variableId}
|
||||||
|
variables={typebot?.variables}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{block.options.dynamicVariableId ? (
|
||||||
|
<Wrap spacing={1}>
|
||||||
|
<Text>Display</Text>
|
||||||
|
<Tag bg="orange.400" color="white">
|
||||||
|
{dynamicVariableName}
|
||||||
|
</Tag>
|
||||||
|
<Text>buttons</Text>
|
||||||
|
</Wrap>
|
||||||
|
) : (
|
||||||
|
<ItemNodesList block={block} indices={indices} />
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CollectVariableLabel = ({
|
||||||
|
variableId,
|
||||||
|
variables,
|
||||||
|
}: {
|
||||||
|
variableId: string
|
||||||
|
variables?: Variable[]
|
||||||
|
}) => {
|
||||||
|
const textColor = useColorModeValue('gray.600', 'gray.400')
|
||||||
|
const variableName = variables?.find(
|
||||||
|
(variable) => variable.id === variableId
|
||||||
|
)?.name
|
||||||
|
|
||||||
|
if (!variableName) return null
|
||||||
|
return (
|
||||||
|
<HStack fontStyle="italic" spacing={1}>
|
||||||
|
<Text fontSize="sm" color={textColor}>
|
||||||
|
Collects
|
||||||
|
</Text>
|
||||||
|
<Tag bg="orange.400" color="white" size="sm">
|
||||||
|
{variableName}
|
||||||
|
</Tag>
|
||||||
|
</HStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Input } from '@/components/inputs'
|
import { Input } from '@/components/inputs'
|
||||||
|
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
||||||
import { SwitchWithLabel } from '@/components/SwitchWithLabel'
|
import { SwitchWithLabel } from '@/components/SwitchWithLabel'
|
||||||
import { VariableSearchInput } from '@/components/VariableSearchInput'
|
import { VariableSearchInput } from '@/components/VariableSearchInput'
|
||||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
import { FormControl, FormLabel, Stack } from '@chakra-ui/react'
|
||||||
import { ChoiceInputOptions, Variable } from 'models'
|
import { ChoiceInputOptions, Variable } from 'models'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
@@ -20,6 +21,8 @@ export const ButtonsOptionsForm = ({
|
|||||||
options && onOptionsChange({ ...options, buttonLabel })
|
options && onOptionsChange({ ...options, buttonLabel })
|
||||||
const handleVariableChange = (variable?: Variable) =>
|
const handleVariableChange = (variable?: Variable) =>
|
||||||
options && onOptionsChange({ ...options, variableId: variable?.id })
|
options && onOptionsChange({ ...options, variableId: variable?.id })
|
||||||
|
const handleDynamicVariableChange = (variable?: Variable) =>
|
||||||
|
options && onOptionsChange({ ...options, dynamicVariableId: variable?.id })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
@@ -40,6 +43,19 @@ export const ButtonsOptionsForm = ({
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>
|
||||||
|
Dynamic items from variable:{' '}
|
||||||
|
<MoreInfoTooltip>
|
||||||
|
If defined, buttons will be dynamically displayed based on what the
|
||||||
|
variable contains.
|
||||||
|
</MoreInfoTooltip>
|
||||||
|
</FormLabel>
|
||||||
|
<VariableSearchInput
|
||||||
|
initialVariableId={options?.dynamicVariableId}
|
||||||
|
onSelectVariable={handleDynamicVariableChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
<Stack>
|
<Stack>
|
||||||
<FormLabel mb="0" htmlFor="variable">
|
<FormLabel mb="0" htmlFor="variable">
|
||||||
Save answer in a variable:
|
Save answer in a variable:
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from './ButtonsItemNode'
|
export * from './ButtonsItemNode'
|
||||||
export * from './ButtonsIcon'
|
export * from './ButtonsIcon'
|
||||||
export * from './ButtonsOptionsForm'
|
export * from './ButtonsBlockSettings'
|
||||||
|
|||||||
@@ -6,21 +6,26 @@ export const getDeepKeys = (obj: any): string[] => {
|
|||||||
const subkeys = getDeepKeys(obj[key])
|
const subkeys = getDeepKeys(obj[key])
|
||||||
keys = keys.concat(
|
keys = keys.concat(
|
||||||
subkeys.map(function (subkey) {
|
subkeys.map(function (subkey) {
|
||||||
return key + '.' + subkey
|
return key + parseKey(subkey)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
} else if (Array.isArray(obj[key])) {
|
} else if (Array.isArray(obj[key])) {
|
||||||
for (let i = 0; i < obj[key].length; i++) {
|
const subkeys = getDeepKeys(obj[key][0])
|
||||||
const subkeys = getDeepKeys(obj[key][i])
|
keys = keys.concat(
|
||||||
keys = keys.concat(
|
subkeys.map(function (subkey) {
|
||||||
subkeys.map(function (subkey) {
|
return `${key}.map(item => item${parseKey(subkey)})`
|
||||||
return key + '[' + i + ']' + '.' + subkey
|
})
|
||||||
})
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
keys.push(key)
|
keys.push(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parseKey = (key: string) => {
|
||||||
|
if (key.includes(' ') && !key.includes('.map((item) => item')) {
|
||||||
|
return `['${key}']`
|
||||||
|
}
|
||||||
|
return `.${key}`
|
||||||
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ test.describe('Builder', () => {
|
|||||||
await page.click('text=Save in variables')
|
await page.click('text=Save in variables')
|
||||||
await page.click('text=Add an entry >> nth=-1')
|
await page.click('text=Add an entry >> nth=-1')
|
||||||
await page.click('input[placeholder="Select the data"]')
|
await page.click('input[placeholder="Select the data"]')
|
||||||
await page.click('text=data[0].name')
|
await page.click('text=data.map(item => item.name)')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -100,8 +100,7 @@ export const BlockNode = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (query.blockId?.toString() === block.id) setOpenedBlockId(block.id)
|
if (query.blockId?.toString() === block.id) setOpenedBlockId(block.id)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [block.id, query, setOpenedBlockId])
|
||||||
}, [query])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsConnecting(
|
setIsConnecting(
|
||||||
|
|||||||
@@ -33,20 +33,18 @@ import { GoogleSheetsNodeContent } from '@/features/blocks/integrations/googleSh
|
|||||||
import { GoogleAnalyticsNodeContent } from '@/features/blocks/integrations/googleAnalytics/components/GoogleAnalyticsNodeContent'
|
import { GoogleAnalyticsNodeContent } from '@/features/blocks/integrations/googleAnalytics/components/GoogleAnalyticsNodeContent'
|
||||||
import { ZapierContent } from '@/features/blocks/integrations/zapier'
|
import { ZapierContent } from '@/features/blocks/integrations/zapier'
|
||||||
import { SendEmailContent } from '@/features/blocks/integrations/sendEmail'
|
import { SendEmailContent } from '@/features/blocks/integrations/sendEmail'
|
||||||
import { isInputBlock, isChoiceInput, blockHasItems } from 'utils'
|
import { isInputBlock, isChoiceInput } from 'utils'
|
||||||
import { MakeComContent } from '@/features/blocks/integrations/makeCom'
|
import { MakeComContent } from '@/features/blocks/integrations/makeCom'
|
||||||
import { AudioBubbleNode } from '@/features/blocks/bubbles/audio'
|
import { AudioBubbleNode } from '@/features/blocks/bubbles/audio'
|
||||||
import { WaitNodeContent } from '@/features/blocks/logic/wait/components/WaitNodeContent'
|
import { WaitNodeContent } from '@/features/blocks/logic/wait/components/WaitNodeContent'
|
||||||
import { ScriptNodeContent } from '@/features/blocks/logic/script/components/ScriptNodeContent'
|
import { ScriptNodeContent } from '@/features/blocks/logic/script/components/ScriptNodeContent'
|
||||||
|
import { ButtonsBlockNode } from '@/features/blocks/inputs/buttons/components/ButtonsBlockNode'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: Block | StartBlock
|
block: Block | StartBlock
|
||||||
indices: BlockIndices
|
indices: BlockIndices
|
||||||
}
|
}
|
||||||
export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
||||||
if (blockHasItems(block))
|
|
||||||
return <ItemNodesList block={block} indices={indices} />
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isInputBlock(block) &&
|
isInputBlock(block) &&
|
||||||
!isChoiceInput(block) &&
|
!isChoiceInput(block) &&
|
||||||
@@ -92,6 +90,9 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
|||||||
case InputBlockType.URL: {
|
case InputBlockType.URL: {
|
||||||
return <UrlNodeContent placeholder={block.options.labels.placeholder} />
|
return <UrlNodeContent placeholder={block.options.labels.placeholder} />
|
||||||
}
|
}
|
||||||
|
case InputBlockType.CHOICE: {
|
||||||
|
return <ButtonsBlockNode block={block} indices={indices} />
|
||||||
|
}
|
||||||
case InputBlockType.PHONE: {
|
case InputBlockType.PHONE: {
|
||||||
return <PhoneNodeContent placeholder={block.options.labels.placeholder} />
|
return <PhoneNodeContent placeholder={block.options.labels.placeholder} />
|
||||||
}
|
}
|
||||||
@@ -126,7 +127,8 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
|||||||
}
|
}
|
||||||
case LogicBlockType.TYPEBOT_LINK:
|
case LogicBlockType.TYPEBOT_LINK:
|
||||||
return <TypebotLinkNode block={block} />
|
return <TypebotLinkNode block={block} />
|
||||||
|
case LogicBlockType.CONDITION:
|
||||||
|
return <ItemNodesList block={block} indices={indices} />
|
||||||
case IntegrationBlockType.GOOGLE_SHEETS: {
|
case IntegrationBlockType.GOOGLE_SHEETS: {
|
||||||
return (
|
return (
|
||||||
<GoogleSheetsNodeContent
|
<GoogleSheetsNodeContent
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Flex,
|
Flex,
|
||||||
HStack,
|
|
||||||
Portal,
|
Portal,
|
||||||
Stack,
|
Stack,
|
||||||
Tag,
|
|
||||||
Text,
|
Text,
|
||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
useEventListener,
|
useEventListener,
|
||||||
@@ -15,13 +13,7 @@ import {
|
|||||||
useGraph,
|
useGraph,
|
||||||
} from '../../../providers'
|
} from '../../../providers'
|
||||||
import { useTypebot } from '@/features/editor'
|
import { useTypebot } from '@/features/editor'
|
||||||
import {
|
import { BlockIndices, BlockWithItems, LogicBlockType, Item } from 'models'
|
||||||
BlockIndices,
|
|
||||||
BlockWithItems,
|
|
||||||
LogicBlockType,
|
|
||||||
Item,
|
|
||||||
Variable,
|
|
||||||
} from 'models'
|
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { ItemNode } from './ItemNode'
|
import { ItemNode } from './ItemNode'
|
||||||
import { SourceEndpoint } from '../../Endpoints'
|
import { SourceEndpoint } from '../../Endpoints'
|
||||||
@@ -137,17 +129,8 @@ export const ItemNodesList = ({
|
|||||||
elem && (placeholderRefs.current[idx] = elem)
|
elem && (placeholderRefs.current[idx] = elem)
|
||||||
}
|
}
|
||||||
|
|
||||||
const collectedVariableId =
|
|
||||||
'options' in block && block.options && block.options.variableId
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack flex={1} spacing={1} maxW="full" onClick={stopPropagating}>
|
<Stack flex={1} spacing={1} maxW="full" onClick={stopPropagating}>
|
||||||
{collectedVariableId && (
|
|
||||||
<CollectVariableLabel
|
|
||||||
variableId={collectedVariableId}
|
|
||||||
variables={typebot?.variables ?? []}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<PlaceholderNode
|
<PlaceholderNode
|
||||||
isVisible={showPlaceholders}
|
isVisible={showPlaceholders}
|
||||||
isExpanded={expandedPlaceholderIndex === 0}
|
isExpanded={expandedPlaceholderIndex === 0}
|
||||||
@@ -221,28 +204,3 @@ const DefaultItemNode = ({ block }: { block: BlockWithItems }) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CollectVariableLabel = ({
|
|
||||||
variableId,
|
|
||||||
variables,
|
|
||||||
}: {
|
|
||||||
variableId: string
|
|
||||||
variables: Variable[]
|
|
||||||
}) => {
|
|
||||||
const textColor = useColorModeValue('gray.600', 'gray.400')
|
|
||||||
const variableName = variables.find(
|
|
||||||
(variable) => variable.id === variableId
|
|
||||||
)?.name
|
|
||||||
|
|
||||||
if (!variableName) return null
|
|
||||||
return (
|
|
||||||
<HStack fontStyle="italic" spacing={1}>
|
|
||||||
<Text fontSize="sm" color={textColor}>
|
|
||||||
Collects
|
|
||||||
</Text>
|
|
||||||
<Tag bg="orange.400" color="white" size="sm">
|
|
||||||
{variableName}
|
|
||||||
</Tag>
|
|
||||||
</HStack>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1152,7 +1152,17 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"value": {
|
"value": {
|
||||||
"type": "string"
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
@@ -1092,6 +1092,9 @@
|
|||||||
},
|
},
|
||||||
"buttonLabel": {
|
"buttonLabel": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"dynamicVariableId": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -2910,7 +2913,17 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"value": {
|
"value": {
|
||||||
"type": "string",
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"nullable": true
|
"nullable": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -4100,6 +4113,9 @@
|
|||||||
},
|
},
|
||||||
"buttonLabel": {
|
"buttonLabel": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"dynamicVariableId": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -4522,15 +4538,25 @@
|
|||||||
{
|
{
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"type": "string"
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "number"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean"
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"nullable": true
|
"nullable": true
|
||||||
@@ -4606,15 +4632,25 @@
|
|||||||
{
|
{
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"type": "string"
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "number"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean"
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { deepParseVariable } from '@/features/variables/utils'
|
||||||
|
import {
|
||||||
|
SessionState,
|
||||||
|
VariableWithValue,
|
||||||
|
ChoiceInputBlock,
|
||||||
|
ItemType,
|
||||||
|
} from 'models'
|
||||||
|
import { isDefined } from 'utils'
|
||||||
|
|
||||||
|
export const injectVariableValuesInButtonsInputBlock =
|
||||||
|
(variables: SessionState['typebot']['variables']) =>
|
||||||
|
(block: ChoiceInputBlock): ChoiceInputBlock => {
|
||||||
|
if (block.options.dynamicVariableId) {
|
||||||
|
const variable = variables.find(
|
||||||
|
(variable) =>
|
||||||
|
variable.id === block.options.dynamicVariableId &&
|
||||||
|
isDefined(variable.value)
|
||||||
|
) as VariableWithValue | undefined
|
||||||
|
if (!variable || typeof variable.value === 'string') return block
|
||||||
|
return {
|
||||||
|
...block,
|
||||||
|
items: variable.value.map((item, idx) => ({
|
||||||
|
id: idx.toString(),
|
||||||
|
type: ItemType.BUTTON,
|
||||||
|
blockId: block.id,
|
||||||
|
content: item,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deepParseVariable(variables)(block)
|
||||||
|
}
|
||||||
@@ -11,11 +11,12 @@ import Stripe from 'stripe'
|
|||||||
import { decrypt } from 'utils/api/encryption'
|
import { decrypt } from 'utils/api/encryption'
|
||||||
|
|
||||||
export const computePaymentInputRuntimeOptions =
|
export const computePaymentInputRuntimeOptions =
|
||||||
(state: SessionState) => (options: PaymentInputOptions) =>
|
(state: Pick<SessionState, 'isPreview' | 'typebot'>) =>
|
||||||
|
(options: PaymentInputOptions) =>
|
||||||
createStripePaymentIntent(state)(options)
|
createStripePaymentIntent(state)(options)
|
||||||
|
|
||||||
const createStripePaymentIntent =
|
const createStripePaymentIntent =
|
||||||
(state: SessionState) =>
|
(state: Pick<SessionState, 'isPreview' | 'typebot'>) =>
|
||||||
async (options: PaymentInputOptions): Promise<PaymentInputRuntimeOptions> => {
|
async (options: PaymentInputOptions): Promise<PaymentInputRuntimeOptions> => {
|
||||||
const {
|
const {
|
||||||
isPreview,
|
isPreview,
|
||||||
|
|||||||
@@ -53,24 +53,21 @@ export const getRow = async (
|
|||||||
})
|
})
|
||||||
return { outgoingEdgeId, logs: log ? [log] : undefined }
|
return { outgoingEdgeId, logs: log ? [log] : undefined }
|
||||||
}
|
}
|
||||||
const randomIndex = Math.floor(Math.random() * filteredRows.length)
|
|
||||||
const extractingColumns = cellsToExtract
|
const extractingColumns = cellsToExtract
|
||||||
.map((cell) => cell.column)
|
.map((cell) => cell.column)
|
||||||
.filter(isNotEmpty)
|
.filter(isNotEmpty)
|
||||||
const selectedRow = filteredRows
|
const selectedRows = filteredRows.map((row) =>
|
||||||
.map((row) =>
|
extractingColumns.reduce<{ [key: string]: string }>(
|
||||||
extractingColumns.reduce<{ [key: string]: string }>(
|
(obj, column) => ({ ...obj, [column]: row[column] }),
|
||||||
(obj, column) => ({ ...obj, [column]: row[column] }),
|
{}
|
||||||
{}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.at(randomIndex)
|
)
|
||||||
if (!selectedRow) return { outgoingEdgeId }
|
if (!selectedRows) return { outgoingEdgeId }
|
||||||
|
|
||||||
const newVariables = options.cellsToExtract.reduce<VariableWithValue[]>(
|
const newVariables = options.cellsToExtract.reduce<VariableWithValue[]>(
|
||||||
(newVariables, cell) => {
|
(newVariables, cell) => {
|
||||||
const existingVariable = variables.find(byId(cell.variableId))
|
const existingVariable = variables.find(byId(cell.variableId))
|
||||||
const value = selectedRow[cell.column ?? ''] ?? null
|
const value = selectedRows.map((row) => row[cell.column ?? ''])
|
||||||
if (!existingVariable) return newVariables
|
if (!existingVariable) return newVariables
|
||||||
return [
|
return [
|
||||||
...newVariables,
|
...newVariables,
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const sendEmail = async ({
|
|||||||
}: SendEmailOptions & {
|
}: SendEmailOptions & {
|
||||||
typebotId: string
|
typebotId: string
|
||||||
resultId?: string
|
resultId?: string
|
||||||
fileUrls?: string
|
fileUrls?: string | string[]
|
||||||
}) => {
|
}) => {
|
||||||
const { name: replyToName } = parseEmailRecipient(replyTo)
|
const { name: replyToName } = parseEmailRecipient(replyTo)
|
||||||
|
|
||||||
@@ -121,7 +121,11 @@ const sendEmail = async ({
|
|||||||
to: recipients,
|
to: recipients,
|
||||||
replyTo,
|
replyTo,
|
||||||
subject,
|
subject,
|
||||||
attachments: fileUrls?.split(', ').map((url) => ({ path: url })),
|
attachments: fileUrls
|
||||||
|
? (typeof fileUrls === 'string' ? fileUrls.split(', ') : fileUrls).map(
|
||||||
|
(url) => ({ path: url })
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
...emailBody,
|
...emailBody,
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ export const executeWebhookBlock = async (
|
|||||||
return { outgoingEdgeId: block.outgoingEdgeId, logs: [log] }
|
return { outgoingEdgeId: block.outgoingEdgeId, logs: [log] }
|
||||||
}
|
}
|
||||||
const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
|
const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
|
||||||
const resultValues = result && (await getResultValues(result.id))
|
const resultValues =
|
||||||
if (!resultValues) return { outgoingEdgeId: block.outgoingEdgeId }
|
(result && (await getResultValues(result.id))) ?? undefined
|
||||||
const webhookResponse = await executeWebhook({ typebot })(
|
const webhookResponse = await executeWebhook({ typebot })(
|
||||||
preparedWebhook,
|
preparedWebhook,
|
||||||
typebot.variables,
|
typebot.variables,
|
||||||
@@ -139,8 +139,8 @@ export const executeWebhook =
|
|||||||
webhook: Webhook,
|
webhook: Webhook,
|
||||||
variables: Variable[],
|
variables: Variable[],
|
||||||
groupId: string,
|
groupId: string,
|
||||||
resultValues: ResultValues,
|
resultValues?: ResultValues,
|
||||||
resultId: string
|
resultId?: string
|
||||||
): Promise<WebhookResponse> => {
|
): Promise<WebhookResponse> => {
|
||||||
if (!webhook.url || !webhook.method)
|
if (!webhook.url || !webhook.method)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ const parseResultSample = (
|
|||||||
headerCells: ResultHeaderCell[],
|
headerCells: ResultHeaderCell[],
|
||||||
variables: Variable[]
|
variables: Variable[]
|
||||||
) =>
|
) =>
|
||||||
headerCells.reduce<Record<string, string | boolean | undefined>>(
|
headerCells.reduce<Record<string, string | string[] | undefined>>(
|
||||||
(resultSample, cell) => {
|
(resultSample, cell) => {
|
||||||
const inputBlock = inputBlocks.find((inputBlock) =>
|
const inputBlock = inputBlocks.find((inputBlock) =>
|
||||||
cell.blocks?.some((block) => block.id === inputBlock.id)
|
cell.blocks?.some((block) => block.id === inputBlock.id)
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ const executeComparison =
|
|||||||
if (!comparison?.variableId) return false
|
if (!comparison?.variableId) return false
|
||||||
const inputValue = (
|
const inputValue = (
|
||||||
variables.find((v) => v.id === comparison.variableId)?.value ?? ''
|
variables.find((v) => v.id === comparison.variableId)?.value ?? ''
|
||||||
).trim()
|
)
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
const value = parseVariables(variables)(comparison.value).trim()
|
const value = parseVariables(variables)(comparison.value).trim()
|
||||||
if (isNotDefined(value)) return false
|
if (isNotDefined(value)) return false
|
||||||
switch (comparison.comparisonOperator) {
|
switch (comparison.comparisonOperator) {
|
||||||
|
|||||||
@@ -232,7 +232,8 @@ export const isReplyValid = (inputValue: string, block: Block): boolean => {
|
|||||||
case InputBlockType.URL:
|
case InputBlockType.URL:
|
||||||
return validateUrl(inputValue)
|
return validateUrl(inputValue)
|
||||||
case InputBlockType.CHOICE:
|
case InputBlockType.CHOICE:
|
||||||
if (block.options.isMultipleChoice) return true
|
if (block.options.isMultipleChoice || block.options.dynamicVariableId)
|
||||||
|
return true
|
||||||
return validateButtonInput(block, inputValue)
|
return validateButtonInput(block, inputValue)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { executeLogic } from './executeLogic'
|
|||||||
import { getNextGroup } from './getNextGroup'
|
import { getNextGroup } from './getNextGroup'
|
||||||
import { executeIntegration } from './executeIntegration'
|
import { executeIntegration } from './executeIntegration'
|
||||||
import { computePaymentInputRuntimeOptions } from '@/features/blocks/inputs/payment/api'
|
import { computePaymentInputRuntimeOptions } from '@/features/blocks/inputs/payment/api'
|
||||||
|
import { injectVariableValuesInButtonsInputBlock } from '@/features/blocks/inputs/buttons/api/utils/injectVariableValuesInButtonsInputBlock'
|
||||||
|
|
||||||
export const executeGroup =
|
export const executeGroup =
|
||||||
(state: SessionState, currentReply?: ChatReply) =>
|
(state: SessionState, currentReply?: ChatReply) =>
|
||||||
@@ -49,13 +50,7 @@ export const executeGroup =
|
|||||||
if (isInputBlock(block))
|
if (isInputBlock(block))
|
||||||
return {
|
return {
|
||||||
messages,
|
messages,
|
||||||
input: deepParseVariable(newSessionState.typebot.variables)({
|
input: await injectVariablesValueInBlock(newSessionState)(block),
|
||||||
...block,
|
|
||||||
runtimeOptions: await computeRuntimeOptions(newSessionState)(block),
|
|
||||||
prefilledValue: getPrefilledInputValue(
|
|
||||||
newSessionState.typebot.variables
|
|
||||||
)(block),
|
|
||||||
}),
|
|
||||||
newSessionState: {
|
newSessionState: {
|
||||||
...newSessionState,
|
...newSessionState,
|
||||||
currentBlock: {
|
currentBlock: {
|
||||||
@@ -110,7 +105,7 @@ export const executeGroup =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const computeRuntimeOptions =
|
const computeRuntimeOptions =
|
||||||
(state: SessionState) =>
|
(state: Pick<SessionState, 'isPreview' | 'typebot'>) =>
|
||||||
(block: InputBlock): Promise<RuntimeOptions> | undefined => {
|
(block: InputBlock): Promise<RuntimeOptions> | undefined => {
|
||||||
switch (block.type) {
|
switch (block.type) {
|
||||||
case InputBlockType.PAYMENT: {
|
case InputBlockType.PAYMENT: {
|
||||||
@@ -122,10 +117,13 @@ const computeRuntimeOptions =
|
|||||||
const getPrefilledInputValue =
|
const getPrefilledInputValue =
|
||||||
(variables: SessionState['typebot']['variables']) => (block: InputBlock) => {
|
(variables: SessionState['typebot']['variables']) => (block: InputBlock) => {
|
||||||
return (
|
return (
|
||||||
variables.find(
|
variables
|
||||||
(variable) =>
|
.find(
|
||||||
variable.id === block.options.variableId && isDefined(variable.value)
|
(variable) =>
|
||||||
)?.value ?? undefined
|
variable.id === block.options.variableId &&
|
||||||
|
isDefined(variable.value)
|
||||||
|
)
|
||||||
|
?.value?.toString() ?? undefined
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,3 +148,24 @@ const parseBubbleBlock =
|
|||||||
return deepParseVariable(variables)(block)
|
return deepParseVariable(variables)(block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const injectVariablesValueInBlock =
|
||||||
|
(state: Pick<SessionState, 'isPreview' | 'typebot'>) =>
|
||||||
|
async (block: InputBlock): Promise<ChatReply['input']> => {
|
||||||
|
switch (block.type) {
|
||||||
|
case InputBlockType.CHOICE: {
|
||||||
|
return injectVariableValuesInButtonsInputBlock(state.typebot.variables)(
|
||||||
|
block
|
||||||
|
)
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return deepParseVariable(state.typebot.variables)({
|
||||||
|
...block,
|
||||||
|
runtimeOptions: await computeRuntimeOptions(state)(block),
|
||||||
|
prefilledValue: getPrefilledInputValue(state.typebot.variables)(
|
||||||
|
block
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ export const parseVariables =
|
|||||||
if (!variable) return ''
|
if (!variable) return ''
|
||||||
if (options.fieldToParse === 'id') return variable.id
|
if (options.fieldToParse === 'id') return variable.id
|
||||||
const { value } = variable
|
const { value } = variable
|
||||||
if (options.escapeForJson) return jsonParse(value)
|
if (options.escapeForJson)
|
||||||
|
return jsonParse(
|
||||||
|
typeof value !== 'string' ? JSON.stringify(value) : value
|
||||||
|
)
|
||||||
const parsedValue = safeStringify(value)
|
const parsedValue = safeStringify(value)
|
||||||
if (!parsedValue) return ''
|
if (!parsedValue) return ''
|
||||||
return parsedValue
|
return parsedValue
|
||||||
@@ -67,9 +70,10 @@ export const safeStringify = (val: unknown): string | null => {
|
|||||||
|
|
||||||
export const parseCorrectValueType = (
|
export const parseCorrectValueType = (
|
||||||
value: Variable['value']
|
value: Variable['value']
|
||||||
): string | boolean | number | null | undefined => {
|
): string | string[] | boolean | number | null | undefined => {
|
||||||
if (value === null) return null
|
if (value === null) return null
|
||||||
if (value === undefined) return undefined
|
if (value === undefined) return undefined
|
||||||
|
if (typeof value !== 'string') return value
|
||||||
const isNumberStartingWithZero =
|
const isNumberStartingWithZero =
|
||||||
value.startsWith('0') && !value.startsWith('0.') && value.length > 1
|
value.startsWith('0') && !value.startsWith('0.') && value.length > 1
|
||||||
if (typeof value === 'string' && isNumberStartingWithZero) return value
|
if (typeof value === 'string' && isNumberStartingWithZero) return value
|
||||||
@@ -152,7 +156,9 @@ const updateResultVariables =
|
|||||||
if (!result) return []
|
if (!result) return []
|
||||||
const serializedNewVariables = newVariables.map((variable) => ({
|
const serializedNewVariables = newVariables.map((variable) => ({
|
||||||
...variable,
|
...variable,
|
||||||
value: safeStringify(variable.value),
|
value: Array.isArray(variable.value)
|
||||||
|
? variable.value.map(safeStringify).filter(isDefined)
|
||||||
|
: safeStringify(variable.value),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const updatedVariables = [
|
const updatedVariables = [
|
||||||
@@ -181,7 +187,9 @@ const updateTypebotVariables =
|
|||||||
(newVariables: VariableWithUnknowValue[]): Variable[] => {
|
(newVariables: VariableWithUnknowValue[]): Variable[] => {
|
||||||
const serializedNewVariables = newVariables.map((variable) => ({
|
const serializedNewVariables = newVariables.map((variable) => ({
|
||||||
...variable,
|
...variable,
|
||||||
value: safeStringify(variable.value),
|
value: Array.isArray(variable.value)
|
||||||
|
? variable.value.map(safeStringify).filter(isDefined)
|
||||||
|
: safeStringify(variable.value),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ const executeComparison =
|
|||||||
if (!comparison?.variableId) return false
|
if (!comparison?.variableId) return false
|
||||||
const inputValue = (
|
const inputValue = (
|
||||||
variables.find((v) => v.id === comparison.variableId)?.value ?? ''
|
variables.find((v) => v.id === comparison.variableId)?.value ?? ''
|
||||||
).trim()
|
)
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
const value = parseVariables(variables)(comparison.value).trim()
|
const value = parseVariables(variables)(comparison.value).trim()
|
||||||
if (isNotDefined(value) || !comparison.comparisonOperator) return false
|
if (isNotDefined(value) || !comparison.comparisonOperator) return false
|
||||||
return matchComparison(inputValue, comparison.comparisonOperator, value)
|
return matchComparison(inputValue, comparison.comparisonOperator, value)
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ export const parseVariables =
|
|||||||
if (!variable) return ''
|
if (!variable) return ''
|
||||||
if (options.fieldToParse === 'id') return variable.id
|
if (options.fieldToParse === 'id') return variable.id
|
||||||
const { value } = variable
|
const { value } = variable
|
||||||
if (options.escapeForJson) return jsonParse(value)
|
if (options.escapeForJson)
|
||||||
|
return typeof value === 'string'
|
||||||
|
? jsonParse(value)
|
||||||
|
: jsonParse(JSON.stringify(value))
|
||||||
const parsedValue = safeStringify(value)
|
const parsedValue = safeStringify(value)
|
||||||
if (!parsedValue) return ''
|
if (!parsedValue) return ''
|
||||||
return parsedValue
|
return parsedValue
|
||||||
@@ -45,9 +48,10 @@ export const safeStringify = (val: unknown): string | null => {
|
|||||||
|
|
||||||
export const parseCorrectValueType = (
|
export const parseCorrectValueType = (
|
||||||
value: Variable['value']
|
value: Variable['value']
|
||||||
): string | boolean | number | null | undefined => {
|
): string | string[] | boolean | number | null | undefined => {
|
||||||
if (value === null) return null
|
if (value === null) return null
|
||||||
if (value === undefined) return undefined
|
if (value === undefined) return undefined
|
||||||
|
if (Array.isArray(value)) return value
|
||||||
if (typeof value === 'number') return value
|
if (typeof value === 'number') return value
|
||||||
if (value === 'true') return true
|
if (value === 'true') return true
|
||||||
if (value === 'false') return false
|
if (value === 'false') return false
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const choiceInputOptionsSchema = optionBaseSchema.and(
|
|||||||
z.object({
|
z.object({
|
||||||
isMultipleChoice: z.boolean(),
|
isMultipleChoice: z.boolean(),
|
||||||
buttonLabel: z.string(),
|
buttonLabel: z.string(),
|
||||||
|
dynamicVariableId: z.string().optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,12 @@ const scriptToExecuteSchema = z.object({
|
|||||||
args: z.array(
|
args: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
value: z.string().or(z.number()).or(z.boolean()).nullish(),
|
value: z
|
||||||
|
.string()
|
||||||
|
.or(z.number())
|
||||||
|
.or(z.boolean())
|
||||||
|
.or(z.array(z.string()))
|
||||||
|
.nullish(),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { z } from 'zod'
|
|||||||
export const variableSchema = z.object({
|
export const variableSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
value: z.string().nullish(),
|
value: z.string().or(z.array(z.string())).nullish(),
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -12,7 +12,7 @@ export const variableSchema = z.object({
|
|||||||
export const variableWithValueSchema = z.object({
|
export const variableWithValueSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
value: z.string(),
|
value: z.string().or(z.array(z.string())),
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ export const parseAnswers =
|
|||||||
if (isVariable) {
|
if (isVariable) {
|
||||||
const variable = answerOrVariable as VariableWithValue
|
const variable = answerOrVariable as VariableWithValue
|
||||||
if (variable.value === null) return o
|
if (variable.value === null) return o
|
||||||
return { ...o, [variable.name]: variable.value }
|
return { ...o, [variable.name]: variable.value.toString() }
|
||||||
}
|
}
|
||||||
const answer = answerOrVariable as Answer
|
const answer = answerOrVariable as Answer
|
||||||
const key = answer.variableId
|
const key = answer.variableId
|
||||||
|
|||||||
Reference in New Issue
Block a user