Skip to content

Commit

Permalink
✨ Add AB test block
Browse files Browse the repository at this point in the history
Closes #449
  • Loading branch information
baptisteArno committed Apr 17, 2023
1 parent b416c6e commit 7e937e1
Show file tree
Hide file tree
Showing 28 changed files with 443 additions and 21 deletions.
10 changes: 10 additions & 0 deletions apps/builder/src/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
</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>
)
5 changes: 4 additions & 1 deletion apps/builder/src/components/inputs/NumberInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
HStack,
FormControl,
FormLabel,
Stack,
} from '@chakra-ui/react'
import { Variable, VariableString } from '@typebot.io/schemas'
import { useEffect, useState } from 'react'
Expand All @@ -27,6 +28,7 @@ type Props<HasVariable extends boolean> = {
label?: string
moreInfoTooltip?: string
isRequired?: boolean
direction?: 'row' | 'column'
onValueChange: (value?: Value<HasVariable>) => void
} & Omit<NumberInputProps, 'defaultValue' | 'value' | 'onChange' | 'isRequired'>

Expand All @@ -38,6 +40,7 @@ export const NumberInput = <HasVariable extends boolean>({
label,
moreInfoTooltip,
isRequired,
direction,
...props
}: Props<HasVariable>) => {
const [value, setValue] = useState(defaultValue?.toString() ?? '')
Expand Down Expand Up @@ -90,7 +93,7 @@ export const NumberInput = <HasVariable extends boolean>({

return (
<FormControl
as={HStack}
as={direction === 'column' ? Stack : HStack}
isRequired={isRequired}
justifyContent="space-between"
width={label ? 'full' : 'auto'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '@chakra-ui/react'
import { PlusIcon } from '@/components/icons'
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 { isNotDefined } from '@typebot.io/lib'

Expand All @@ -26,7 +26,9 @@ export const ButtonsItemNode = ({ item, indices, isMouseOver }: Props) => {
const handleInputSubmit = () => {
if (itemValue === '') deleteItem(indices)
else
updateItem(indices, { content: itemValue === '' ? undefined : itemValue })
updateItem(indices, {
content: itemValue === '' ? undefined : itemValue,
} as Item)
}

const handleKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
Expand Down
24 changes: 24 additions & 0 deletions apps/builder/src/features/blocks/logic/abTest/abTest.spec.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
Original file line number Diff line number Diff line change
@@ -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} />
)
Original file line number Diff line number Diff line change
@@ -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>
)
}
Original file line number Diff line number Diff line change
@@ -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>
)
}
3 changes: 3 additions & 0 deletions apps/builder/src/features/editor/components/BlockIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { ConditionIcon } from '@/features/blocks/logic/condition/components/Cond
import { RedirectIcon } from '@/features/blocks/logic/redirect/components/RedirectIcon'
import { SetVariableIcon } from '@/features/blocks/logic/setVariable/components/SetVariableIcon'
import { TypebotLinkIcon } from '@/features/blocks/logic/typebotLink/components/TypebotLinkIcon'
import { AbTestIcon } from '@/features/blocks/logic/abTest/components/AbTestIcon'

type BlockIconProps = { type: BlockType } & IconProps

