2
0

Add new Jump block

Also improve Select input with a clear button

Closes #186
This commit is contained in:
Baptiste Arnaud
2023-03-03 15:03:31 +01:00
parent f1a9a1ce8b
commit 022c5a5738
32 changed files with 598 additions and 242 deletions

View File

@ -545,3 +545,10 @@ export const PackageIcon = (props: IconProps) => (
<line x1="12" y1="22.08" x2="12" y2="12"></line>
</Icon>
)
export const CloseIcon = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</Icon>
)

View File

@ -12,14 +12,16 @@ import {
InputRightElement,
Text,
Box,
IconButton,
HStack,
} from '@chakra-ui/react'
import { useState, useRef, ChangeEvent } from 'react'
import { isDefined } from 'utils'
import { useOutsideClick } from '@/hooks/useOutsideClick'
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
import { ChevronDownIcon } from '../icons'
import { ChevronDownIcon, CloseIcon } from '../icons'
const dropdownCloseAnimationDuration = 200
const dropdownCloseAnimationDuration = 300
type Item = string | { icon?: JSX.Element; label: string; value: string }
@ -27,7 +29,7 @@ type Props = {
selectedItem?: string
items: Item[]
placeholder?: string
onSelect?: (value: string) => void
onSelect?: (value: string | undefined) => void
}
export const Select = ({
@ -119,6 +121,14 @@ export const Select = ({
}
}
const clearSelection = (e: React.MouseEvent) => {
e.preventDefault()
setInputValue('')
onSelect?.(undefined)
setKeyboardFocusIndex(undefined)
closeDropwdown()
}
const resetIsTouched = () => {
setTimeout(() => {
setIsTouched(false)
@ -136,7 +146,15 @@ export const Select = ({
>
<PopoverAnchor>
<InputGroup>
<Box pos="absolute" py={2} pl={4} pr={6}>
<Box
pos="absolute"
pb={2}
// We need absolute positioning the overlay match the underlying input
pt="8.5px"
pl="17px"
pr={selectedItem ? 16 : 8}
w="full"
>
{!isTouched && (
<Text noOfLines={1} data-testid="selected-item-label">
{inputValue}
@ -156,10 +174,26 @@ export const Select = ({
onChange={handleInputChange}
onFocus={onOpen}
onKeyDown={handleKeyUp}
pr={selectedItem ? 16 : undefined}
/>
<InputRightElement pointerEvents="none" cursor="pointer">
<ChevronDownIcon />
<InputRightElement
width={selectedItem ? '5rem' : undefined}
pointerEvents="none"
>
<HStack>
{selectedItem && (
<IconButton
onClick={clearSelection}
icon={<CloseIcon />}
aria-label={'Clear'}
size="sm"
variant="ghost"
pointerEvents="all"
/>
)}
<ChevronDownIcon />
</HStack>
</InputRightElement>
</InputGroup>
</PopoverAnchor>

View File

@ -51,9 +51,9 @@ export const GoogleSheetsSettingsBody = ({
)
const handleCredentialsIdChange = (credentialsId?: string) =>
onOptionsChange({ ...omit(options, 'credentialsId'), credentialsId })
const handleSpreadsheetIdChange = (spreadsheetId: string) =>
const handleSpreadsheetIdChange = (spreadsheetId: string | undefined) =>
onOptionsChange({ ...options, spreadsheetId })
const handleSheetIdChange = (sheetId: string) =>
const handleSheetIdChange = (sheetId: string | undefined) =>
onOptionsChange({ ...options, sheetId })
const handleActionChange = (action: GoogleSheetsAction) => {

View File

@ -7,7 +7,7 @@ type Props = {
sheets: Sheet[]
isLoading: boolean
sheetId?: string
onSelectSheetId: (id: string) => void
onSelectSheetId: (id: string | undefined) => void
}
export const SheetsDropdown = ({

View File

@ -5,7 +5,7 @@ import { useSpreadsheets } from '../../hooks/useSpreadsheets'
type Props = {
credentialsId: string
spreadsheetId?: string
onSelectSpreadsheetId: (id: string) => void
onSelectSpreadsheetId: (id: string | undefined) => void
}
export const SpreadsheetsDropdown = ({

View File

@ -0,0 +1,10 @@
import { featherIconsBaseProps } from '@/components/icons'
import { Icon, IconProps } from '@chakra-ui/react'
import React from 'react'
export const JumpIcon = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
<polygon points="13 19 22 12 13 5 13 19"></polygon>
<polygon points="2 19 11 12 2 5 2 19"></polygon>
</Icon>
)

View File

@ -0,0 +1,26 @@
import React from 'react'
import { Tag, Text } from '@chakra-ui/react'
import { useTypebot } from '@/features/editor'
import { byId, isDefined } from 'utils'
import { JumpBlock } from 'models/features/blocks/logic/jump'
type Props = {
options: JumpBlock['options']
}
export const JumpNodeBody = ({ options }: Props) => {
const { typebot } = useTypebot()
const selectedGroup = typebot?.groups.find(byId(options.groupId))
const blockIndex = selectedGroup?.blocks.findIndex(byId(options.blockId))
if (!selectedGroup) return <Text color="gray.500">Configure...</Text>
return (
<Text>
Jump to <Tag colorScheme="blue">{selectedGroup.title}</Tag>{' '}
{isDefined(blockIndex) && blockIndex >= 0 ? (
<>
at block <Tag colorScheme="blue">{blockIndex + 1}</Tag>
</>
) : null}
</Text>
)
}

View File

@ -0,0 +1,55 @@
import { Select } from '@/components/inputs/Select'
import { useTypebot } from '@/features/editor'
import { Stack } from '@chakra-ui/react'
import { JumpBlock } from 'models/features/blocks/logic/jump'
import React from 'react'
import { byId } from 'utils'
type Props = {
groupId: string
options: JumpBlock['options']
onOptionsChange: (options: JumpBlock['options']) => void
}
export const JumpSettings = ({ groupId, options, onOptionsChange }: Props) => {
const { typebot } = useTypebot()
const handleGroupIdChange = (groupId?: string) =>
onOptionsChange({ ...options, groupId })
const handleBlockIdChange = (blockId?: string) =>
onOptionsChange({ ...options, blockId })
const currentGroupId = typebot?.groups.find(byId(groupId))?.id
const selectedGroup = typebot?.groups.find(byId(options.groupId))
if (!typebot) return null
return (
<Stack spacing={4}>
<Select
items={typebot.groups
.filter((group) => group.id !== currentGroupId)
.map((group) => ({
label: group.title,
value: group.id,
}))}
selectedItem={selectedGroup?.id}
onSelect={handleGroupIdChange}
placeholder="Select a group"
/>
{selectedGroup && selectedGroup.blocks.length > 1 && (
<Select
selectedItem={options.blockId}
items={selectedGroup.blocks.map((block, index) => ({
label: `Block #${(index + 1).toString()}`,
value: block.id,
}))}
onSelect={handleBlockIdChange}
placeholder="Select a block"
/>
)}
</Stack>
)
}

View File

@ -0,0 +1,28 @@
import test, { expect } from '@playwright/test'
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
import { createId } from '@paralleldrive/cuid2'
import { getTestAsset } from '@/test/utils/playwright'
test('should work as expected', async ({ page }) => {
const typebotId = createId()
await importTypebotInDatabase(getTestAsset('typebots/logic/jump.json'), {
id: typebotId,
})
await page.goto(`/typebots/${typebotId}/edit`)
await page.getByText('Configure...').click()
await page.getByPlaceholder('Select a group').click()
await expect(page.getByRole('menuitem', { name: 'Group #2' })).toBeHidden()
await page.getByRole('menuitem', { name: 'Group #1' }).click()
await page.getByPlaceholder('Select a block').click()
await page.getByRole('menuitem', { name: 'Block #2' }).click()
await page.getByRole('button', { name: 'Preview' }).click()
await page.getByPlaceholder('Type your answer...').fill('Hi there!')
await page.getByRole('button', { name: 'Send' }).click()
await expect(
page.locator('typebot-standard').getByText('How are you?').nth(1)
).toBeVisible()
await expect(
page.locator('typebot-standard').getByText('Hello this is a test!').nth(1)
).toBeHidden()
})

View File

@ -5,7 +5,7 @@ import { Group } from 'models'
type Props = {
groups: Group[]
groupId?: string
onGroupIdSelected: (groupId: string) => void
onGroupIdSelected: (groupId: string | undefined) => void
isLoading?: boolean
}

View File

@ -13,9 +13,9 @@ type Props = {
export const TypebotLinkForm = ({ options, onOptionsChange }: Props) => {
const { linkedTypebots, typebot } = useTypebot()
const handleTypebotIdChange = (typebotId: string | 'current') =>
const handleTypebotIdChange = (typebotId: string | 'current' | undefined) =>
onOptionsChange({ ...options, typebotId })
const handleGroupIdChange = (groupId: string) =>
const handleGroupIdChange = (groupId: string | undefined) =>
onOptionsChange({ ...options, groupId })
return (

View File

@ -11,7 +11,7 @@ type Props = {
idsToExclude: string[]
typebotId?: string | 'current'
currentWorkspaceId: string
onSelect: (typebotId: string | 'current') => void
onSelect: (typebotId: string | 'current' | undefined) => void
}
export const TypebotsDropdown = ({

View File

@ -1,19 +1,102 @@
import { Flex, HStack, Tooltip, useColorModeValue } from '@chakra-ui/react'
import { DraggableBlockType } from 'models'
import {
BubbleBlockType,
DraggableBlockType,
InputBlockType,
IntegrationBlockType,
LogicBlockType,
} from 'models'
import { useBlockDnd } from '@/features/graph'
import React, { useEffect, useState } from 'react'
import { BlockIcon } from './BlockIcon'
import { BlockTypeLabel } from './BlockTypeLabel'
import { isFreePlan, LockTag } from '@/features/billing'
import { Plan } from 'db'
import { useWorkspace } from '@/features/workspace'
import { BlockLabel } from './BlockLabel'
export const BlockCard = ({
type,
onMouseDown,
isDisabled = false,
}: {
type Props = {
type: DraggableBlockType
tooltip?: string
isDisabled?: boolean
children: React.ReactNode
onMouseDown: (e: React.MouseEvent, type: DraggableBlockType) => void
}) => {
}
export const BlockCard = (
props: Pick<Props, 'type' | 'onMouseDown'>
): JSX.Element => {
const { workspace } = useWorkspace()
switch (props.type) {
case BubbleBlockType.EMBED:
return (
<BlockCardLayout
{...props}
tooltip="Embed a pdf, an iframe, a website..."
>
<BlockIcon type={props.type} />
<BlockLabel type={props.type} />
</BlockCardLayout>
)
case InputBlockType.FILE:
return (
<BlockCardLayout {...props} tooltip="Upload Files">
<BlockIcon type={props.type} />
<HStack>
<BlockLabel type={props.type} />
{isFreePlan(workspace) && <LockTag plan={Plan.STARTER} />}
</HStack>
</BlockCardLayout>
)
case LogicBlockType.SCRIPT:
return (
<BlockCardLayout {...props} tooltip="Execute Javascript code">
<BlockIcon type={props.type} />
<BlockLabel type={props.type} />
</BlockCardLayout>
)
case LogicBlockType.TYPEBOT_LINK:
return (
<BlockCardLayout {...props} tooltip="Link and jump to another typebot">
<BlockIcon type={props.type} />
<BlockLabel type={props.type} />
</BlockCardLayout>
)
case LogicBlockType.JUMP:
return (
<BlockCardLayout
{...props}
tooltip="Fast forward the flow to another group"
>
<BlockIcon type={props.type} />
<BlockLabel type={props.type} />
</BlockCardLayout>
)
case IntegrationBlockType.GOOGLE_SHEETS:
return (
<BlockCardLayout {...props} tooltip="Google Sheets">
<BlockIcon type={props.type} />
<BlockLabel type={props.type} />
</BlockCardLayout>
)
case IntegrationBlockType.GOOGLE_ANALYTICS:
return (
<BlockCardLayout {...props} tooltip="Google Analytics">
<BlockIcon type={props.type} />
<BlockLabel type={props.type} />
</BlockCardLayout>
)
default:
return (
<BlockCardLayout {...props}>
<BlockIcon type={props.type} />
<BlockLabel type={props.type} />
</BlockCardLayout>
)
}
}
const BlockCardLayout = ({ type, onMouseDown, tooltip, children }: Props) => {
const { draggedBlockType } = useBlockDnd()
const [isMouseDown, setIsMouseDown] = useState(false)
@ -24,7 +107,7 @@ export const BlockCard = ({
const handleMouseDown = (e: React.MouseEvent) => onMouseDown(e, type)
return (
<Tooltip label="Coming soon!" isDisabled={!isDisabled}>
<Tooltip label={tooltip}>
<Flex pos="relative">
<HStack
borderWidth="1px"
@ -32,21 +115,15 @@ export const BlockCard = ({
rounded="lg"
flex="1"
cursor={'grab'}
opacity={isMouseDown || isDisabled ? '0.4' : '1'}
opacity={isMouseDown ? '0.4' : '1'}
onMouseDown={handleMouseDown}
bgColor={useColorModeValue('gray.50', 'gray.850')}
px="4"
py="2"
_hover={useColorModeValue({ shadow: 'md' }, { bgColor: 'gray.800' })}
transition="box-shadow 200ms, background-color 200ms"
pointerEvents={isDisabled ? 'none' : 'auto'}
>
{!isMouseDown ? (
<>
<BlockIcon type={type} />
<BlockTypeLabel type={type} />
</>
) : null}
{!isMouseDown ? children : null}
</HStack>
</Flex>
</Tooltip>

View File

@ -1,7 +1,7 @@
import { StackProps, HStack, useColorModeValue } from '@chakra-ui/react'
import { BlockType } from 'models'
import { BlockIcon } from './BlockIcon'
import { BlockTypeLabel } from './BlockTypeLabel'
import { BlockLabel } from './BlockLabel'
export const BlockCardOverlay = ({
type,
@ -24,7 +24,7 @@ export const BlockCardOverlay = ({
{...props}
>
<BlockIcon type={type} />
<BlockTypeLabel type={type} />
<BlockLabel type={type} />
</HStack>
)
}

View File

@ -37,10 +37,11 @@ import { GoogleAnalyticsLogo } from '@/features/blocks/integrations/googleAnalyt
import { AudioBubbleIcon } from '@/features/blocks/bubbles/audio'
import { WaitIcon } from '@/features/blocks/logic/wait/components/WaitIcon'
import { ScriptIcon } from '@/features/blocks/logic/script/components/ScriptIcon'
import { JumpIcon } from '@/features/blocks/logic/jump/components/JumpIcon'
type BlockIconProps = { type: BlockType } & IconProps
export const BlockIcon = ({ type, ...props }: BlockIconProps) => {
export const BlockIcon = ({ type, ...props }: BlockIconProps): JSX.Element => {
const blue = useColorModeValue('blue.500', 'blue.300')
const orange = useColorModeValue('orange.500', 'orange.300')
const purple = useColorModeValue('purple.500', 'purple.300')
@ -85,6 +86,8 @@ export const BlockIcon = ({ type, ...props }: BlockIconProps) => {
return <ScriptIcon {...props} />
case LogicBlockType.WAIT:
return <WaitIcon color={purple} {...props} />
case LogicBlockType.JUMP:
return <JumpIcon color={purple} {...props} />
case LogicBlockType.TYPEBOT_LINK:
return <TypebotLinkIcon color={purple} {...props} />
case IntegrationBlockType.GOOGLE_SHEETS:

View File

@ -1,6 +1,4 @@
import { HStack, Text, Tooltip } from '@chakra-ui/react'
import { useWorkspace } from '@/features/workspace'
import { Plan } from 'db'
import { Text } from '@chakra-ui/react'
import {
BubbleBlockType,
InputBlockType,
@ -9,13 +7,10 @@ import {
BlockType,
} from 'models'
import React from 'react'
import { isFreePlan, LockTag } from '@/features/billing'
type Props = { type: BlockType }
export const BlockTypeLabel = ({ type }: Props): JSX.Element => {
const { workspace } = useWorkspace()
export const BlockLabel = ({ type }: Props): JSX.Element => {
switch (type) {
case 'start':
return <Text>Start</Text>
@ -27,11 +22,7 @@ export const BlockTypeLabel = ({ type }: Props): JSX.Element => {
case BubbleBlockType.VIDEO:
return <Text>Video</Text>
case BubbleBlockType.EMBED:
return (
<Tooltip label="Embed a pdf, an iframe, a website...">
<Text>Embed</Text>
</Tooltip>
)
return <Text>Embed</Text>
case BubbleBlockType.AUDIO:
return <Text>Audio</Text>
case InputBlockType.NUMBER:
@ -51,14 +42,7 @@ export const BlockTypeLabel = ({ type }: Props): JSX.Element => {
case InputBlockType.RATING:
return <Text>Rating</Text>
case InputBlockType.FILE:
return (
<Tooltip label="Upload Files">
<HStack>
<Text>File</Text>
{isFreePlan(workspace) && <LockTag plan={Plan.STARTER} />}
</HStack>
</Tooltip>
)
return <Text>File</Text>
case LogicBlockType.SET_VARIABLE:
return <Text>Set variable</Text>
case LogicBlockType.CONDITION:
@ -66,31 +50,17 @@ export const BlockTypeLabel = ({ type }: Props): JSX.Element => {
case LogicBlockType.REDIRECT:
return <Text>Redirect</Text>
case LogicBlockType.SCRIPT:
return (
<Tooltip label="Run Javascript code">
<Text>Script</Text>
</Tooltip>
)
return <Text>Script</Text>
case LogicBlockType.TYPEBOT_LINK:
return (
<Tooltip label="Link to another of your typebots">
<Text>Typebot</Text>
</Tooltip>
)
return <Text>Typebot</Text>
case LogicBlockType.WAIT:
return <Text>Wait</Text>
case LogicBlockType.JUMP:
return <Text>Jump</Text>
case IntegrationBlockType.GOOGLE_SHEETS:
return (
<Tooltip label="Google Sheets">
<Text>Sheets</Text>
</Tooltip>
)
return <Text>Sheets</Text>
case IntegrationBlockType.GOOGLE_ANALYTICS:
return (
<Tooltip label="Google Analytics">
<Text>Analytics</Text>
</Tooltip>
)
return <Text>Analytics</Text>
case IntegrationBlockType.WEBHOOK:
return <Text>Webhook</Text>
case IntegrationBlockType.ZAPIER:

View File

@ -224,6 +224,7 @@ export const BlockNode = ({
align="flex-start"
w="full"
transition="border-color 0.2s"
overflow="hidden"
>
<BlockIcon
type={block.type}

View File

@ -39,6 +39,7 @@ import { AudioBubbleNode } from '@/features/blocks/bubbles/audio'
import { WaitNodeContent } from '@/features/blocks/logic/wait/components/WaitNodeContent'
import { ScriptNodeContent } from '@/features/blocks/logic/script/components/ScriptNodeContent'
import { ButtonsBlockNode } from '@/features/blocks/inputs/buttons/components/ButtonsBlockNode'
import { JumpNodeBody } from '@/features/blocks/logic/jump/components/JumpNodeBody'
type Props = {
block: Block | StartBlock
@ -125,6 +126,9 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
case LogicBlockType.WAIT: {
return <WaitNodeContent options={block.options} />
}
case LogicBlockType.JUMP: {
return <JumpNodeBody options={block.options} />
}
case LogicBlockType.TYPEBOT_LINK:
return <TypebotLinkNode block={block} />
case LogicBlockType.CONDITION:

View File

@ -42,6 +42,7 @@ import { MakeComSettings } from '@/features/blocks/integrations/makeCom'
import { HelpDocButton } from './HelpDocButton'
import { WaitSettings } from '@/features/blocks/logic/wait/components/WaitSettings'
import { ScriptSettings } from '@/features/blocks/logic/script/components/ScriptSettings'
import { JumpSettings } from '@/features/blocks/logic/jump/components/JumpSettings'
type Props = {
block: BlockWithOptions
@ -220,6 +221,15 @@ export const BlockSettings = ({
/>
)
}
case LogicBlockType.JUMP: {
return (
<JumpSettings
groupId={block.groupId}
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
case IntegrationBlockType.GOOGLE_SHEETS: {
return (
<GoogleSheetsSettingsBody

View File

@ -227,6 +227,7 @@ const NonMemoizedDraggableGroupNode = ({
bg: editableHoverBg,
}}
px="1"
noOfLines={2}
userSelect={'none'}
/>
<EditableInput minW="0" px="1" className="prevent-group-drag" />

View File

@ -409,6 +409,8 @@ const parseDefaultBlockOptions = (type: BlockWithOptionsType): BlockOptions => {
return defaultScriptOptions
case LogicBlockType.WAIT:
return defaultWaitOptions
case LogicBlockType.JUMP:
return {}
case LogicBlockType.TYPEBOT_LINK:
return {}
case IntegrationBlockType.GOOGLE_SHEETS:

View File

@ -0,0 +1 @@
{"id":"clemrhnsp00001auiaoun42wf","version":"3","createdAt":"2023-02-27T11:53:48.217Z","updatedAt":"2023-02-27T11:53:48.217Z","icon":null,"name":"My typebot","folderId":null,"groups":[{"id":"et7lna5mxbmn9blchekkejdf","title":"Start","blocks":[{"id":"ivcklgamqz8e3uwmtshm32n9","type":"start","label":"Start","groupId":"et7lna5mxbmn9blchekkejdf","outgoingEdgeId":"ogi8zj19v4krzm9xe4c9orv2"}],"graphCoordinates":{"x":0,"y":0}},{"id":"wrd4694r5fo8czflp4t79259","graphCoordinates":{"x":41.28125,"y":169.53125},"title":"Group #1","blocks":[{"id":"pjnrvjh6un6el1tjsbu7d6w1","groupId":"wrd4694r5fo8czflp4t79259","type":"text","content":{"html":"<div>Hello this is a test!</div>","richText":[{"type":"p","children":[{"text":"Hello this is a test!"}]}],"plainText":"Hello this is a test!"}},{"id":"wvt13rpcvuy5s7mj83bm946s","groupId":"wrd4694r5fo8czflp4t79259","type":"text","content":{"html":"<div>How are you?</div>","richText":[{"type":"p","children":[{"text":"How are you?"}]}],"plainText":"How are you?"}},{"id":"he864w99l8y6c6epfd9girvv","groupId":"wrd4694r5fo8czflp4t79259","type":"text input","options":{"isLong":false,"labels":{"button":"Send","placeholder":"Type your answer..."}},"outgoingEdgeId":"a42zatxkw0q8w2kq886m9ek8"}]},{"id":"eao5ufv4wtl0yos09w6wag3j","graphCoordinates":{"x":411.078125,"y":169.94140625},"title":"Group #2","blocks":[{"id":"t33v5ijddw2e2786jiurep3p","groupId":"eao5ufv4wtl0yos09w6wag3j","type":"Jump","options":{}}]}],"variables":[],"edges":[{"from":{"groupId":"et7lna5mxbmn9blchekkejdf","blockId":"ivcklgamqz8e3uwmtshm32n9"},"to":{"groupId":"wrd4694r5fo8czflp4t79259"},"id":"ogi8zj19v4krzm9xe4c9orv2"},{"from":{"groupId":"wrd4694r5fo8czflp4t79259","blockId":"he864w99l8y6c6epfd9girvv"},"to":{"groupId":"eao5ufv4wtl0yos09w6wag3j"},"id":"a42zatxkw0q8w2kq886m9ek8"}],"theme":{"chat":{"inputs":{"color":"#303235","backgroundColor":"#FFFFFF","placeholderColor":"#9095A0"},"buttons":{"color":"#FFFFFF","backgroundColor":"#0042DA"},"hostAvatar":{"url":"https://avatars.githubusercontent.com/u/16015833?v=4","isEnabled":true},"hostBubbles":{"color":"#303235","backgroundColor":"#F7F8FF"},"guestBubbles":{"color":"#FFFFFF","backgroundColor":"#FF8E21"}},"general":{"font":"Open Sans","background":{"type":"Color","content":"#ffffff"}}},"settings":{"general":{"isBrandingEnabled":true,"isInputPrefillEnabled":true,"isResultSavingEnabled":true,"isHideQueryParamsEnabled":true,"isNewResultOnRefreshEnabled":true},"metadata":{"description":"Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."},"typingEmulation":{"speed":300,"enabled":true,"maxDelay":1.5}},"publicId":null,"customDomain":null,"workspaceId":"freeWorkspace","resultsTablePreferences":null,"isArchived":false,"isClosed":false}

View File

@ -1456,59 +1456,193 @@
{
"anyOf": [
{
"allOf": [
"anyOf": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"groupId": {
"type": "string"
},
"outgoingEdgeId": {
"type": "string"
}
},
"required": [
"id",
"groupId"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"Code"
]
},
"options": {
"allOf": [
{
"type": "object",
"properties": {
"name": {
"id": {
"type": "string"
},
"content": {
"groupId": {
"type": "string"
},
"shouldExecuteInParentContext": {
"type": "boolean"
"outgoingEdgeId": {
"type": "string"
}
},
"required": [
"name"
"id",
"groupId"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"Code"
]
},
"options": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"content": {
"type": "string"
},
"shouldExecuteInParentContext": {
"type": "boolean"
}
},
"required": [
"name"
],
"additionalProperties": false
}
},
"required": [
"type",
"options"
],
"additionalProperties": false
}
},
"required": [
"type",
"options"
],
"additionalProperties": false
]
},
{
"allOf": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"groupId": {
"type": "string"
},
"outgoingEdgeId": {
"type": "string"
}
},
"required": [
"id",
"groupId"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"Condition"
]
},
"items": {
"type": "array",
"items": {
"allOf": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"blockId": {
"type": "string"
},
"outgoingEdgeId": {
"type": "string"
}
},
"required": [
"id",
"blockId"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "number",
"enum": [
1
]
},
"content": {
"type": "object",
"properties": {
"logicalOperator": {
"type": "string",
"enum": [
"OR",
"AND"
]
},
"comparisons": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"variableId": {
"type": "string"
},
"comparisonOperator": {
"type": "string",
"enum": [
"Equal to",
"Not equal",
"Contains",
"Greater than",
"Less than",
"Is set"
]
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"logicalOperator",
"comparisons"
],
"additionalProperties": false
}
},
"required": [
"type",
"content"
],
"additionalProperties": false
}
]
}
}
},
"required": [
"type",
"items"
],
"additionalProperties": false
}
]
}
]
},
@ -1539,104 +1673,28 @@
"type": {
"type": "string",
"enum": [
"Condition"
"Redirect"
]
},
"items": {
"type": "array",
"items": {
"allOf": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"blockId": {
"type": "string"
},
"outgoingEdgeId": {
"type": "string"
}
},
"required": [
"id",
"blockId"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "number",
"enum": [
1
]
},
"content": {
"type": "object",
"properties": {
"logicalOperator": {
"type": "string",
"enum": [
"OR",
"AND"
]
},
"comparisons": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"variableId": {
"type": "string"
},
"comparisonOperator": {
"type": "string",
"enum": [
"Equal to",
"Not equal",
"Contains",
"Greater than",
"Less than",
"Is set"
]
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"logicalOperator",
"comparisons"
],
"additionalProperties": false
}
},
"required": [
"type",
"content"
],
"additionalProperties": false
}
]
}
"options": {
"type": "object",
"properties": {
"url": {
"type": "string"
},
"isNewTab": {
"type": "boolean"
}
},
"required": [
"isNewTab"
],
"additionalProperties": false
}
},
"required": [
"type",
"items"
"options"
],
"additionalProperties": false
}
@ -1671,22 +1729,19 @@
"type": {
"type": "string",
"enum": [
"Redirect"
"Typebot link"
]
},
"options": {
"type": "object",
"properties": {
"url": {
"typebotId": {
"type": "string"
},
"isNewTab": {
"type": "boolean"
"groupId": {
"type": "string"
}
},
"required": [
"isNewTab"
],
"additionalProperties": false
}
},
@ -1727,17 +1782,20 @@
"type": {
"type": "string",
"enum": [
"Typebot link"
"Set variable"
]
},
"options": {
"type": "object",
"properties": {
"typebotId": {
"variableId": {
"type": "string"
},
"groupId": {
"expressionToEvaluate": {
"type": "string"
},
"isCode": {
"type": "boolean"
}
},
"additionalProperties": false
@ -1780,20 +1838,14 @@
"type": {
"type": "string",
"enum": [
"Set variable"
"Wait"
]
},
"options": {
"type": "object",
"properties": {
"variableId": {
"secondsToWaitFor": {
"type": "string"
},
"expressionToEvaluate": {
"type": "string"
},
"isCode": {
"type": "boolean"
}
},
"additionalProperties": false
@ -1836,13 +1888,16 @@
"type": {
"type": "string",
"enum": [
"Wait"
"Jump"
]
},
"options": {
"type": "object",
"properties": {
"secondsToWaitFor": {
"groupId": {
"type": "string"
},
"blockId": {
"type": "string"
}
},

View File

@ -0,0 +1,33 @@
import { ExecuteLogicResponse } from '@/features/chat'
import {
addEdgeToTypebot,
createPortalEdge,
} from '@/features/chat/api/utils/addEdgeToTypebot'
import { TRPCError } from '@trpc/server'
import { SessionState } from 'models'
import { JumpBlock } from 'models/features/blocks/logic/jump'
export const executeJumpBlock = (
state: SessionState,
{ groupId, blockId }: JumpBlock['options']
): ExecuteLogicResponse => {
const groupToJumpTo = state.typebot.groups.find(
(group) => group.id === groupId
)
const blockToJumpTo =
groupToJumpTo?.blocks.find((block) => block.id === blockId) ??
groupToJumpTo?.blocks[0]
if (!blockToJumpTo?.groupId)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Block to jump to is not found',
})
const portalEdge = createPortalEdge({
to: { groupId: blockToJumpTo?.groupId, blockId: blockToJumpTo?.id },
})
const newSessionState = addEdgeToTypebot(state, portalEdge)
return { outgoingEdgeId: portalEdge.id, newSessionState }
}

View File

@ -1,9 +1,12 @@
import { ExecuteLogicResponse } from '@/features/chat'
import {
addEdgeToTypebot,
createPortalEdge,
} from '@/features/chat/api/utils/addEdgeToTypebot'
import { saveErrorLog } from '@/features/logs/api'
import prisma from '@/lib/prisma'
import {
TypebotLinkBlock,
Edge,
SessionState,
TypebotInSession,
Variable,
@ -48,28 +51,17 @@ export const executeTypebotLink = async (
})
return { outgoingEdgeId: block.outgoingEdgeId }
}
const portalEdge: Edge = {
id: (Math.random() * 1000).toString(),
from: { blockId: '', groupId: '' },
to: {
groupId: nextGroupId,
},
}
const portalEdge = createPortalEdge({ to: { groupId: nextGroupId } })
newSessionState = addEdgeToTypebot(newSessionState, portalEdge)
return {
outgoingEdgeId: portalEdge.id,
newSessionState,
}
}
const addEdgeToTypebot = (state: SessionState, edge: Edge): SessionState => ({
...state,
typebot: {
...state.typebot,
edges: [...state.typebot.edges, edge],
},
})
const addLinkedTypebotToState = (
state: SessionState,
block: TypebotLinkBlock,

View File

@ -0,0 +1,19 @@
import { createId } from '@paralleldrive/cuid2'
import { SessionState, Edge } from 'models'
export const addEdgeToTypebot = (
state: SessionState,
edge: Edge
): SessionState => ({
...state,
typebot: {
...state.typebot,
edges: [...state.typebot.edges, edge],
},
})
export const createPortalEdge = ({ to }: Pick<Edge, 'to'>) => ({
id: createId(),
from: { blockId: '', groupId: '' },
to,
})

View File

@ -6,6 +6,7 @@ import { executeWait } from '@/features/blocks/logic/wait/api/utils/executeWait'
import { LogicBlock, LogicBlockType, SessionState } from 'models'
import { ExecuteLogicResponse } from '../../types'
import { executeScript } from '@/features/blocks/logic/script/executeScript'
import { executeJumpBlock } from '@/features/blocks/logic/jump/executeJumpBlock'
export const executeLogic =
(state: SessionState, lastBubbleBlockId?: string) =>
@ -23,5 +24,7 @@ export const executeLogic =
return executeTypebotLink(state, block)
case LogicBlockType.WAIT:
return executeWait(state, block, lastBubbleBlockId)
case LogicBlockType.JUMP:
return executeJumpBlock(state, block.options)
}
}

View File

@ -27,5 +27,7 @@ export const executeIntegration = ({
return executeSendEmailBlock(block, context)
case IntegrationBlockType.CHATWOOT:
return executeChatwootBlock(block, context)
default:
return
}
}

View File

@ -30,5 +30,7 @@ export const executeLogic = async (
return executeTypebotLink(block, context)
case LogicBlockType.WAIT:
return { nextEdgeId: await executeWait(block, context) }
default:
return {}
}
}

View File

@ -5,4 +5,5 @@ export enum LogicBlockType {
SCRIPT = 'Code',
TYPEBOT_LINK = 'Typebot link',
WAIT = 'Wait',
JUMP = 'Jump',
}

View File

@ -0,0 +1,17 @@
import { z } from 'zod'
import { blockBaseSchema } from '../baseSchemas'
import { LogicBlockType } from './enums'
export const jumpOptionsSchema = z.object({
groupId: z.string().optional(),
blockId: z.string().optional(),
})
export const jumpBlockSchema = blockBaseSchema.and(
z.object({
type: z.enum([LogicBlockType.JUMP]),
options: jumpOptionsSchema,
})
)
export type JumpBlock = z.infer<typeof jumpBlockSchema>

View File

@ -5,12 +5,14 @@ import { redirectOptionsSchema, redirectBlockSchema } from './redirect'
import { setVariableOptionsSchema, setVariableBlockSchema } from './setVariable'
import { typebotLinkOptionsSchema, typebotLinkBlockSchema } from './typebotLink'
import { waitBlockSchema, waitOptionsSchema } from './wait'
import { jumpBlockSchema, jumpOptionsSchema } from './jump'
const logicBlockOptionsSchema = scriptOptionsSchema
.or(redirectOptionsSchema)
.or(setVariableOptionsSchema)
.or(typebotLinkOptionsSchema)
.or(waitOptionsSchema)
.or(jumpOptionsSchema)
export const logicBlockSchema = scriptBlockSchema
.or(conditionBlockSchema)
@ -18,6 +20,7 @@ export const logicBlockSchema = scriptBlockSchema
.or(typebotLinkBlockSchema)
.or(setVariableBlockSchema)
.or(waitBlockSchema)
.or(jumpBlockSchema)
export type LogicBlock = z.infer<typeof logicBlockSchema>
export type LogicBlockOptions = z.infer<typeof logicBlockOptionsSchema>