@ -594,3 +594,13 @@ export const TableIcon = (props: IconProps) => (
|
|||||||
<path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"></path>
|
<path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"></path>
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const ShuffleIcon = (props: IconProps) => (
|
||||||
|
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||||
|
<polyline points="16 3 21 3 21 8"></polyline>
|
||||||
|
<line x1="4" y1="20" x2="21" y2="3"></line>
|
||||||
|
<polyline points="21 16 21 21 16 21"></polyline>
|
||||||
|
<line x1="15" y1="15" x2="21" y2="21"></line>
|
||||||
|
<line x1="4" y1="4" x2="9" y2="9"></line>
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
HStack,
|
HStack,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
|
Stack,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { Variable, VariableString } from '@typebot.io/schemas'
|
import { Variable, VariableString } from '@typebot.io/schemas'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
@ -27,6 +28,7 @@ type Props<HasVariable extends boolean> = {
|
|||||||
label?: string
|
label?: string
|
||||||
moreInfoTooltip?: string
|
moreInfoTooltip?: string
|
||||||
isRequired?: boolean
|
isRequired?: boolean
|
||||||
|
direction?: 'row' | 'column'
|
||||||
onValueChange: (value?: Value<HasVariable>) => void
|
onValueChange: (value?: Value<HasVariable>) => void
|
||||||
} & Omit<NumberInputProps, 'defaultValue' | 'value' | 'onChange' | 'isRequired'>
|
} & Omit<NumberInputProps, 'defaultValue' | 'value' | 'onChange' | 'isRequired'>
|
||||||
|
|
||||||
@ -38,6 +40,7 @@ export const NumberInput = <HasVariable extends boolean>({
|
|||||||
label,
|
label,
|
||||||
moreInfoTooltip,
|
moreInfoTooltip,
|
||||||
isRequired,
|
isRequired,
|
||||||
|
direction,
|
||||||
...props
|
...props
|
||||||
}: Props<HasVariable>) => {
|
}: Props<HasVariable>) => {
|
||||||
const [value, setValue] = useState(defaultValue?.toString() ?? '')
|
const [value, setValue] = useState(defaultValue?.toString() ?? '')
|
||||||
@ -90,7 +93,7 @@ export const NumberInput = <HasVariable extends boolean>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl
|
<FormControl
|
||||||
as={HStack}
|
as={direction === 'column' ? Stack : HStack}
|
||||||
isRequired={isRequired}
|
isRequired={isRequired}
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
width={label ? 'full' : 'auto'}
|
width={label ? 'full' : 'auto'}
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { PlusIcon } from '@/components/icons'
|
import { PlusIcon } from '@/components/icons'
|
||||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
import { ButtonItem, ItemIndices, ItemType } from '@typebot.io/schemas'
|
import { ButtonItem, Item, ItemIndices, ItemType } from '@typebot.io/schemas'
|
||||||
import React, { useRef, useState } from 'react'
|
import React, { useRef, useState } from 'react'
|
||||||
import { isNotDefined } from '@typebot.io/lib'
|
import { isNotDefined } from '@typebot.io/lib'
|
||||||
|
|
||||||
@ -26,7 +26,9 @@ export const ButtonsItemNode = ({ item, indices, isMouseOver }: Props) => {
|
|||||||
const handleInputSubmit = () => {
|
const handleInputSubmit = () => {
|
||||||
if (itemValue === '') deleteItem(indices)
|
if (itemValue === '') deleteItem(indices)
|
||||||
else
|
else
|
||||||
updateItem(indices, { content: itemValue === '' ? undefined : itemValue })
|
updateItem(indices, {
|
||||||
|
content: itemValue === '' ? undefined : itemValue,
|
||||||
|
} as Item)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
const handleKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
24
apps/builder/src/features/blocks/logic/abTest/abTest.spec.ts
Normal file
24
apps/builder/src/features/blocks/logic/abTest/abTest.spec.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import test, { expect } from '@playwright/test'
|
||||||
|
import { importTypebotInDatabase } from '@typebot.io/lib/playwright/databaseActions'
|
||||||
|
import { createId } from '@paralleldrive/cuid2'
|
||||||
|
import { getTestAsset } from '@/test/utils/playwright'
|
||||||
|
|
||||||
|
const typebotId = createId()
|
||||||
|
|
||||||
|
test.describe('AB Test block', () => {
|
||||||
|
test('its configuration should work', async ({ page }) => {
|
||||||
|
await importTypebotInDatabase(getTestAsset('typebots/logic/abTest.json'), {
|
||||||
|
id: typebotId,
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
|
await page.getByText('A 50%').click()
|
||||||
|
await page.getByLabel('Percent of users to follow A:').fill('100')
|
||||||
|
await expect(page.getByText('A 100%')).toBeVisible()
|
||||||
|
await expect(page.getByText('B 0%')).toBeVisible()
|
||||||
|
await page.getByRole('button', { name: 'Preview' }).click()
|
||||||
|
await expect(
|
||||||
|
page.locator('typebot-standard').getByText('How are you?')
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,7 @@
|
|||||||
|
import { ShuffleIcon } from '@/components/icons'
|
||||||
|
import { IconProps } from '@chakra-ui/react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const AbTestIcon = (props: IconProps) => (
|
||||||
|
<ShuffleIcon color="purple.500" {...props} />
|
||||||
|
)
|
@ -0,0 +1,66 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Flex, Stack, useColorModeValue, Text, Tag } from '@chakra-ui/react'
|
||||||
|
import { AbTestBlock } from '@typebot.io/schemas'
|
||||||
|
import { SourceEndpoint } from '@/features/graph/components/endpoints/SourceEndpoint'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
block: AbTestBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AbTestNodeBody = ({ block }: Props) => {
|
||||||
|
const borderColor = useColorModeValue('gray.200', 'gray.700')
|
||||||
|
const bg = useColorModeValue('white', undefined)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={2} w="full">
|
||||||
|
<Flex
|
||||||
|
pos="relative"
|
||||||
|
align="center"
|
||||||
|
shadow="sm"
|
||||||
|
rounded="md"
|
||||||
|
bg={bg}
|
||||||
|
borderWidth={'1px'}
|
||||||
|
borderColor={borderColor}
|
||||||
|
w="full"
|
||||||
|
>
|
||||||
|
<Text p="3">
|
||||||
|
A <Tag>{block.options.aPercent}%</Tag>
|
||||||
|
</Text>
|
||||||
|
<SourceEndpoint
|
||||||
|
source={{
|
||||||
|
groupId: block.groupId,
|
||||||
|
blockId: block.id,
|
||||||
|
itemId: block.items[0].id,
|
||||||
|
}}
|
||||||
|
pos="absolute"
|
||||||
|
right="-49px"
|
||||||
|
pointerEvents="all"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
pos="relative"
|
||||||
|
align="center"
|
||||||
|
shadow="sm"
|
||||||
|
rounded="md"
|
||||||
|
bg={bg}
|
||||||
|
borderWidth={'1px'}
|
||||||
|
borderColor={borderColor}
|
||||||
|
w="full"
|
||||||
|
>
|
||||||
|
<Text p="3">
|
||||||
|
B <Tag>{100 - block.options.aPercent}%</Tag>
|
||||||
|
</Text>
|
||||||
|
<SourceEndpoint
|
||||||
|
source={{
|
||||||
|
groupId: block.groupId,
|
||||||
|
blockId: block.id,
|
||||||
|
itemId: block.items[1].id,
|
||||||
|
}}
|
||||||
|
pos="absolute"
|
||||||
|
right="-49px"
|
||||||
|
pointerEvents="all"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
import { Stack } from '@chakra-ui/react'
|
||||||
|
import React from 'react'
|
||||||
|
import { isDefined } from '@typebot.io/lib'
|
||||||
|
import { AbTestBlock } from '@typebot.io/schemas'
|
||||||
|
import { NumberInput } from '@/components/inputs'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
options: AbTestBlock['options']
|
||||||
|
onOptionsChange: (options: AbTestBlock['options']) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AbTestSettings = ({ options, onOptionsChange }: Props) => {
|
||||||
|
const updateAPercent = (aPercent?: number) =>
|
||||||
|
isDefined(aPercent) ? onOptionsChange({ ...options, aPercent }) : null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<NumberInput
|
||||||
|
defaultValue={options.aPercent}
|
||||||
|
onValueChange={updateAPercent}
|
||||||
|
withVariableButton={false}
|
||||||
|
label="Percent of users to follow A:"
|
||||||
|
direction="column"
|
||||||
|
max={100}
|
||||||
|
min={0}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
@ -37,6 +37,7 @@ import { ConditionIcon } from '@/features/blocks/logic/condition/components/Cond
|
|||||||
import { RedirectIcon } from '@/features/blocks/logic/redirect/components/RedirectIcon'
|
import { RedirectIcon } from '@/features/blocks/logic/redirect/components/RedirectIcon'
|
||||||
import { SetVariableIcon } from '@/features/blocks/logic/setVariable/components/SetVariableIcon'
|
import { SetVariableIcon } from '@/features/blocks/logic/setVariable/components/SetVariableIcon'
|
||||||
import { TypebotLinkIcon } from '@/features/blocks/logic/typebotLink/components/TypebotLinkIcon'
|
import { TypebotLinkIcon } from '@/features/blocks/logic/typebotLink/components/TypebotLinkIcon'
|
||||||
|
import { AbTestIcon } from '@/features/blocks/logic/abTest/components/AbTestIcon'
|
||||||
|
|
||||||
type BlockIconProps = { type: BlockType } & IconProps
|
type BlockIconProps = { type: BlockType } & IconProps
|
||||||
|
|
||||||
@ -91,6 +92,8 @@ export const BlockIcon = ({ type, ...props }: BlockIconProps): JSX.Element => {
|
|||||||
return <JumpIcon color={purple} {...props} />
|
return <JumpIcon color={purple} {...props} />
|
||||||
case LogicBlockType.TYPEBOT_LINK:
|
case LogicBlockType.TYPEBOT_LINK:
|
||||||
return <TypebotLinkIcon color={purple} {...props} />
|
return <TypebotLinkIcon color={purple} {...props} />
|
||||||
|
case LogicBlockType.AB_TEST:
|
||||||
|
return <AbTestIcon color={purple} {...props} />
|
||||||
case IntegrationBlockType.GOOGLE_SHEETS:
|
case IntegrationBlockType.GOOGLE_SHEETS:
|
||||||
return <GoogleSheetsLogo {...props} />
|
return <GoogleSheetsLogo {...props} />
|
||||||
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
||||||
|
@ -57,6 +57,8 @@ export const BlockLabel = ({ type }: Props): JSX.Element => {
|
|||||||
return <Text>Wait</Text>
|
return <Text>Wait</Text>
|
||||||
case LogicBlockType.JUMP:
|
case LogicBlockType.JUMP:
|
||||||
return <Text>Jump</Text>
|
return <Text>Jump</Text>
|
||||||
|
case LogicBlockType.AB_TEST:
|
||||||
|
return <Text>AB Test</Text>
|
||||||
case IntegrationBlockType.GOOGLE_SHEETS:
|
case IntegrationBlockType.GOOGLE_SHEETS:
|
||||||
return <Text>Sheets</Text>
|
return <Text>Sheets</Text>
|
||||||
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
||||||
|
@ -7,6 +7,8 @@ import {
|
|||||||
Block,
|
Block,
|
||||||
LogicBlockType,
|
LogicBlockType,
|
||||||
InputBlockType,
|
InputBlockType,
|
||||||
|
ConditionItem,
|
||||||
|
ButtonItem,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { SetTypebot } from '../TypebotProvider'
|
import { SetTypebot } from '../TypebotProvider'
|
||||||
import produce from 'immer'
|
import produce from 'immer'
|
||||||
@ -15,7 +17,11 @@ import { byId, blockHasItems } from '@typebot.io/lib'
|
|||||||
import { createId } from '@paralleldrive/cuid2'
|
import { createId } from '@paralleldrive/cuid2'
|
||||||
import { WritableDraft } from 'immer/dist/types/types-external'
|
import { WritableDraft } from 'immer/dist/types/types-external'
|
||||||
|
|
||||||
type NewItem = Pick<Item, 'blockId' | 'outgoingEdgeId' | 'type'> & Partial<Item>
|
type NewItem = Pick<
|
||||||
|
ConditionItem | ButtonItem,
|
||||||
|
'blockId' | 'outgoingEdgeId' | 'type'
|
||||||
|
> &
|
||||||
|
Partial<ConditionItem | ButtonItem>
|
||||||
|
|
||||||
export type ItemsActions = {
|
export type ItemsActions = {
|
||||||
createItem: (item: NewItem, indices: ItemIndices) => void
|
createItem: (item: NewItem, indices: ItemIndices) => void
|
||||||
|
@ -39,6 +39,7 @@ import { TypebotLinkNode } from '@/features/blocks/logic/typebotLink/components/
|
|||||||
import { ItemNodesList } from '../item/ItemNodesList'
|
import { ItemNodesList } from '../item/ItemNodesList'
|
||||||
import { GoogleAnalyticsNodeBody } from '@/features/blocks/integrations/googleAnalytics/components/GoogleAnalyticsNodeBody'
|
import { GoogleAnalyticsNodeBody } from '@/features/blocks/integrations/googleAnalytics/components/GoogleAnalyticsNodeBody'
|
||||||
import { ChatwootNodeBody } from '@/features/blocks/integrations/chatwoot/components/ChatwootNodeBody'
|
import { ChatwootNodeBody } from '@/features/blocks/integrations/chatwoot/components/ChatwootNodeBody'
|
||||||
|
import { AbTestNodeBody } from '@/features/blocks/logic/abTest/components/AbTestNodeBody'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: Block | StartBlock
|
block: Block | StartBlock
|
||||||
@ -142,6 +143,9 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
|||||||
case LogicBlockType.JUMP: {
|
case LogicBlockType.JUMP: {
|
||||||
return <JumpNodeBody options={block.options} />
|
return <JumpNodeBody options={block.options} />
|
||||||
}
|
}
|
||||||
|
case LogicBlockType.AB_TEST: {
|
||||||
|
return <AbTestNodeBody block={block} />
|
||||||
|
}
|
||||||
case LogicBlockType.TYPEBOT_LINK:
|
case LogicBlockType.TYPEBOT_LINK:
|
||||||
return <TypebotLinkNode block={block} />
|
return <TypebotLinkNode block={block} />
|
||||||
case LogicBlockType.CONDITION:
|
case LogicBlockType.CONDITION:
|
||||||
|
@ -45,6 +45,7 @@ import { DateInputSettings } from '@/features/blocks/inputs/date/components/Date
|
|||||||
import { PhoneInputSettings } from '@/features/blocks/inputs/phone/components/PhoneInputSettings'
|
import { PhoneInputSettings } from '@/features/blocks/inputs/phone/components/PhoneInputSettings'
|
||||||
import { GoogleSheetsSettings } from '@/features/blocks/integrations/googleSheets/components/GoogleSheetsSettings'
|
import { GoogleSheetsSettings } from '@/features/blocks/integrations/googleSheets/components/GoogleSheetsSettings'
|
||||||
import { ChatwootSettings } from '@/features/blocks/integrations/chatwoot/components/ChatwootSettings'
|
import { ChatwootSettings } from '@/features/blocks/integrations/chatwoot/components/ChatwootSettings'
|
||||||
|
import { AbTestSettings } from '@/features/blocks/logic/abTest/components/AbTestSettings'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: BlockWithOptions
|
block: BlockWithOptions
|
||||||
@ -229,6 +230,14 @@ export const BlockSettings = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
case LogicBlockType.AB_TEST: {
|
||||||
|
return (
|
||||||
|
<AbTestSettings
|
||||||
|
options={block.options}
|
||||||
|
onOptionsChange={updateOptions}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
case IntegrationBlockType.GOOGLE_SHEETS: {
|
case IntegrationBlockType.GOOGLE_SHEETS: {
|
||||||
return (
|
return (
|
||||||
<GoogleSheetsSettings
|
<GoogleSheetsSettings
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import { Flex, useColorModeValue } from '@chakra-ui/react'
|
import { Flex, useColorModeValue } from '@chakra-ui/react'
|
||||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
import { ChoiceInputBlock, Item, ItemIndices } from '@typebot.io/schemas'
|
import {
|
||||||
|
ChoiceInputBlock,
|
||||||
|
Item,
|
||||||
|
ItemIndices,
|
||||||
|
ItemType,
|
||||||
|
} from '@typebot.io/schemas'
|
||||||
import React, { useRef, useState } from 'react'
|
import React, { useRef, useState } from 'react'
|
||||||
import { SourceEndpoint } from '../../endpoints/SourceEndpoint'
|
import { SourceEndpoint } from '../../endpoints/SourceEndpoint'
|
||||||
import { ItemNodeContent } from './ItemNodeContent'
|
import { ItemNodeContent } from './ItemNodeContent'
|
||||||
@ -9,6 +14,7 @@ import { ContextMenu } from '@/components/ContextMenu'
|
|||||||
import { isDefined } from '@typebot.io/lib'
|
import { isDefined } from '@typebot.io/lib'
|
||||||
import { Coordinates } from '@/features/graph/types'
|
import { Coordinates } from '@/features/graph/types'
|
||||||
import {
|
import {
|
||||||
|
DraggabbleItem,
|
||||||
NodePosition,
|
NodePosition,
|
||||||
useDragDistance,
|
useDragDistance,
|
||||||
} from '@/features/graph/providers/GraphDndProvider'
|
} from '@/features/graph/providers/GraphDndProvider'
|
||||||
@ -20,7 +26,7 @@ type Props = {
|
|||||||
indices: ItemIndices
|
indices: ItemIndices
|
||||||
onMouseDown?: (
|
onMouseDown?: (
|
||||||
blockNodePosition: { absolute: Coordinates; relative: Coordinates },
|
blockNodePosition: { absolute: Coordinates; relative: Coordinates },
|
||||||
item: Item
|
item: DraggabbleItem
|
||||||
) => void
|
) => void
|
||||||
connectionDisabled?: boolean
|
connectionDisabled?: boolean
|
||||||
}
|
}
|
||||||
@ -33,7 +39,7 @@ export const ItemNode = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const previewingBorderColor = useColorModeValue('blue.400', 'blue.300')
|
const previewingBorderColor = useColorModeValue('blue.400', 'blue.300')
|
||||||
const borderColor = useColorModeValue('gray.200', 'gray.700')
|
const borderColor = useColorModeValue('gray.200', 'gray.700')
|
||||||
const bg = useColorModeValue('white', undefined)
|
const bg = useColorModeValue('white', 'gray.850')
|
||||||
const { typebot } = useTypebot()
|
const { typebot } = useTypebot()
|
||||||
const { previewingEdge } = useGraph()
|
const { previewingEdge } = useGraph()
|
||||||
const [isMouseOver, setIsMouseOver] = useState(false)
|
const [isMouseOver, setIsMouseOver] = useState(false)
|
||||||
@ -48,7 +54,7 @@ export const ItemNode = ({
|
|||||||
| undefined
|
| undefined
|
||||||
)?.options?.isMultipleChoice
|
)?.options?.isMultipleChoice
|
||||||
const onDrag = (position: NodePosition) => {
|
const onDrag = (position: NodePosition) => {
|
||||||
if (!onMouseDown) return
|
if (!onMouseDown || item.type === ItemType.AB_TEST) return
|
||||||
onMouseDown(position, item)
|
onMouseDown(position, item)
|
||||||
}
|
}
|
||||||
useDragDistance({
|
useDragDistance({
|
||||||
|
@ -28,5 +28,7 @@ export const ItemNodeContent = ({ item, indices, isMouseOver }: Props) => {
|
|||||||
indices={indices}
|
indices={indices}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
case ItemType.AB_TEST:
|
||||||
|
return <></>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,6 @@ import {
|
|||||||
BlockIndices,
|
BlockIndices,
|
||||||
BlockWithItems,
|
BlockWithItems,
|
||||||
LogicBlockType,
|
LogicBlockType,
|
||||||
Item,
|
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { ItemNode } from './ItemNode'
|
import { ItemNode } from './ItemNode'
|
||||||
@ -20,6 +19,7 @@ import { isDefined } from '@typebot.io/lib'
|
|||||||
import {
|
import {
|
||||||
useBlockDnd,
|
useBlockDnd,
|
||||||
computeNearestPlaceholderIndex,
|
computeNearestPlaceholderIndex,
|
||||||
|
DraggabbleItem,
|
||||||
} from '@/features/graph/providers/GraphDndProvider'
|
} from '@/features/graph/providers/GraphDndProvider'
|
||||||
import { useGraph } from '@/features/graph/providers/GraphProvider'
|
import { useGraph } from '@/features/graph/providers/GraphProvider'
|
||||||
import { Coordinates } from '@dnd-kit/utilities'
|
import { Coordinates } from '@dnd-kit/utilities'
|
||||||
@ -107,7 +107,7 @@ export const ItemNodesList = ({
|
|||||||
(itemIndex: number) =>
|
(itemIndex: number) =>
|
||||||
(
|
(
|
||||||
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
|
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
|
||||||
item: Item
|
item: DraggabbleItem
|
||||||
) => {
|
) => {
|
||||||
if (!typebot || block.items.length <= 1) return
|
if (!typebot || block.items.length <= 1) return
|
||||||
placeholderRefs.current.splice(itemIndex + 1, 1)
|
placeholderRefs.current.splice(itemIndex + 1, 1)
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { useEventListener } from '@chakra-ui/react'
|
import { useEventListener } from '@chakra-ui/react'
|
||||||
import { DraggableBlock, DraggableBlockType, Item } from '@typebot.io/schemas'
|
import {
|
||||||
|
AbTestBlock,
|
||||||
|
DraggableBlock,
|
||||||
|
DraggableBlockType,
|
||||||
|
Item,
|
||||||
|
} from '@typebot.io/schemas'
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
Dispatch,
|
Dispatch,
|
||||||
@ -18,13 +23,15 @@ type NodeElement = {
|
|||||||
element: HTMLDivElement
|
element: HTMLDivElement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DraggabbleItem = Exclude<Item, AbTestBlock['items'][number]>
|
||||||
|
|
||||||
const graphDndContext = createContext<{
|
const graphDndContext = createContext<{
|
||||||
draggedBlockType?: DraggableBlockType
|
draggedBlockType?: DraggableBlockType
|
||||||
setDraggedBlockType: Dispatch<SetStateAction<DraggableBlockType | undefined>>
|
setDraggedBlockType: Dispatch<SetStateAction<DraggableBlockType | undefined>>
|
||||||
draggedBlock?: DraggableBlock
|
draggedBlock?: DraggableBlock
|
||||||
setDraggedBlock: Dispatch<SetStateAction<DraggableBlock | undefined>>
|
setDraggedBlock: Dispatch<SetStateAction<DraggableBlock | undefined>>
|
||||||
draggedItem?: Item
|
draggedItem?: DraggabbleItem
|
||||||
setDraggedItem: Dispatch<SetStateAction<Item | undefined>>
|
setDraggedItem: Dispatch<SetStateAction<DraggabbleItem | undefined>>
|
||||||
mouseOverGroup?: NodeElement
|
mouseOverGroup?: NodeElement
|
||||||
setMouseOverGroup: (node: NodeElement | undefined) => void
|
setMouseOverGroup: (node: NodeElement | undefined) => void
|
||||||
mouseOverBlock?: NodeElement
|
mouseOverBlock?: NodeElement
|
||||||
@ -40,7 +47,7 @@ export const GraphDndProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
const [draggedBlockType, setDraggedBlockType] = useState<
|
const [draggedBlockType, setDraggedBlockType] = useState<
|
||||||
DraggableBlockType | undefined
|
DraggableBlockType | undefined
|
||||||
>()
|
>()
|
||||||
const [draggedItem, setDraggedItem] = useState<Item | undefined>()
|
const [draggedItem, setDraggedItem] = useState<DraggabbleItem | undefined>()
|
||||||
const [mouseOverGroup, _setMouseOverGroup] = useState<NodeElement>()
|
const [mouseOverGroup, _setMouseOverGroup] = useState<NodeElement>()
|
||||||
const [mouseOverBlock, _setMouseOverBlock] = useState<NodeElement>()
|
const [mouseOverBlock, _setMouseOverBlock] = useState<NodeElement>()
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { isChoiceInput, isConditionBlock, isDefined } from '@typebot.io/lib'
|
import { isChoiceInput, isConditionBlock, isDefined } from '@typebot.io/lib'
|
||||||
import { Block, InputBlockType } from '@typebot.io/schemas'
|
import { Block, InputBlockType, LogicBlockType } from '@typebot.io/schemas'
|
||||||
|
|
||||||
export const hasDefaultConnector = (block: Block) =>
|
export const hasDefaultConnector = (block: Block) =>
|
||||||
(!isChoiceInput(block) && !isConditionBlock(block)) ||
|
(!isChoiceInput(block) &&
|
||||||
|
!isConditionBlock(block) &&
|
||||||
|
block.type !== LogicBlockType.AB_TEST) ||
|
||||||
(block.type === InputBlockType.CHOICE &&
|
(block.type === InputBlockType.CHOICE &&
|
||||||
isDefined(block.options.dynamicVariableId))
|
isDefined(block.options.dynamicVariableId))
|
||||||
|
@ -42,10 +42,14 @@ import {
|
|||||||
Item,
|
Item,
|
||||||
ItemType,
|
ItemType,
|
||||||
LogicBlockType,
|
LogicBlockType,
|
||||||
|
defaultAbTestOptions,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
|
|
||||||
const parseDefaultItems = (
|
const parseDefaultItems = (
|
||||||
type: LogicBlockType.CONDITION | InputBlockType.CHOICE,
|
type:
|
||||||
|
| LogicBlockType.CONDITION
|
||||||
|
| InputBlockType.CHOICE
|
||||||
|
| LogicBlockType.AB_TEST,
|
||||||
blockId: string
|
blockId: string
|
||||||
): Item[] => {
|
): Item[] => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -60,6 +64,11 @@ const parseDefaultItems = (
|
|||||||
content: defaultConditionContent,
|
content: defaultConditionContent,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
case LogicBlockType.AB_TEST:
|
||||||
|
return [
|
||||||
|
{ id: createId(), blockId, type: ItemType.AB_TEST, path: 'a' },
|
||||||
|
{ id: createId(), blockId, type: ItemType.AB_TEST, path: 'b' },
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,6 +121,8 @@ const parseDefaultBlockOptions = (type: BlockWithOptionsType): BlockOptions => {
|
|||||||
return {}
|
return {}
|
||||||
case LogicBlockType.TYPEBOT_LINK:
|
case LogicBlockType.TYPEBOT_LINK:
|
||||||
return {}
|
return {}
|
||||||
|
case LogicBlockType.AB_TEST:
|
||||||
|
return defaultAbTestOptions
|
||||||
case IntegrationBlockType.GOOGLE_SHEETS:
|
case IntegrationBlockType.GOOGLE_SHEETS:
|
||||||
return defaultGoogleSheetsOptions
|
return defaultGoogleSheetsOptions
|
||||||
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
||||||
|
164
apps/builder/src/test/assets/typebots/logic/abTest.json
Normal file
164
apps/builder/src/test/assets/typebots/logic/abTest.json
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
{
|
||||||
|
"id": "clgkx6u4n00021ait8o4c1nvs",
|
||||||
|
"version": "3",
|
||||||
|
"createdAt": "2023-04-17T14:17:13.223Z",
|
||||||
|
"updatedAt": "2023-04-17T14:17:13.223Z",
|
||||||
|
"icon": null,
|
||||||
|
"name": "My typebot",
|
||||||
|
"folderId": null,
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"id": "bb5ad8yx3j52fecxdnztlap5",
|
||||||
|
"title": "Start",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "q20ww00q5nnv4icmi3rhbtda",
|
||||||
|
"type": "start",
|
||||||
|
"label": "Start",
|
||||||
|
"groupId": "bb5ad8yx3j52fecxdnztlap5",
|
||||||
|
"outgoingEdgeId": "qr2ebzm62bcz21jh2wmv1odd"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"graphCoordinates": { "x": 0, "y": 0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wv63xqdlwj6ub5vb3ulbmzd3",
|
||||||
|
"graphCoordinates": { "x": 310, "y": 206 },
|
||||||
|
"title": "Group #1",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "ice9jqqw4glrx5kblisez4lp",
|
||||||
|
"groupId": "wv63xqdlwj6ub5vb3ulbmzd3",
|
||||||
|
"type": "text",
|
||||||
|
"content": {
|
||||||
|
"richText": [{ "type": "p", "children": [{ "text": "Hey" }] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rorajfbwc2evvumo837ulwmy",
|
||||||
|
"groupId": "wv63xqdlwj6ub5vb3ulbmzd3",
|
||||||
|
"type": "AB test",
|
||||||
|
"options": { "aPercent": 50 },
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "rcw6vr8xfcb5dmfx2ts04rb0",
|
||||||
|
"blockId": "rorajfbwc2evvumo837ulwmy",
|
||||||
|
"type": 2,
|
||||||
|
"path": "a",
|
||||||
|
"outgoingEdgeId": "lmof3n7qyjo5bkw9kaksgrzh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "i9shyze7fyd5wfxfq0svg2lg",
|
||||||
|
"blockId": "rorajfbwc2evvumo837ulwmy",
|
||||||
|
"type": 2,
|
||||||
|
"path": "b",
|
||||||
|
"outgoingEdgeId": "kvaqhcb1df8cmvekwtpd1irr"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ybebe0sxg6zu307a4ruwhmb6",
|
||||||
|
"graphCoordinates": { "x": 705.96875, "y": 166.57421875 },
|
||||||
|
"title": "Group #2",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "jqqjruceaqwqvm74isgoxbez",
|
||||||
|
"groupId": "ybebe0sxg6zu307a4ruwhmb6",
|
||||||
|
"type": "text",
|
||||||
|
"content": {
|
||||||
|
"richText": [
|
||||||
|
{ "type": "p", "children": [{ "text": "How are you?" }] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d66ppqavqy5mac7fy9jo5lib",
|
||||||
|
"graphCoordinates": { "x": 702.44921875, "y": 350.9921875 },
|
||||||
|
"title": "Group #2 copy",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "x74mxf3hby8hw7zo3k8b5vc1",
|
||||||
|
"groupId": "d66ppqavqy5mac7fy9jo5lib",
|
||||||
|
"type": "text",
|
||||||
|
"content": {
|
||||||
|
"richText": [
|
||||||
|
{ "type": "p", "children": [{ "text": "What's up?" }] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variables": [],
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"from": {
|
||||||
|
"groupId": "wv63xqdlwj6ub5vb3ulbmzd3",
|
||||||
|
"blockId": "rorajfbwc2evvumo837ulwmy",
|
||||||
|
"itemId": "rcw6vr8xfcb5dmfx2ts04rb0"
|
||||||
|
},
|
||||||
|
"to": { "groupId": "ybebe0sxg6zu307a4ruwhmb6" },
|
||||||
|
"id": "lmof3n7qyjo5bkw9kaksgrzh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": {
|
||||||
|
"groupId": "wv63xqdlwj6ub5vb3ulbmzd3",
|
||||||
|
"blockId": "rorajfbwc2evvumo837ulwmy",
|
||||||
|
"itemId": "i9shyze7fyd5wfxfq0svg2lg"
|
||||||
|
},
|
||||||
|
"to": { "groupId": "d66ppqavqy5mac7fy9jo5lib" },
|
||||||
|
"id": "kvaqhcb1df8cmvekwtpd1irr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": {
|
||||||
|
"groupId": "bb5ad8yx3j52fecxdnztlap5",
|
||||||
|
"blockId": "q20ww00q5nnv4icmi3rhbtda"
|
||||||
|
},
|
||||||
|
"to": { "groupId": "wv63xqdlwj6ub5vb3ulbmzd3" },
|
||||||
|
"id": "qr2ebzm62bcz21jh2wmv1odd"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selectedThemeTemplateId": null,
|
||||||
|
"settings": {
|
||||||
|
"general": {
|
||||||
|
"isBrandingEnabled": false,
|
||||||
|
"isInputPrefillEnabled": 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": "proWorkspace",
|
||||||
|
"resultsTablePreferences": null,
|
||||||
|
"isArchived": false,
|
||||||
|
"isClosed": false
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import { AbTestBlock, SessionState } from '@typebot.io/schemas'
|
||||||
|
import { ExecuteLogicResponse } from '@/features/chat/types'
|
||||||
|
|
||||||
|
export const executeAbTest = (
|
||||||
|
_: SessionState,
|
||||||
|
block: AbTestBlock
|
||||||
|
): ExecuteLogicResponse => {
|
||||||
|
const aEdgeId = block.items[0].outgoingEdgeId
|
||||||
|
const random = Math.random() * 100
|
||||||
|
if (random < block.options.aPercent && aEdgeId) {
|
||||||
|
return { outgoingEdgeId: aEdgeId }
|
||||||
|
}
|
||||||
|
const bEdgeId = block.items[1].outgoingEdgeId
|
||||||
|
if (bEdgeId) return { outgoingEdgeId: bEdgeId }
|
||||||
|
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||||
|
}
|
@ -7,6 +7,7 @@ import { executeRedirect } from '@/features/blocks/logic/redirect/executeRedirec
|
|||||||
import { executeCondition } from '@/features/blocks/logic/condition/executeCondition'
|
import { executeCondition } from '@/features/blocks/logic/condition/executeCondition'
|
||||||
import { executeSetVariable } from '@/features/blocks/logic/setVariable/executeSetVariable'
|
import { executeSetVariable } from '@/features/blocks/logic/setVariable/executeSetVariable'
|
||||||
import { executeTypebotLink } from '@/features/blocks/logic/typebotLink/executeTypebotLink'
|
import { executeTypebotLink } from '@/features/blocks/logic/typebotLink/executeTypebotLink'
|
||||||
|
import { executeAbTest } from '@/features/blocks/logic/abTest/executeAbTest'
|
||||||
|
|
||||||
export const executeLogic =
|
export const executeLogic =
|
||||||
(state: SessionState, lastBubbleBlockId?: string) =>
|
(state: SessionState, lastBubbleBlockId?: string) =>
|
||||||
@ -26,5 +27,7 @@ export const executeLogic =
|
|||||||
return executeWait(state, block, lastBubbleBlockId)
|
return executeWait(state, block, lastBubbleBlockId)
|
||||||
case LogicBlockType.JUMP:
|
case LogicBlockType.JUMP:
|
||||||
return executeJumpBlock(state, block.options)
|
return executeJumpBlock(state, block.options)
|
||||||
|
case LogicBlockType.AB_TEST:
|
||||||
|
return executeAbTest(state, block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,8 +132,13 @@ export const blockTypeHasWebhook = (
|
|||||||
|
|
||||||
export const blockTypeHasItems = (
|
export const blockTypeHasItems = (
|
||||||
type: BlockType
|
type: BlockType
|
||||||
): type is LogicBlockType.CONDITION | InputBlockType.CHOICE =>
|
): type is
|
||||||
type === LogicBlockType.CONDITION || type === InputBlockType.CHOICE
|
| LogicBlockType.CONDITION
|
||||||
|
| InputBlockType.CHOICE
|
||||||
|
| LogicBlockType.AB_TEST =>
|
||||||
|
type === LogicBlockType.CONDITION ||
|
||||||
|
type === InputBlockType.CHOICE ||
|
||||||
|
type === LogicBlockType.AB_TEST
|
||||||
|
|
||||||
export const blockHasItems = (
|
export const blockHasItems = (
|
||||||
block: Block
|
block: Block
|
||||||
|
31
packages/schemas/features/blocks/logic/abTest.ts
Normal file
31
packages/schemas/features/blocks/logic/abTest.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { blockBaseSchema } from '../baseSchemas'
|
||||||
|
import { LogicBlockType } from './enums'
|
||||||
|
import { itemBaseSchema } from '../../items/baseSchemas'
|
||||||
|
import { ItemType } from '../../items/enums'
|
||||||
|
|
||||||
|
export const aItemSchema = itemBaseSchema.extend({
|
||||||
|
type: z.literal(ItemType.AB_TEST),
|
||||||
|
path: z.literal('a'),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const bItemSchema = itemBaseSchema.extend({
|
||||||
|
type: z.literal(ItemType.AB_TEST),
|
||||||
|
path: z.literal('b'),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const abTestBlockSchema = blockBaseSchema.merge(
|
||||||
|
z.object({
|
||||||
|
type: z.enum([LogicBlockType.AB_TEST]),
|
||||||
|
items: z.tuple([aItemSchema, bItemSchema]),
|
||||||
|
options: z.object({
|
||||||
|
aPercent: z.number().min(0).max(100),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const defaultAbTestOptions = {
|
||||||
|
aPercent: 50,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AbTestBlock = z.infer<typeof abTestBlockSchema>
|
@ -6,4 +6,5 @@ export enum LogicBlockType {
|
|||||||
TYPEBOT_LINK = 'Typebot link',
|
TYPEBOT_LINK = 'Typebot link',
|
||||||
WAIT = 'Wait',
|
WAIT = 'Wait',
|
||||||
JUMP = 'Jump',
|
JUMP = 'Jump',
|
||||||
|
AB_TEST = 'AB test',
|
||||||
}
|
}
|
||||||
|
@ -5,3 +5,4 @@ export * from './redirect'
|
|||||||
export * from './setVariable'
|
export * from './setVariable'
|
||||||
export * from './typebotLink'
|
export * from './typebotLink'
|
||||||
export * from './wait'
|
export * from './wait'
|
||||||
|
export * from './abTest'
|
||||||
|
@ -42,6 +42,8 @@ import {
|
|||||||
setVariableBlockSchema,
|
setVariableBlockSchema,
|
||||||
typebotLinkBlockSchema,
|
typebotLinkBlockSchema,
|
||||||
waitBlockSchema,
|
waitBlockSchema,
|
||||||
|
abTestBlockSchema,
|
||||||
|
AbTestBlock,
|
||||||
} from './logic'
|
} from './logic'
|
||||||
import { jumpBlockSchema } from './logic/jump'
|
import { jumpBlockSchema } from './logic/jump'
|
||||||
|
|
||||||
@ -79,7 +81,7 @@ export type BlockOptions =
|
|||||||
| LogicBlockOptions
|
| LogicBlockOptions
|
||||||
| IntegrationBlockOptions
|
| IntegrationBlockOptions
|
||||||
|
|
||||||
export type BlockWithItems = ConditionBlock | ChoiceInputBlock
|
export type BlockWithItems = ConditionBlock | ChoiceInputBlock | AbTestBlock
|
||||||
|
|
||||||
export type BlockBase = z.infer<typeof blockBaseSchema>
|
export type BlockBase = z.infer<typeof blockBaseSchema>
|
||||||
|
|
||||||
@ -123,6 +125,7 @@ export const logicBlockSchema = z.discriminatedUnion('type', [
|
|||||||
typebotLinkBlockSchema,
|
typebotLinkBlockSchema,
|
||||||
waitBlockSchema,
|
waitBlockSchema,
|
||||||
jumpBlockSchema,
|
jumpBlockSchema,
|
||||||
|
abTestBlockSchema,
|
||||||
])
|
])
|
||||||
|
|
||||||
export type LogicBlock = z.infer<typeof logicBlockSchema>
|
export type LogicBlock = z.infer<typeof logicBlockSchema>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export enum ItemType {
|
export enum ItemType {
|
||||||
BUTTON,
|
BUTTON,
|
||||||
CONDITION,
|
CONDITION,
|
||||||
|
AB_TEST,
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { buttonItemSchema } from '../blocks/inputs/choice'
|
import { buttonItemSchema } from '../blocks/inputs/choice'
|
||||||
import { conditionItemSchema } from '../blocks/logic/condition'
|
import { conditionItemSchema } from '../blocks/logic/condition'
|
||||||
|
import { aItemSchema, bItemSchema } from '../blocks'
|
||||||
|
|
||||||
const itemSchema = buttonItemSchema.or(conditionItemSchema)
|
const itemSchema = buttonItemSchema
|
||||||
|
.or(conditionItemSchema)
|
||||||
|
.or(aItemSchema)
|
||||||
|
.or(bItemSchema)
|
||||||
|
|
||||||
export type Item = z.infer<typeof itemSchema>
|
export type Item = z.infer<typeof itemSchema>
|
||||||
|
Reference in New Issue
Block a user