Add Meta Pixel block

Closes #582
This commit is contained in:
Baptiste Arnaud
2023-06-28 09:52:03 +02:00
parent 92f7f3cbe2
commit 033f8f99dd
39 changed files with 826 additions and 38 deletions

View File

@@ -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>
)

View File

@@ -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>
)

View File

@@ -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>
)
}

View File

@@ -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()
})
})