feat(integration): ✨ Add Google Analytics integration
This commit is contained in:
@ -244,3 +244,103 @@ export const GoogleSheetsLogo = (props: IconProps) => (
|
||||
</g>
|
||||
</Icon>
|
||||
)
|
||||
|
||||
export const GoogleAnalyticsLogo = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 353 353" {...props}>
|
||||
<g clipPath="url(#clip0_1458_69)">
|
||||
<path
|
||||
d="M324.433 0H260.155C244.607 0 231.844 12.773 231.844 28.3329V111.474H138.792C123.709 111.474 111.41 123.782 111.41 139.11V232.237H27.6395C12.3241 232.237 0.0253906 244.545 0.0253906 259.873V324.899C0.0253906 340.227 12.3241 352.536 27.6395 353H324.665C340.212 353 352.975 340.227 352.975 324.667V28.3329C352.743 12.773 339.98 0 324.433 0Z"
|
||||
fill="url(#paint0_linear_1458_69)"
|
||||
/>
|
||||
<path
|
||||
d="M324.433 0H260.155C244.607 0 231.844 12.773 231.844 28.3329V111.474H138.792C123.709 111.474 111.41 123.782 111.41 139.11V232.237H27.6395C12.3241 232.237 0.0253906 244.545 0.0253906 259.873V324.899C0.0253906 340.227 12.3241 352.536 27.6395 353H324.665C340.212 353 352.975 340.227 352.975 324.667V28.3329C352.743 12.773 339.98 0 324.433 0Z"
|
||||
fill="url(#paint1_linear_1458_69)"
|
||||
/>
|
||||
<path
|
||||
d="M324.433 0H260.619C245.071 0 232.309 12.773 232.309 28.3329V353H324.433C339.98 353 352.743 340.227 352.743 324.667V28.3329C352.743 12.773 339.98 0 324.433 0Z"
|
||||
fill="#F57C00"
|
||||
/>
|
||||
<path
|
||||
d="M111.41 139.342V232.237H27.8715C12.5561 232.237 0.0253906 244.778 0.0253906 260.105V325.132C0.0253906 340.459 12.5561 353 27.8715 353H232.076V111.474H139.256C123.941 111.474 111.41 124.014 111.41 139.342Z"
|
||||
fill="#FFC107"
|
||||
/>
|
||||
<path
|
||||
d="M232.076 111.474V353H324.2C339.748 353 352.511 340.227 352.511 324.667V232.237L232.076 111.474Z"
|
||||
fill="url(#paint2_linear_1458_69)"
|
||||
/>
|
||||
<path
|
||||
opacity="0.2"
|
||||
d="M139.256 113.796H232.077V111.474H139.256C123.941 111.474 111.41 124.014 111.41 139.342V141.664C111.41 126.337 123.941 113.796 139.256 113.796Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
opacity="0.2"
|
||||
d="M27.8715 234.56H111.41V232.237H27.8715C12.5561 232.237 0.0253906 244.778 0.0253906 260.106V262.428C0.0253906 247.1 12.5561 234.56 27.8715 234.56Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
opacity="0.2"
|
||||
d="M324.433 0H260.619C245.071 0 232.309 12.773 232.309 28.3329V30.6553C232.309 15.0954 245.071 2.32237 260.619 2.32237H324.433C339.98 2.32237 352.743 15.0954 352.743 30.6553V28.3329C352.743 12.773 339.98 0 324.433 0Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
opacity="0.2"
|
||||
d="M324.433 350.678H27.8715C12.5561 350.678 0.0253906 338.137 0.0253906 322.809V325.132C0.0253906 340.459 12.5561 353 27.8715 353H324.201C339.748 353 352.511 340.227 352.511 324.667V322.345C352.743 337.905 339.98 350.678 324.433 350.678V350.678Z"
|
||||
fill="#BF360C"
|
||||
/>
|
||||
<path
|
||||
d="M324.433 0H260.619C245.071 0 232.309 12.773 232.309 28.3329V111.474H139.488C124.173 111.474 111.642 124.014 111.642 139.342V232.237H27.8715C12.5561 232.237 0.0253906 244.778 0.0253906 260.105V325.132C0.0253906 340.459 12.5561 353 27.8715 353H324.433C339.98 353 352.743 340.227 352.743 324.667V28.3329C352.743 12.773 339.98 0 324.433 0Z"
|
||||
fill="url(#paint3_linear_1458_69)"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1458_69"
|
||||
x1="0.0253906"
|
||||
y1="176.5"
|
||||
x2="352.975"
|
||||
y2="176.5"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="white" stopOpacity="0.1" />
|
||||
<stop offset="1" stopColor="white" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_1458_69"
|
||||
x1="0.0253906"
|
||||
y1="176.5"
|
||||
x2="352.975"
|
||||
y2="176.5"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="white" stopOpacity="0.1" />
|
||||
<stop offset="1" stopColor="white" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_1458_69"
|
||||
x1="172.323"
|
||||
y1="172.436"
|
||||
x2="344.434"
|
||||
y2="344.409"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#BF360C" stopOpacity="0.2" />
|
||||
<stop offset="1" stopColor="#BF360C" stopOpacity="0.02" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint3_linear_1458_69"
|
||||
x1="118.3"
|
||||
y1="118.513"
|
||||
x2="346.649"
|
||||
y2="346.679"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="white" stopOpacity="0.1" />
|
||||
<stop offset="1" stopColor="white" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1458_69">
|
||||
<rect width="353" height="353" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</Icon>
|
||||
)
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
PhoneIcon,
|
||||
TextIcon,
|
||||
} from 'assets/icons'
|
||||
import { GoogleSheetsLogo } from 'assets/logos'
|
||||
import { GoogleAnalyticsLogo, GoogleSheetsLogo } from 'assets/logos'
|
||||
import {
|
||||
BubbleStepType,
|
||||
InputStepType,
|
||||
@ -48,6 +48,8 @@ export const StepIcon = ({ type, ...props }: StepIconProps) => {
|
||||
return <FilterIcon {...props} />
|
||||
case IntegrationStepType.GOOGLE_SHEETS:
|
||||
return <GoogleSheetsLogo {...props} />
|
||||
case IntegrationStepType.GOOGLE_ANALYTICS:
|
||||
return <GoogleAnalyticsLogo {...props} />
|
||||
case 'start':
|
||||
return <FlagIcon {...props} />
|
||||
default:
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { Text, Tooltip } from '@chakra-ui/react'
|
||||
import {
|
||||
BubbleStepType,
|
||||
InputStepType,
|
||||
@ -41,7 +41,18 @@ export const StepTypeLabel = ({ type }: Props) => {
|
||||
return <Text>Condition</Text>
|
||||
}
|
||||
case IntegrationStepType.GOOGLE_SHEETS: {
|
||||
return <Text>Sheets</Text>
|
||||
return (
|
||||
<Tooltip label="Google Sheets">
|
||||
<Text>Sheets</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
case IntegrationStepType.GOOGLE_ANALYTICS: {
|
||||
return (
|
||||
<Tooltip label="Google Analytics">
|
||||
<Text>Analytics</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
return <></>
|
||||
|
@ -4,9 +4,7 @@ import {
|
||||
PopoverBody,
|
||||
useEventListener,
|
||||
Portal,
|
||||
Stack,
|
||||
IconButton,
|
||||
Flex,
|
||||
} from '@chakra-ui/react'
|
||||
import { ExpandIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
@ -28,6 +26,7 @@ import {
|
||||
} from './bodies'
|
||||
import { ChoiceInputSettingsBody } from './bodies/ChoiceInputSettingsBody'
|
||||
import { ConditionSettingsBody } from './bodies/ConditionSettingsBody'
|
||||
import { GoogleAnalyticsSettings } from './bodies/GoogleAnalyticsSettings'
|
||||
import { GoogleSheetsSettingsBody } from './bodies/GoogleSheetsSettingsBody'
|
||||
import { PhoneNumberSettingsBody } from './bodies/PhoneNumberSettingsBody'
|
||||
import { SetVariableSettingsBody } from './bodies/SetVariableSettingsBody'
|
||||
@ -47,7 +46,7 @@ export const SettingsPopoverContent = ({ step, onExpandClick }: Props) => {
|
||||
useEventListener('wheel', handleMouseWheel, ref.current)
|
||||
return (
|
||||
<Portal>
|
||||
<PopoverContent onMouseDown={handleMouseDown}>
|
||||
<PopoverContent onMouseDown={handleMouseDown} pos="relative">
|
||||
<PopoverArrow />
|
||||
<PopoverBody
|
||||
px="6"
|
||||
@ -58,18 +57,17 @@ export const SettingsPopoverContent = ({ step, onExpandClick }: Props) => {
|
||||
ref={ref}
|
||||
shadow="lg"
|
||||
>
|
||||
<Stack>
|
||||
<Flex justifyContent="flex-end">
|
||||
<IconButton
|
||||
aria-label="expand"
|
||||
icon={<ExpandIcon />}
|
||||
size="xs"
|
||||
onClick={onExpandClick}
|
||||
/>
|
||||
</Flex>
|
||||
<StepSettings step={step} />
|
||||
</Stack>
|
||||
<StepSettings step={step} />
|
||||
</PopoverBody>
|
||||
<IconButton
|
||||
pos="absolute"
|
||||
top="5px"
|
||||
right="5px"
|
||||
aria-label="expand"
|
||||
icon={<ExpandIcon />}
|
||||
size="xs"
|
||||
onClick={onExpandClick}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
)
|
||||
@ -162,6 +160,14 @@ export const StepSettings = ({ step }: { step: Step }) => {
|
||||
/>
|
||||
)
|
||||
}
|
||||
case IntegrationStepType.GOOGLE_ANALYTICS: {
|
||||
return (
|
||||
<GoogleAnalyticsSettings
|
||||
options={step.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
return <></>
|
||||
}
|
||||
|
@ -0,0 +1,121 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
FormLabel,
|
||||
Stack,
|
||||
Tag,
|
||||
} from '@chakra-ui/react'
|
||||
import { DebouncedInput } from 'components/shared/DebouncedInput'
|
||||
import { InputWithVariableButton } from 'components/shared/InputWithVariableButton'
|
||||
import { GoogleAnalyticsOptions } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
options?: GoogleAnalyticsOptions
|
||||
onOptionsChange: (options: GoogleAnalyticsOptions) => void
|
||||
}
|
||||
|
||||
export const GoogleAnalyticsSettings = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
}: Props) => {
|
||||
const handleTrackingIdChange = (trackingId: string) =>
|
||||
onOptionsChange({ ...options, trackingId })
|
||||
|
||||
const handleCategoryChange = (category: string) =>
|
||||
onOptionsChange({ ...options, category })
|
||||
|
||||
const handleActionChange = (action: string) =>
|
||||
onOptionsChange({ ...options, action })
|
||||
|
||||
const handleLabelChange = (label: string) =>
|
||||
onOptionsChange({ ...options, label })
|
||||
|
||||
const handleValueChange = (value?: string) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
value: value ? parseFloat(value) : undefined,
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="tracking-id">
|
||||
Tracking ID:
|
||||
</FormLabel>
|
||||
<DebouncedInput
|
||||
id="tracking-id"
|
||||
initialValue={options?.trackingId ?? ''}
|
||||
placeholder="G-123456..."
|
||||
delay={100}
|
||||
onChange={handleTrackingIdChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="category">
|
||||
Event category:
|
||||
</FormLabel>
|
||||
<InputWithVariableButton
|
||||
id="category"
|
||||
initialValue={options?.category ?? ''}
|
||||
placeholder="Example: Typebot"
|
||||
delay={100}
|
||||
onChange={handleCategoryChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="action">
|
||||
Event action:
|
||||
</FormLabel>
|
||||
<InputWithVariableButton
|
||||
id="action"
|
||||
initialValue={options?.action ?? ''}
|
||||
placeholder="Example: Submit email"
|
||||
delay={100}
|
||||
onChange={handleActionChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Accordion allowToggle>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex="1" textAlign="left">
|
||||
Advanced
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="label">
|
||||
Event label <Tag>Optional</Tag>:
|
||||
</FormLabel>
|
||||
<InputWithVariableButton
|
||||
id="label"
|
||||
initialValue={options?.label ?? ''}
|
||||
placeholder="Example: Campaign Z"
|
||||
delay={100}
|
||||
onChange={handleLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="value">
|
||||
Event value <Tag>Optional</Tag>:
|
||||
</FormLabel>
|
||||
<InputWithVariableButton
|
||||
id="value"
|
||||
initialValue={options?.value?.toString() ?? ''}
|
||||
placeholder="Example: 0"
|
||||
onChange={handleValueChange}
|
||||
/>
|
||||
</Stack>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react'
|
||||
import { PlusIcon, TrashIcon } from 'assets/icons'
|
||||
import { DropdownList } from 'components/shared/DropdownList'
|
||||
import { InputWithVariable } from 'components/shared/InputWithVariable'
|
||||
import { InputWithVariableButton } from 'components/shared/InputWithVariableButton'
|
||||
import { Cell, Table } from 'models'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Sheet } from 'services/integrations'
|
||||
@ -131,9 +131,9 @@ export const CellWithValueStack = ({
|
||||
items={columns}
|
||||
placeholder="Select a column"
|
||||
/>
|
||||
<InputWithVariable
|
||||
<InputWithVariableButton
|
||||
initialValue={cell.value ?? ''}
|
||||
onValueChange={handleValueChange}
|
||||
onChange={handleValueChange}
|
||||
placeholder="Type a value..."
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -41,7 +41,7 @@ export const SourceEndpoint = ({
|
||||
align="center"
|
||||
{...props}
|
||||
>
|
||||
<Box bgColor="gray.400" rounded="full" boxSize="7px" />
|
||||
<Box bgColor="gray.400" rounded="full" boxSize="6px" />
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
@ -89,6 +89,11 @@ export const StepNodeContent = ({ step }: Props) => {
|
||||
if (!step.options) return <Text color={'gray.500'}>Configure...</Text>
|
||||
return <Text>{step.options?.action}</Text>
|
||||
}
|
||||
case IntegrationStepType.GOOGLE_ANALYTICS: {
|
||||
if (!step.options || !step.options.action)
|
||||
return <Text color={'gray.500'}>Configure...</Text>
|
||||
return <Text>Track "{step.options?.action}"</Text>
|
||||
}
|
||||
case 'start': {
|
||||
return <Text>{step.label}</Text>
|
||||
}
|
||||
|
@ -14,23 +14,23 @@ import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { VariableSearchInput } from './VariableSearchInput'
|
||||
|
||||
export const InputWithVariable = ({
|
||||
export const InputWithVariableButton = ({
|
||||
initialValue,
|
||||
noAbsolute,
|
||||
onValueChange,
|
||||
onChange,
|
||||
delay,
|
||||
...props
|
||||
}: {
|
||||
initialValue: string
|
||||
onValueChange: (value: string) => void
|
||||
noAbsolute?: boolean
|
||||
} & InputProps) => {
|
||||
onChange: (value: string) => void
|
||||
delay?: number
|
||||
} & Omit<InputProps, 'onChange'>) => {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [debouncedValue] = useDebounce(value, 100)
|
||||
const [debouncedValue] = useDebounce(value, delay ?? 100)
|
||||
const [carretPosition, setCarretPosition] = useState<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
onValueChange(debouncedValue)
|
||||
onChange(debouncedValue)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedValue])
|
||||
|
||||
@ -77,10 +77,7 @@ export const InputWithVariable = ({
|
||||
{...props}
|
||||
bgColor={'white'}
|
||||
/>
|
||||
<InputRightElement
|
||||
pos={noAbsolute ? 'relative' : 'absolute'}
|
||||
zIndex={noAbsolute ? 'unset' : '1'}
|
||||
>
|
||||
<InputRightElement>
|
||||
<Popover matchWidth isLazy>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
@ -9,6 +9,7 @@ import {
|
||||
Button,
|
||||
InputProps,
|
||||
IconButton,
|
||||
Portal,
|
||||
} from '@chakra-ui/react'
|
||||
import { PlusIcon, TrashIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
@ -110,60 +111,62 @@ export const VariableSearchInput = ({
|
||||
{...inputProps}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
maxH="35vh"
|
||||
overflowY="scroll"
|
||||
spacing="0"
|
||||
role="menu"
|
||||
w="inherit"
|
||||
shadow="lg"
|
||||
>
|
||||
{(inputValue?.length ?? 0) > 0 &&
|
||||
!isDefined(variables.find((v) => v.name === inputValue)) && (
|
||||
<Button
|
||||
role="menuitem"
|
||||
minH="40px"
|
||||
onClick={handleCreateNewVariableClick}
|
||||
fontSize="16px"
|
||||
fontWeight="normal"
|
||||
rounded="none"
|
||||
colorScheme="gray"
|
||||
variant="ghost"
|
||||
justifyContent="flex-start"
|
||||
leftIcon={<PlusIcon />}
|
||||
>
|
||||
Create "{inputValue}"
|
||||
</Button>
|
||||
<Portal>
|
||||
<PopoverContent
|
||||
maxH="35vh"
|
||||
overflowY="scroll"
|
||||
spacing="0"
|
||||
role="menu"
|
||||
w="inherit"
|
||||
shadow="lg"
|
||||
>
|
||||
{(inputValue?.length ?? 0) > 0 &&
|
||||
!isDefined(variables.find((v) => v.name === inputValue)) && (
|
||||
<Button
|
||||
role="menuitem"
|
||||
minH="40px"
|
||||
onClick={handleCreateNewVariableClick}
|
||||
fontSize="16px"
|
||||
fontWeight="normal"
|
||||
rounded="none"
|
||||
colorScheme="gray"
|
||||
variant="ghost"
|
||||
justifyContent="flex-start"
|
||||
leftIcon={<PlusIcon />}
|
||||
>
|
||||
Create "{inputValue}"
|
||||
</Button>
|
||||
)}
|
||||
{filteredItems.length > 0 && (
|
||||
<>
|
||||
{filteredItems.map((item, idx) => {
|
||||
return (
|
||||
<Button
|
||||
role="menuitem"
|
||||
minH="40px"
|
||||
key={idx}
|
||||
onClick={handleVariableNameClick(item)}
|
||||
fontSize="16px"
|
||||
fontWeight="normal"
|
||||
rounded="none"
|
||||
colorScheme="gray"
|
||||
variant="ghost"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
{item.name}
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove variable"
|
||||
size="xs"
|
||||
onClick={handleDeleteVariableClick(item)}
|
||||
/>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{filteredItems.length > 0 && (
|
||||
<>
|
||||
{filteredItems.map((item, idx) => {
|
||||
return (
|
||||
<Button
|
||||
role="menuitem"
|
||||
minH="40px"
|
||||
key={idx}
|
||||
onClick={handleVariableNameClick(item)}
|
||||
fontSize="16px"
|
||||
fontWeight="normal"
|
||||
rounded="none"
|
||||
colorScheme="gray"
|
||||
variant="ghost"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
{item.name}
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove variable"
|
||||
size="xs"
|
||||
onClick={handleDeleteVariableClick(item)}
|
||||
/>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
</Popover>
|
||||
</Flex>
|
||||
)
|
||||
|
@ -1 +1,51 @@
|
||||
import { Step, InputStepType } from 'models'
|
||||
import { parseTestTypebot } from './utils'
|
||||
|
||||
export const userIds = ['user1', 'user2']
|
||||
|
||||
export const createTypebotWithStep = (step: Omit<Step, 'id' | 'blockId'>) => {
|
||||
cy.task(
|
||||
'createTypebot',
|
||||
parseTestTypebot({
|
||||
id: 'typebot3',
|
||||
name: 'Typebot #3',
|
||||
ownerId: userIds[1],
|
||||
steps: {
|
||||
byId: {
|
||||
step1: {
|
||||
...step,
|
||||
id: 'step1',
|
||||
blockId: 'block1',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
options:
|
||||
step.type === InputStepType.CHOICE
|
||||
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
{ itemIds: ['item1'] }
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
allIds: ['step1'],
|
||||
},
|
||||
blocks: {
|
||||
byId: {
|
||||
block1: {
|
||||
id: 'block1',
|
||||
graphCoordinates: { x: 400, y: 200 },
|
||||
title: 'Block #1',
|
||||
stepIds: ['step1'],
|
||||
},
|
||||
},
|
||||
allIds: ['block1'],
|
||||
},
|
||||
choiceItems:
|
||||
step.type === InputStepType.CHOICE
|
||||
? {
|
||||
byId: { item1: { stepId: 'step1', id: 'item1' } },
|
||||
allIds: ['item1'],
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,7 @@
|
||||
import { userIds } from 'cypress/plugins/data'
|
||||
import {
|
||||
parseTestTypebot,
|
||||
preventUserFromRefreshing,
|
||||
} from 'cypress/plugins/utils'
|
||||
import { createTypebotWithStep } from 'cypress/plugins/data'
|
||||
import { preventUserFromRefreshing } from 'cypress/plugins/utils'
|
||||
import { getIframeBody } from 'cypress/support'
|
||||
import { InputStep, InputStepType } from 'models'
|
||||
import { InputStepType } from 'models'
|
||||
|
||||
describe('Text input', () => {
|
||||
beforeEach(() => {
|
||||
@ -262,50 +259,3 @@ describe('Button input', () => {
|
||||
getIframeBody().findByText('Cool!').should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
const createTypebotWithStep = (step: Omit<InputStep, 'id' | 'blockId'>) => {
|
||||
cy.task(
|
||||
'createTypebot',
|
||||
parseTestTypebot({
|
||||
id: 'typebot3',
|
||||
name: 'Typebot #3',
|
||||
ownerId: userIds[1],
|
||||
steps: {
|
||||
byId: {
|
||||
step1: {
|
||||
...step,
|
||||
id: 'step1',
|
||||
blockId: 'block1',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
options:
|
||||
step.type === InputStepType.CHOICE
|
||||
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
{ itemIds: ['item1'] }
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
allIds: ['step1'],
|
||||
},
|
||||
blocks: {
|
||||
byId: {
|
||||
block1: {
|
||||
id: 'block1',
|
||||
graphCoordinates: { x: 400, y: 200 },
|
||||
title: 'Block #1',
|
||||
stepIds: ['step1'],
|
||||
},
|
||||
},
|
||||
allIds: ['block1'],
|
||||
},
|
||||
choiceItems:
|
||||
step.type === InputStepType.CHOICE
|
||||
? {
|
||||
byId: { item1: { stepId: 'step1', id: 'item1' } },
|
||||
allIds: ['item1'],
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
36
apps/builder/cypress/tests/integrations/googleAnalytics.ts
Normal file
36
apps/builder/cypress/tests/integrations/googleAnalytics.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { createTypebotWithStep } from 'cypress/plugins/data'
|
||||
import { preventUserFromRefreshing } from 'cypress/plugins/utils'
|
||||
import { IntegrationStepType } from 'models'
|
||||
|
||||
describe('Google Analytics', () => {
|
||||
beforeEach(() => {
|
||||
cy.task('seed')
|
||||
createTypebotWithStep({ type: IntegrationStepType.GOOGLE_ANALYTICS })
|
||||
cy.signOut()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cy.window().then((win) => {
|
||||
win.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
})
|
||||
})
|
||||
|
||||
it.only('can be filled correctly', () => {
|
||||
cy.signIn('test2@gmail.com')
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.intercept({
|
||||
url: '/g/collect',
|
||||
method: 'POST',
|
||||
}).as('gaRequest')
|
||||
cy.findByTestId('step-step1').click()
|
||||
cy.findByRole('textbox', { name: 'Tracking ID:' }).type('G-VWX9WG1TNS')
|
||||
cy.findByRole('textbox', { name: 'Event category:' }).type('Typebot')
|
||||
cy.findByRole('textbox', { name: 'Event action:' }).type('Submit email')
|
||||
cy.findByRole('button', { name: 'Advanced' }).click()
|
||||
cy.findByRole('textbox', { name: 'Event label Optional :' }).type(
|
||||
'Campaign Z'
|
||||
)
|
||||
cy.findByRole('textbox', { name: 'Event value Optional :' }).type('20')
|
||||
// Not sure how to test if GA integration works correctly in the preview tab
|
||||
})
|
||||
})
|
37
packages/bot-engine/lib/gtag.ts
Normal file
37
packages/bot-engine/lib/gtag.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { GoogleAnalyticsOptions } from 'models'
|
||||
|
||||
declare const gtag: any
|
||||
|
||||
const initGoogleAnalytics = (id: string): Promise<void> =>
|
||||
new Promise((resolve) => {
|
||||
const existingScript = document.getElementById('gtag')
|
||||
if (!existingScript) {
|
||||
const script = document.createElement('script')
|
||||
script.src = `https://www.googletagmanager.com/gtag/js?id=${id}`
|
||||
script.id = 'gtag'
|
||||
const initScript = document.createElement('script')
|
||||
initScript.innerHTML = `window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', '${id}');
|
||||
`
|
||||
document.body.appendChild(script)
|
||||
document.body.appendChild(initScript)
|
||||
script.onload = () => {
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
if (existingScript) resolve()
|
||||
})
|
||||
|
||||
export const sendGaEvent = (options: GoogleAnalyticsOptions) => {
|
||||
if (!options) return
|
||||
gtag('event', options.action, {
|
||||
event_category: options.category,
|
||||
event_label: options.label,
|
||||
value: options.value,
|
||||
})
|
||||
}
|
||||
|
||||
export default initGoogleAnalytics
|
@ -26,7 +26,8 @@ export const HostMessageBubble = ({
|
||||
const [isTyping, setIsTyping] = useState(true)
|
||||
|
||||
const content = useMemo(
|
||||
() => parseVariables(step.content.html, typebot.variables),
|
||||
() =>
|
||||
parseVariables({ text: step.content.html, variables: typebot.variables }),
|
||||
[typebot.variables]
|
||||
)
|
||||
|
||||
|
@ -9,10 +9,12 @@ import {
|
||||
GoogleSheetsUpdateRowOptions,
|
||||
Cell,
|
||||
GoogleSheetsGetOptions,
|
||||
GoogleAnalyticsStep,
|
||||
} from 'models'
|
||||
import { stringify } from 'qs'
|
||||
import { sendRequest } from 'utils'
|
||||
import { parseVariables } from './variable'
|
||||
import { sendGaEvent } from '../../lib/gtag'
|
||||
import { parseVariables, parseVariablesInObject } from './variable'
|
||||
|
||||
export const executeIntegration = (
|
||||
step: IntegrationStep,
|
||||
@ -22,9 +24,21 @@ export const executeIntegration = (
|
||||
switch (step.type) {
|
||||
case IntegrationStepType.GOOGLE_SHEETS:
|
||||
return executeGoogleSheetIntegration(step, variables, updateVariableValue)
|
||||
case IntegrationStepType.GOOGLE_ANALYTICS:
|
||||
return executeGoogleAnalyticsIntegration(step, variables)
|
||||
}
|
||||
}
|
||||
|
||||
export const executeGoogleAnalyticsIntegration = async (
|
||||
step: GoogleAnalyticsStep,
|
||||
variables: Table<Variable>
|
||||
) => {
|
||||
if (!step.options?.trackingId) return
|
||||
const { default: initGoogleAnalytics } = await import('../../lib/gtag')
|
||||
await initGoogleAnalytics(step.options.trackingId)
|
||||
sendGaEvent(parseVariablesInObject(step.options, variables))
|
||||
}
|
||||
|
||||
const executeGoogleSheetIntegration = async (
|
||||
step: GoogleSheetsStep,
|
||||
variables: Table<Variable>,
|
||||
@ -73,7 +87,10 @@ const updateRowInGoogleSheets = async (
|
||||
values: parseCellValues(options.cellsToUpsert, variables),
|
||||
referenceCell: {
|
||||
column: options.referenceCell.column,
|
||||
value: parseVariables(options.referenceCell.value ?? '', variables),
|
||||
value: parseVariables({
|
||||
text: options.referenceCell.value ?? '',
|
||||
variables,
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -90,7 +107,10 @@ const getRowFromGoogleSheets = async (
|
||||
credentialsId: options.credentialsId,
|
||||
referenceCell: {
|
||||
column: options.referenceCell.column,
|
||||
value: parseVariables(options.referenceCell.value ?? '', variables),
|
||||
value: parseVariables({
|
||||
text: options.referenceCell.value ?? '',
|
||||
variables,
|
||||
}),
|
||||
},
|
||||
columns: options.cellsToExtract.allIds.map(
|
||||
(id) => options.cellsToExtract?.byId[id].column
|
||||
@ -117,5 +137,8 @@ const parseCellValues = (
|
||||
const cell = cells.byId[id]
|
||||
return !cell.column || !cell.value
|
||||
? row
|
||||
: { ...row, [cell.column]: parseVariables(cell.value, variables) }
|
||||
: {
|
||||
...row,
|
||||
[cell.column]: parseVariables({ text: cell.value, variables }),
|
||||
}
|
||||
}, {})
|
||||
|
@ -22,7 +22,7 @@ export const executeLogic = (
|
||||
return
|
||||
const expression = step.options.expressionToEvaluate
|
||||
const evaluatedExpression = isMathFormula(expression)
|
||||
? evaluateExpression(parseVariables(expression, variables))
|
||||
? evaluateExpression(parseVariables({ text: expression, variables }))
|
||||
: expression
|
||||
updateVariableValue(step.options.variableId, evaluatedExpression)
|
||||
return
|
||||
|
@ -6,11 +6,14 @@ const safeEval = eval
|
||||
export const stringContainsVariable = (str: string): boolean =>
|
||||
/\{\{(.*?)\}\}/g.test(str)
|
||||
|
||||
export const parseVariables = (
|
||||
text: string,
|
||||
export const parseVariables = ({
|
||||
text,
|
||||
variables,
|
||||
}: {
|
||||
text?: string
|
||||
variables: Table<Variable>
|
||||
): string => {
|
||||
if (text === '') return text
|
||||
}): string => {
|
||||
if (!text || text === '') return ''
|
||||
return text.replace(/\{\{(.*?)\}\}/g, (_, fullVariableString) => {
|
||||
const matchedVarName = fullVariableString.replace(/{{|}}/g, '')
|
||||
const matchedVariableId = variables.allIds.find((variableId) => {
|
||||
@ -44,3 +47,18 @@ const countDecimals = (value: number) => {
|
||||
if (value % 1 != 0) return value.toString().split('.')[1].length
|
||||
return 0
|
||||
}
|
||||
|
||||
export const parseVariablesInObject = (
|
||||
object: { [key: string]: string | number },
|
||||
variables: Table<Variable>
|
||||
) =>
|
||||
Object.keys(object).reduce((newObj, key) => {
|
||||
const currentValue = object[key]
|
||||
return {
|
||||
...newObj,
|
||||
[key]:
|
||||
typeof currentValue === 'string'
|
||||
? parseVariables({ text: currentValue, variables })
|
||||
: currentValue,
|
||||
}
|
||||
}, {})
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { StepBase } from '.'
|
||||
import { Table } from '../..'
|
||||
|
||||
export type IntegrationStep = GoogleSheetsStep
|
||||
export type IntegrationStep = GoogleSheetsStep | GoogleAnalyticsStep
|
||||
|
||||
export type IntegrationStepOptions = GoogleSheetsOptions
|
||||
export type IntegrationStepOptions =
|
||||
| GoogleSheetsOptions
|
||||
| GoogleAnalyticsOptions
|
||||
|
||||
export enum IntegrationStepType {
|
||||
GOOGLE_SHEETS = 'Google Sheets',
|
||||
GOOGLE_ANALYTICS = 'Google Analytics',
|
||||
}
|
||||
|
||||
export type GoogleSheetsStep = StepBase & {
|
||||
@ -14,6 +17,19 @@ export type GoogleSheetsStep = StepBase & {
|
||||
options?: GoogleSheetsOptions
|
||||
}
|
||||
|
||||
export type GoogleAnalyticsStep = StepBase & {
|
||||
type: IntegrationStepType.GOOGLE_ANALYTICS
|
||||
options?: GoogleAnalyticsOptions
|
||||
}
|
||||
|
||||
export type GoogleAnalyticsOptions = {
|
||||
trackingId?: string
|
||||
category?: string
|
||||
action?: string
|
||||
label?: string
|
||||
value?: number
|
||||
}
|
||||
|
||||
export enum GoogleSheetsAction {
|
||||
GET = 'Get data from sheet',
|
||||
INSERT_ROW = 'Insert a row',
|
||||
|
@ -73,4 +73,4 @@ export const isConditionStep = (step: Step): step is ConditionStep =>
|
||||
step.type === LogicStepType.CONDITION
|
||||
|
||||
export const isIntegrationStep = (step: Step): step is IntegrationStep =>
|
||||
step.type === IntegrationStepType.GOOGLE_SHEETS
|
||||
(Object.values(IntegrationStepType) as string[]).includes(step.type)
|
||||
|
Reference in New Issue
Block a user