Expand Down Expand Up @@ -91,6 +92,8 @@ export const BlockIcon = ({ type, ...props }: BlockIconProps): JSX.Element => {
return <JumpIcon color={purple} {...props} />
case LogicBlockType.TYPEBOT_LINK:
return <TypebotLinkIcon color={purple} {...props} />
case LogicBlockType.AB_TEST:
return <AbTestIcon color={purple} {...props} />
case IntegrationBlockType.GOOGLE_SHEETS:
return <GoogleSheetsLogo {...props} />
case IntegrationBlockType.GOOGLE_ANALYTICS:
Expand Down
2 changes: 2 additions & 0 deletions apps/builder/src/features/editor/components/BlockLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export const BlockLabel = ({ type }: Props): JSX.Element => {
return <Text>Wait</Text>
case LogicBlockType.JUMP:
return <Text>Jump</Text>
case LogicBlockType.AB_TEST:
return <Text>AB Test</Text>
case IntegrationBlockType.GOOGLE_SHEETS:
return <Text>Sheets</Text>
case IntegrationBlockType.GOOGLE_ANALYTICS:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
Block,
LogicBlockType,
InputBlockType,
ConditionItem,
ButtonItem,
} from '@typebot.io/schemas'
import { SetTypebot } from '../TypebotProvider'
import produce from 'immer'
Expand All @@ -15,7 +17,11 @@ import { byId, blockHasItems } from '@typebot.io/lib'
import { createId } from '@paralleldrive/cuid2'
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 = {
createItem: (item: NewItem, indices: ItemIndices) => void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { TypebotLinkNode } from '@/features/blocks/logic/typebotLink/components/
import { ItemNodesList } from '../item/ItemNodesList'
import { GoogleAnalyticsNodeBody } from '@/features/blocks/integrations/googleAnalytics/components/GoogleAnalyticsNodeBody'
import { ChatwootNodeBody } from '@/features/blocks/integrations/chatwoot/components/ChatwootNodeBody'
import { AbTestNodeBody } from '@/features/blocks/logic/abTest/components/AbTestNodeBody'

type Props = {
block: Block | StartBlock
Expand Down Expand Up @@ -142,6 +143,9 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
case LogicBlockType.JUMP: {
return <JumpNodeBody options={block.options} />
}
case LogicBlockType.AB_TEST: {
return <AbTestNodeBody block={block} />
}
case LogicBlockType.TYPEBOT_LINK:
return <TypebotLinkNode block={block} />
case LogicBlockType.CONDITION:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { DateInputSettings } from '@/features/blocks/inputs/date/components/Date
import { PhoneInputSettings } from '@/features/blocks/inputs/phone/components/PhoneInputSettings'
import { GoogleSheetsSettings } from '@/features/blocks/integrations/googleSheets/components/GoogleSheetsSettings'
import { ChatwootSettings } from '@/features/blocks/integrations/chatwoot/components/ChatwootSettings'
import { AbTestSettings } from '@/features/blocks/logic/abTest/components/AbTestSettings'

type Props = {
block: BlockWithOptions
Expand Down Expand Up @@ -229,6 +230,14 @@ export const BlockSettings = ({
/>
)
}
case LogicBlockType.AB_TEST: {
return (
<AbTestSettings
options={block.options}
onOptionsChange={updateOptions}
/>
)
}
case IntegrationBlockType.GOOGLE_SHEETS: {
return (
<GoogleSheetsSettings
Expand Down
14 changes: 10 additions & 4 deletions apps/builder/src/features/graph/components/nodes/item/ItemNode.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Flex, useColorModeValue } from '@chakra-ui/react'
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 { SourceEndpoint } from '../../endpoints/SourceEndpoint'
import { ItemNodeContent } from './ItemNodeContent'
Expand All @@ -9,6 +14,7 @@ import { ContextMenu } from '@/components/ContextMenu'
import { isDefined } from '@typebot.io/lib'
import { Coordinates } from '@/features/graph/types'
import {
DraggabbleItem,
NodePosition,
useDragDistance,
} from '@/features/graph/providers/GraphDndProvider'
Expand All @@ -20,7 +26,7 @@ type Props = {
indices: ItemIndices
onMouseDown?: (
blockNodePosition: { absolute: Coordinates; relative: Coordinates },
item: Item
item: DraggabbleItem
) => void
connectionDisabled?: boolean
}
Expand All @@ -33,7 +39,7 @@ export const ItemNode = ({
}: Props) => {
const previewingBorderColor = useColorModeValue('blue.400', 'blue.300')
const borderColor = useColorModeValue('gray.200', 'gray.700')
const bg = useColorModeValue('white', undefined)
const bg = useColorModeValue('white', 'gray.850')
const { typebot } = useTypebot()
const { previewingEdge } = useGraph()
const [isMouseOver, setIsMouseOver] = useState(false)
Expand All @@ -48,7 +54,7 @@ export const ItemNode = ({
| undefined
)?.options?.isMultipleChoice
const onDrag = (position: NodePosition) => {
if (!onMouseDown) return
if (!onMouseDown || item.type === ItemType.AB_TEST) return
onMouseDown(position, item)
}
useDragDistance({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,7 @@ export const ItemNodeContent = ({ item, indices, isMouseOver }: Props) => {
indices={indices}
/>
)
case ItemType.AB_TEST:
return <></>
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
BlockIndices,
BlockWithItems,
LogicBlockType,
Item,
} from '@typebot.io/schemas'
import React, { useEffect, useRef, useState } from 'react'
import { ItemNode } from './ItemNode'
Expand All @@ -20,6 +19,7 @@ import { isDefined } from '@typebot.io/lib'
import {
useBlockDnd,
computeNearestPlaceholderIndex,
DraggabbleItem,
} from '@/features/graph/providers/GraphDndProvider'
import { useGraph } from '@/features/graph/providers/GraphProvider'
import { Coordinates } from '@dnd-kit/utilities'
Expand Down Expand Up @@ -107,7 +107,7 @@ export const ItemNodesList = ({
(itemIndex: number) =>
(
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
item: Item
item: DraggabbleItem
) => {
if (!typebot || block.items.length <= 1) return
placeholderRefs.current.splice(itemIndex + 1, 1)
Expand Down
Loading

4 comments on commit 7e937e1

@vercel
Copy link

@vercel vercel bot commented on 7e937e1 Apr 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs – ./apps/docs

docs-typebot-io.vercel.app
docs-git-main-typebot-io.vercel.app
docs.typebot.io

@vercel
Copy link

@vercel vercel bot commented on 7e937e1 Apr 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 7e937e1 Apr 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 7e937e1 Apr 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

builder-v2 – ./apps/builder

app.typebot.io
builder-v2-git-main-typebot-io.vercel.app
builder-v2-typebot-io.vercel.app

Please sign in to comment.