@@ -9,7 +9,7 @@ import {
|
||||
} from '@chakra-ui/react'
|
||||
import { TrashIcon, PlusIcon } from '@/components/icons'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
type ItemWithId<T> = T & { id: string }
|
||||
|
||||
@@ -40,6 +40,10 @@ export const TableList = <T,>({
|
||||
const [items, setItems] = useState(initialItems)
|
||||
const [showDeleteIndex, setShowDeleteIndex] = useState<number | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (items.length && initialItems.length === 0) setItems(initialItems)
|
||||
}, [initialItems, items.length])
|
||||
|
||||
const createItem = () => {
|
||||
const id = createId()
|
||||
const newItem = { id, ...newItemDefaultProps } as ItemWithId<T>
|
||||
|
||||
@@ -225,8 +225,10 @@ test('should display invoices', async ({ page }) => {
|
||||
await page.click('text=Settings & Members')
|
||||
await page.click('text=Billing & Usage')
|
||||
await expect(page.locator('text="Invoices"')).toBeVisible()
|
||||
await expect(page.locator('tr')).toHaveCount(2)
|
||||
await expect(page.locator('tr')).toHaveCount(3)
|
||||
await expect(page.locator('text="$39.00"')).toBeVisible()
|
||||
await expect(page.locator('text="$34.00"')).toBeVisible()
|
||||
await expect(page.locator('text="$174.00"')).toBeVisible()
|
||||
})
|
||||
|
||||
test('custom plans should work', async ({ page }) => {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { IconProps, Icon } from '@chakra-ui/react'
|
||||
|
||||
export const PixelLogo = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 288 191" fill="none" {...props}>
|
||||
<path
|
||||
d="M31.06 125.96C31.06 136.94 33.47 145.37 36.62 150.47C40.75 157.15 46.91 159.98 53.19 159.98C61.29 159.98 68.7 157.97 82.98 138.22C94.42 122.39 107.9 100.17 116.97 86.24L132.33 62.64C143 46.25 155.35 28.03 169.51 15.68C181.07 5.6 193.54 0 206.09 0C227.16 0 247.23 12.21 262.59 35.11C279.4 60.19 287.56 91.78 287.56 124.38C287.56 143.76 283.74 158 277.24 169.25C270.96 180.13 258.72 191 238.13 191V159.98C255.76 159.98 260.16 143.78 260.16 125.24C260.16 98.82 254 69.5 240.43 48.55C230.8 33.69 218.32 24.61 204.59 24.61C189.74 24.61 177.79 35.81 164.36 55.78C157.22 66.39 149.89 79.32 141.66 93.91L132.6 109.96C114.4 142.23 109.79 149.58 100.69 161.71C84.74 182.95 71.12 191 53.19 191C31.92 191 18.47 181.79 10.14 167.91C3.34 156.6 0 141.76 0 124.85L31.06 125.96Z"
|
||||
fill="#0081FB"
|
||||
/>
|
||||
<path
|
||||
d="M24.4902 37.3C38.7302 15.35 59.2802 0 82.8502 0C96.5002 0 110.07 4.04 124.24 15.61C139.74 28.26 156.26 49.09 176.87 83.42L184.26 95.74C202.1 125.46 212.25 140.75 218.19 147.96C225.83 157.22 231.18 159.98 238.13 159.98C255.76 159.98 260.16 143.78 260.16 125.24L287.56 124.38C287.56 143.76 283.74 158 277.24 169.25C270.96 180.13 258.72 191 238.13 191C225.33 191 213.99 188.22 201.45 176.39C191.81 167.31 180.54 151.18 171.87 136.68L146.08 93.6C133.14 71.98 121.27 55.86 114.4 48.56C107.01 40.71 97.5102 31.23 82.3502 31.23C70.0802 31.23 59.6602 39.84 50.9402 53.01L24.4902 37.3Z"
|
||||
fill="url(#paint0_linear_1302_7)"
|
||||
/>
|
||||
<path
|
||||
d="M82.35 31.23C70.08 31.23 59.66 39.84 50.94 53.01C38.61 71.62 31.06 99.34 31.06 125.96C31.06 136.94 33.47 145.37 36.62 150.47L10.14 167.91C3.34 156.6 0 141.76 0 124.85C0 94.1 8.44 62.05 24.49 37.3C38.73 15.35 59.28 0 82.85 0L82.35 31.23Z"
|
||||
fill="url(#paint1_linear_1302_7)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1302_7"
|
||||
x1="61.0002"
|
||||
y1="117"
|
||||
x2="259"
|
||||
y2="127"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#0064E1" />
|
||||
<stop offset="0.4" stop-color="#0064E1" />
|
||||
<stop offset="0.83" stop-color="#0073EE" />
|
||||
<stop offset="1" stop-color="#0082FB" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_1302_7"
|
||||
x1="45"
|
||||
y1="139"
|
||||
x2="45"
|
||||
y2="66"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#0082FB" />
|
||||
<stop offset="1" stop-color="#0064E0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</Icon>
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { PixelBlock } from '@typebot.io/schemas'
|
||||
|
||||
type Props = {
|
||||
options: PixelBlock['options']
|
||||
}
|
||||
|
||||
export const PixelNodeBody = ({ options }: Props) => (
|
||||
<Text
|
||||
color={options.eventType || options.pixelId ? 'currentcolor' : 'gray.500'}
|
||||
noOfLines={1}
|
||||
>
|
||||
{options.eventType
|
||||
? `Track "${options.eventType}"`
|
||||
: options.pixelId
|
||||
? 'Init Pixel'
|
||||
: 'Configure...'}
|
||||
</Text>
|
||||
)
|
||||
@@ -0,0 +1,185 @@
|
||||
import { DropdownList } from '@/components/DropdownList'
|
||||
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
|
||||
import { TableList, TableListItemProps } from '@/components/TableList'
|
||||
import { TextLink } from '@/components/TextLink'
|
||||
import { TextInput } from '@/components/inputs'
|
||||
import { CodeEditor } from '@/components/inputs/CodeEditor'
|
||||
import { Select } from '@/components/inputs/Select'
|
||||
import { Stack, Text } from '@chakra-ui/react'
|
||||
import { isDefined, isEmpty } from '@typebot.io/lib'
|
||||
import {
|
||||
PixelBlock,
|
||||
pixelEventTypes,
|
||||
pixelObjectProperties,
|
||||
} from '@typebot.io/schemas'
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
const pixelReferenceUrl =
|
||||
'https://developers.facebook.com/docs/meta-pixel/reference#standard-events'
|
||||
|
||||
type Props = {
|
||||
options?: PixelBlock['options']
|
||||
onOptionsChange: (options: PixelBlock['options']) => void
|
||||
}
|
||||
|
||||
type Item = NonNullable<PixelBlock['options']['params']>[number]
|
||||
|
||||
export const PixelSettings = ({ options, onOptionsChange }: Props) => {
|
||||
const updatePixelId = (pixelId: string) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
pixelId: isEmpty(pixelId) ? undefined : pixelId,
|
||||
})
|
||||
|
||||
const updateIsTrackingEventEnabled = (isChecked: boolean) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
params: isChecked && !options?.params ? [] : undefined,
|
||||
})
|
||||
|
||||
const updateEventType = (
|
||||
_: string | undefined,
|
||||
eventType?: (typeof pixelEventTypes)[number] | 'Custom'
|
||||
) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
params: [],
|
||||
eventType,
|
||||
})
|
||||
|
||||
const updateParams = (params: PixelBlock['options']['params']) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
params,
|
||||
})
|
||||
|
||||
const updateEventName = (name: string) => {
|
||||
if (options?.eventType !== 'Custom') return
|
||||
onOptionsChange({
|
||||
...options,
|
||||
name: isEmpty(name) ? undefined : name,
|
||||
})
|
||||
}
|
||||
|
||||
const Item = useMemo(
|
||||
() =>
|
||||
function Component(props: TableListItemProps<Item>) {
|
||||
return <ParamItem {...props} eventType={options?.eventType} />
|
||||
},
|
||||
[options?.eventType]
|
||||
)
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<TextInput
|
||||
defaultValue={options?.pixelId ?? ''}
|
||||
onChange={updatePixelId}
|
||||
withVariableButton={false}
|
||||
placeholder='Pixel ID (e.g. "123456789")'
|
||||
/>
|
||||
<SwitchWithRelatedSettings
|
||||
label={'Track event'}
|
||||
initialValue={isDefined(options?.params)}
|
||||
onCheckChange={updateIsTrackingEventEnabled}
|
||||
>
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
Read the{' '}
|
||||
<TextLink href={pixelReferenceUrl} isExternal>
|
||||
reference
|
||||
</TextLink>{' '}
|
||||
to better understand the available options.
|
||||
</Text>
|
||||
<Select
|
||||
items={['Custom', ...pixelEventTypes] as const}
|
||||
selectedItem={options?.eventType}
|
||||
placeholder="Select event type"
|
||||
onSelect={updateEventType}
|
||||
/>
|
||||
{options?.eventType === 'Custom' && (
|
||||
<TextInput
|
||||
defaultValue={options.name ?? ''}
|
||||
onChange={updateEventName}
|
||||
placeholder="Event name"
|
||||
/>
|
||||
)}
|
||||
{options?.eventType &&
|
||||
(options.eventType === 'Custom' ||
|
||||
pixelObjectProperties.filter((prop) =>
|
||||
prop.associatedEvents.includes(options.eventType)
|
||||
).length > 0) && (
|
||||
<TableList
|
||||
initialItems={options?.params ?? []}
|
||||
Item={Item}
|
||||
onItemsChange={updateParams}
|
||||
addLabel="Add parameter"
|
||||
/>
|
||||
)}
|
||||
</SwitchWithRelatedSettings>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
type ParamItemProps = {
|
||||
item: Item
|
||||
eventType: 'Custom' | (typeof pixelEventTypes)[number] | undefined
|
||||
onItemChange: (item: Item) => void
|
||||
}
|
||||
|
||||
const ParamItem = ({ item, eventType, onItemChange }: ParamItemProps) => {
|
||||
const possibleObjectProps =
|
||||
eventType && eventType !== 'Custom'
|
||||
? pixelObjectProperties.filter((prop) =>
|
||||
prop.associatedEvents.includes(eventType)
|
||||
)
|
||||
: []
|
||||
|
||||
const currentObject = possibleObjectProps.find(
|
||||
(prop) => prop.key === item.key
|
||||
)
|
||||
|
||||
const updateKey = (key: string) =>
|
||||
onItemChange({
|
||||
...item,
|
||||
key,
|
||||
})
|
||||
|
||||
const updateValue = (value: string) =>
|
||||
onItemChange({
|
||||
...item,
|
||||
value,
|
||||
})
|
||||
|
||||
if (!eventType) return null
|
||||
|
||||
return (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
{eventType === 'Custom' ? (
|
||||
<TextInput
|
||||
defaultValue={item.key}
|
||||
onChange={updateKey}
|
||||
placeholder="Key"
|
||||
/>
|
||||
) : (
|
||||
<DropdownList
|
||||
currentItem={item.key}
|
||||
items={possibleObjectProps.map((prop) => prop.key)}
|
||||
onItemSelect={updateKey}
|
||||
placeholder="Select key"
|
||||
/>
|
||||
)}
|
||||
{currentObject?.type === 'code' ? (
|
||||
<CodeEditor
|
||||
lang={'javascript'}
|
||||
defaultValue={item.value}
|
||||
onChange={updateValue}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
defaultValue={item.value}
|
||||
onChange={updateValue}
|
||||
placeholder="Value"
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
|
||||
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
|
||||
test.describe('Pixel block', () => {
|
||||
test('its configuration should work', async ({ page }) => {
|
||||
const typebotId = createId()
|
||||
await createTypebots([
|
||||
{
|
||||
id: typebotId,
|
||||
...parseDefaultGroupWithBlock({
|
||||
type: IntegrationBlockType.PIXEL,
|
||||
options: {},
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('text=Configure...')
|
||||
await page.getByPlaceholder('Pixel ID (e.g. "123456789")').fill('pixelid')
|
||||
await expect(page.getByText('Init Pixel')).toBeVisible()
|
||||
await page.getByText('Track event').click()
|
||||
await page.getByPlaceholder('Select event type').click()
|
||||
await page.getByRole('menuitem', { name: 'Lead' }).click()
|
||||
await expect(page.getByText('Track "Lead"')).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Add parameter' }).click()
|
||||
await page.getByRole('button', { name: 'Select key' }).click()
|
||||
await page.getByRole('menuitem', { name: 'currency' }).click()
|
||||
await page.getByPlaceholder('Value').fill('USD')
|
||||
await page.getByRole('button', { name: 'Preview' }).click()
|
||||
await expect(
|
||||
page.getByText('Pixel is not enabled in Preview mode').nth(1)
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -39,6 +39,7 @@ import { SetVariableIcon } from '@/features/blocks/logic/setVariable/components/
|
||||
import { TypebotLinkIcon } from '@/features/blocks/logic/typebotLink/components/TypebotLinkIcon'
|
||||
import { AbTestIcon } from '@/features/blocks/logic/abTest/components/AbTestIcon'
|
||||
import { PictureChoiceIcon } from '@/features/blocks/inputs/pictureChoice/components/PictureChoiceIcon'
|
||||
import { PixelLogo } from '@/features/blocks/integrations/pixel/components/PixelLogo'
|
||||
|
||||
type BlockIconProps = { type: BlockType } & IconProps
|
||||
|
||||
@@ -115,6 +116,8 @@ export const BlockIcon = ({ type, ...props }: BlockIconProps): JSX.Element => {
|
||||
return <ChatwootLogo {...props} />
|
||||
case IntegrationBlockType.OPEN_AI:
|
||||
return <OpenAILogo fill={openAIColor} {...props} />
|
||||
case IntegrationBlockType.PIXEL:
|
||||
return <PixelLogo {...props} />
|
||||
case 'start':
|
||||
return <FlagIcon {...props} />
|
||||
}
|
||||
|
||||
@@ -79,5 +79,7 @@ export const BlockLabel = ({ type }: Props): JSX.Element => {
|
||||
return <Text fontSize="sm">Chatwoot</Text>
|
||||
case IntegrationBlockType.OPEN_AI:
|
||||
return <Text fontSize="sm">OpenAI</Text>
|
||||
case IntegrationBlockType.PIXEL:
|
||||
return <Text fontSize="sm">Pixel</Text>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import { GoogleAnalyticsNodeBody } from '@/features/blocks/integrations/googleAn
|
||||
import { ChatwootNodeBody } from '@/features/blocks/integrations/chatwoot/components/ChatwootNodeBody'
|
||||
import { AbTestNodeBody } from '@/features/blocks/logic/abTest/components/AbTestNodeBody'
|
||||
import { PictureChoiceNode } from '@/features/blocks/inputs/pictureChoice/components/PictureChoiceNode'
|
||||
import { PixelNodeBody } from '@/features/blocks/integrations/pixel/components/PixelNodeBody'
|
||||
|
||||
type Props = {
|
||||
block: Block | StartBlock
|
||||
@@ -194,6 +195,9 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
||||
/>
|
||||
)
|
||||
}
|
||||
case IntegrationBlockType.PIXEL: {
|
||||
return <PixelNodeBody options={block.options} />
|
||||
}
|
||||
case 'start': {
|
||||
return <Text>Start</Text>
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import { ChatwootSettings } from '@/features/blocks/integrations/chatwoot/compon
|
||||
import { AbTestSettings } from '@/features/blocks/logic/abTest/components/AbTestSettings'
|
||||
import { PictureChoiceSettings } from '@/features/blocks/inputs/pictureChoice/components/PictureChoiceSettings'
|
||||
import { SettingsHoverBar } from './SettingsHoverBar'
|
||||
import { PixelSettings } from '@/features/blocks/integrations/pixel/components/PixelSettings'
|
||||
|
||||
type Props = {
|
||||
block: BlockWithOptions
|
||||
@@ -311,5 +312,13 @@ export const BlockSettings = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
case IntegrationBlockType.PIXEL: {
|
||||
return (
|
||||
<PixelSettings
|
||||
options={block.options}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,5 +61,7 @@ export const getHelpDocUrl = (blockType: BlockWithOptions['type']): string => {
|
||||
return 'https://docs.typebot.io/editor/blocks/logic/abTest'
|
||||
case LogicBlockType.JUMP:
|
||||
return 'https://docs.typebot.io/editor/blocks/logic/jump'
|
||||
case IntegrationBlockType.PIXEL:
|
||||
return 'https://docs.typebot.io/editor/blocks/integrations/pixel'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ export const FlutterFlowLogo = (props: IconProps) => (
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M60.6347 0C62.6766 0 64.5668 1.10496 65.5244 2.90118C66.4419 4.6217 66.403 6.64918 65.4227 8.33205L65.3893 8.38864L56.7972 22.7617C55.8256 24.387 54.0715 25.4014 52.1968 25.423L52.1356 25.4234L42.3241 25.4233L46.4021 34.7593L46.4183 34.7867L46.4434 34.8308C47.4038 36.5666 47.3804 38.6304 46.3844 40.3403L46.3509 40.3972L37.7588 54.7702C36.7872 56.3955 35.0332 57.4099 33.1585 57.4315L33.0973 57.4319L21.0549 57.4318L12.3978 66.5627L12.3814 66.5797C11.5668 67.4078 10.4661 67.8698 9.31846 67.8698C9.03158 67.8698 8.74452 67.8408 8.46053 67.7829C7.06066 67.4971 5.89759 66.5365 5.33774 65.2117L5.32195 65.1739L0.766133 54.7156L0.742168 54.6755L0.717276 54.6324C0.713141 54.625 0.708936 54.6175 0.704544 54.6095C-0.255836 52.8739 -0.232574 50.8101 0.763263 49.1001L0.796818 49.0432L9.13021 35.1029L2.98975 21.0452L3.10227 20.9879L3.10057 20.9794C2.84075 19.6863 3.0499 18.3378 3.71716 17.1567L3.75401 17.0925L3.78807 17.0347L12.3802 2.66159C13.3519 1.03641 15.106 0.0219807 16.9806 0H17.0418H60.6347ZM15.5195 57.3226L6.39039 57.3224L9.0717 63.525L9.07829 63.5415C9.12274 63.6526 9.19463 63.7134 9.30164 63.7354C9.39709 63.7551 9.47432 63.7348 9.54736 63.6682L9.56013 63.6558L15.5195 57.3226ZM41.6168 35.9985H14.0216C13.9912 35.9985 13.9607 35.9995 13.9303 36.0016L13.9219 36.0022L21.4883 53.3099H33.0959C33.5731 53.3099 34.0327 53.0544 34.2941 52.6441L34.3148 52.6105L42.9193 38.2287C43.176 37.7997 43.2044 37.3019 43.001 36.854C42.7665 36.3375 42.2221 35.9985 41.6168 35.9985ZM11.1531 39.5525L4.32399 51.0629C4.05225 51.5211 4.0365 52.0576 4.27984 52.5298L4.30565 52.5778L4.3194 52.6013L4.34697 52.6464L4.40158 52.7276L4.46255 52.8069L4.52088 52.8735L4.531 52.8843L4.57045 52.9243L4.60472 52.9569C4.81954 53.1519 5.08509 53.2709 5.37798 53.3015L5.44378 53.3071L5.48188 53.3091L5.53015 53.3099H17.1171L11.1531 39.5525ZM9.35736 25.4512L12.3566 32.3299L12.4151 32.3099C12.933 32.1366 13.4746 32.0427 14.022 32.0341L14.1042 32.0335L40.8529 32.0334L37.9829 25.4513L9.35736 25.4512ZM60.5585 4.01257H32.8649L40.4335 21.3239H52.037C52.515 21.3239 52.975 21.0683 53.2365 20.6578L53.2573 20.6242L61.8623 6.24261C62.1189 5.81371 62.1473 5.31633 61.9441 4.8688C61.7144 4.36289 61.1876 4.02709 60.5971 4.01303L60.5585 4.01257ZM28.6254 4.01257H17.0861C16.6116 4.01257 16.1539 4.26756 15.8936 4.67744L15.8729 4.71101L7.29049 19.0929C7.03426 19.5222 7.00595 20.0207 7.20912 20.4692C7.43797 20.9746 7.96188 21.3095 8.54887 21.3235L8.58723 21.3239H36.1742L28.6254 4.01257Z"
|
||||
fill="#4B39EF"
|
||||
/>
|
||||
|
||||
@@ -27,6 +27,6 @@ test.describe.parallel('Templates page', () => {
|
||||
await page.click('text=Customer Support')
|
||||
await expect(page.locator('text=How can I help you?')).toBeVisible()
|
||||
await page.click('text=Use this template')
|
||||
await expect(page).toHaveURL(new RegExp(`/edit`))
|
||||
await expect(page).toHaveURL(new RegExp(`/edit`), { timeout: 10000 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -141,6 +141,8 @@ const parseDefaultBlockOptions = (type: BlockWithOptionsType): BlockOptions => {
|
||||
return defaultChatwootOptions
|
||||
case IntegrationBlockType.OPEN_AI:
|
||||
return {}
|
||||
case IntegrationBlockType.PIXEL:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user