feat(integration): ✨ Add Google Analytics integration
This commit is contained in:
@@ -244,3 +244,103 @@ export const GoogleSheetsLogo = (props: IconProps) => (
|
|||||||
</g>
|
</g>
|
||||||
</Icon>
|
</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,
|
PhoneIcon,
|
||||||
TextIcon,
|
TextIcon,
|
||||||
} from 'assets/icons'
|
} from 'assets/icons'
|
||||||
import { GoogleSheetsLogo } from 'assets/logos'
|
import { GoogleAnalyticsLogo, GoogleSheetsLogo } from 'assets/logos'
|
||||||
import {
|
import {
|
||||||
BubbleStepType,
|
BubbleStepType,
|
||||||
InputStepType,
|
InputStepType,
|
||||||
@@ -48,6 +48,8 @@ export const StepIcon = ({ type, ...props }: StepIconProps) => {
|
|||||||
return <FilterIcon {...props} />
|
return <FilterIcon {...props} />
|
||||||
case IntegrationStepType.GOOGLE_SHEETS:
|
case IntegrationStepType.GOOGLE_SHEETS:
|
||||||
return <GoogleSheetsLogo {...props} />
|
return <GoogleSheetsLogo {...props} />
|
||||||
|
case IntegrationStepType.GOOGLE_ANALYTICS:
|
||||||
|
return <GoogleAnalyticsLogo {...props} />
|
||||||
case 'start':
|
case 'start':
|
||||||
return <FlagIcon {...props} />
|
return <FlagIcon {...props} />
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Text } from '@chakra-ui/react'
|
import { Text, Tooltip } from '@chakra-ui/react'
|
||||||
import {
|
import {
|
||||||
BubbleStepType,
|
BubbleStepType,
|
||||||
InputStepType,
|
InputStepType,
|
||||||
@@ -41,7 +41,18 @@ export const StepTypeLabel = ({ type }: Props) => {
|
|||||||
return <Text>Condition</Text>
|
return <Text>Condition</Text>
|
||||||
}
|
}
|
||||||
case IntegrationStepType.GOOGLE_SHEETS: {
|
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: {
|
default: {
|
||||||
return <></>
|
return <></>
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import {
|
|||||||
PopoverBody,
|
PopoverBody,
|
||||||
useEventListener,
|
useEventListener,
|
||||||
Portal,
|
Portal,
|
||||||
Stack,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
Flex,
|
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { ExpandIcon } from 'assets/icons'
|
import { ExpandIcon } from 'assets/icons'
|
||||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||||
@@ -28,6 +26,7 @@ import {
|
|||||||
} from './bodies'
|
} from './bodies'
|
||||||
import { ChoiceInputSettingsBody } from './bodies/ChoiceInputSettingsBody'
|
import { ChoiceInputSettingsBody } from './bodies/ChoiceInputSettingsBody'
|
||||||
import { ConditionSettingsBody } from './bodies/ConditionSettingsBody'
|
import { ConditionSettingsBody } from './bodies/ConditionSettingsBody'
|
||||||
|
import { GoogleAnalyticsSettings } from './bodies/GoogleAnalyticsSettings'
|
||||||
import { GoogleSheetsSettingsBody } from './bodies/GoogleSheetsSettingsBody'
|
import { GoogleSheetsSettingsBody } from './bodies/GoogleSheetsSettingsBody'
|
||||||
import { PhoneNumberSettingsBody } from './bodies/PhoneNumberSettingsBody'
|
import { PhoneNumberSettingsBody } from './bodies/PhoneNumberSettingsBody'
|
||||||
import { SetVariableSettingsBody } from './bodies/SetVariableSettingsBody'
|
import { SetVariableSettingsBody } from './bodies/SetVariableSettingsBody'
|
||||||
@@ -47,7 +46,7 @@ export const SettingsPopoverContent = ({ step, onExpandClick }: Props) => {
|
|||||||
useEventListener('wheel', handleMouseWheel, ref.current)
|
useEventListener('wheel', handleMouseWheel, ref.current)
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
<PopoverContent onMouseDown={handleMouseDown}>
|
<PopoverContent onMouseDown={handleMouseDown} pos="relative">
|
||||||
<PopoverArrow />
|
<PopoverArrow />
|
||||||
<PopoverBody
|
<PopoverBody
|
||||||
px="6"
|
px="6"
|
||||||
@@ -58,18 +57,17 @@ export const SettingsPopoverContent = ({ step, onExpandClick }: Props) => {
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
shadow="lg"
|
shadow="lg"
|
||||||
>
|
>
|
||||||
<Stack>
|
<StepSettings step={step} />
|
||||||
<Flex justifyContent="flex-end">
|
</PopoverBody>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
pos="absolute"
|
||||||
|
top="5px"
|
||||||
|
right="5px"
|
||||||
aria-label="expand"
|
aria-label="expand"
|
||||||
icon={<ExpandIcon />}
|
icon={<ExpandIcon />}
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={onExpandClick}
|
onClick={onExpandClick}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
|
||||||
<StepSettings step={step} />
|
|
||||||
</Stack>
|
|
||||||
</PopoverBody>
|
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Portal>
|
</Portal>
|
||||||
)
|
)
|
||||||
@@ -162,6 +160,14 @@ export const StepSettings = ({ step }: { step: Step }) => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
case IntegrationStepType.GOOGLE_ANALYTICS: {
|
||||||
|
return (
|
||||||
|
<GoogleAnalyticsSettings
|
||||||
|
options={step.options}
|
||||||
|
onOptionsChange={handleOptionsChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return <></>
|
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 { Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react'
|
||||||
import { PlusIcon, TrashIcon } from 'assets/icons'
|
import { PlusIcon, TrashIcon } from 'assets/icons'
|
||||||
import { DropdownList } from 'components/shared/DropdownList'
|
import { DropdownList } from 'components/shared/DropdownList'
|
||||||
import { InputWithVariable } from 'components/shared/InputWithVariable'
|
import { InputWithVariableButton } from 'components/shared/InputWithVariableButton'
|
||||||
import { Cell, Table } from 'models'
|
import { Cell, Table } from 'models'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { Sheet } from 'services/integrations'
|
import { Sheet } from 'services/integrations'
|
||||||
@@ -131,9 +131,9 @@ export const CellWithValueStack = ({
|
|||||||
items={columns}
|
items={columns}
|
||||||
placeholder="Select a column"
|
placeholder="Select a column"
|
||||||
/>
|
/>
|
||||||
<InputWithVariable
|
<InputWithVariableButton
|
||||||
initialValue={cell.value ?? ''}
|
initialValue={cell.value ?? ''}
|
||||||
onValueChange={handleValueChange}
|
onChange={handleValueChange}
|
||||||
placeholder="Type a value..."
|
placeholder="Type a value..."
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const SourceEndpoint = ({
|
|||||||
align="center"
|
align="center"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Box bgColor="gray.400" rounded="full" boxSize="7px" />
|
<Box bgColor="gray.400" rounded="full" boxSize="6px" />
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,11 @@ export const StepNodeContent = ({ step }: Props) => {
|
|||||||
if (!step.options) return <Text color={'gray.500'}>Configure...</Text>
|
if (!step.options) return <Text color={'gray.500'}>Configure...</Text>
|
||||||
return <Text>{step.options?.action}</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': {
|
case 'start': {
|
||||||
return <Text>{step.label}</Text>
|
return <Text>{step.label}</Text>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,23 +14,23 @@ import React, { useEffect, useRef, useState } from 'react'
|
|||||||
import { useDebounce } from 'use-debounce'
|
import { useDebounce } from 'use-debounce'
|
||||||
import { VariableSearchInput } from './VariableSearchInput'
|
import { VariableSearchInput } from './VariableSearchInput'
|
||||||
|
|
||||||
export const InputWithVariable = ({
|
export const InputWithVariableButton = ({
|
||||||
initialValue,
|
initialValue,
|
||||||
noAbsolute,
|
onChange,
|
||||||
onValueChange,
|
delay,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
initialValue: string
|
initialValue: string
|
||||||
onValueChange: (value: string) => void
|
onChange: (value: string) => void
|
||||||
noAbsolute?: boolean
|
delay?: number
|
||||||
} & InputProps) => {
|
} & Omit<InputProps, 'onChange'>) => {
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const [value, setValue] = useState(initialValue)
|
const [value, setValue] = useState(initialValue)
|
||||||
const [debouncedValue] = useDebounce(value, 100)
|
const [debouncedValue] = useDebounce(value, delay ?? 100)
|
||||||
const [carretPosition, setCarretPosition] = useState<number>(0)
|
const [carretPosition, setCarretPosition] = useState<number>(0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onValueChange(debouncedValue)
|
onChange(debouncedValue)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [debouncedValue])
|
}, [debouncedValue])
|
||||||
|
|
||||||
@@ -77,10 +77,7 @@ export const InputWithVariable = ({
|
|||||||
{...props}
|
{...props}
|
||||||
bgColor={'white'}
|
bgColor={'white'}
|
||||||
/>
|
/>
|
||||||
<InputRightElement
|
<InputRightElement>
|
||||||
pos={noAbsolute ? 'relative' : 'absolute'}
|
|
||||||
zIndex={noAbsolute ? 'unset' : '1'}
|
|
||||||
>
|
|
||||||
<Popover matchWidth isLazy>
|
<Popover matchWidth isLazy>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
InputProps,
|
InputProps,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
Portal,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { PlusIcon, TrashIcon } from 'assets/icons'
|
import { PlusIcon, TrashIcon } from 'assets/icons'
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
@@ -110,6 +111,7 @@ export const VariableSearchInput = ({
|
|||||||
{...inputProps}
|
{...inputProps}
|
||||||
/>
|
/>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
<Portal>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
maxH="35vh"
|
maxH="35vh"
|
||||||
overflowY="scroll"
|
overflowY="scroll"
|
||||||
@@ -164,6 +166,7 @@ export const VariableSearchInput = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
|
</Portal>
|
||||||
</Popover>
|
</Popover>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1 +1,51 @@
|
|||||||
|
import { Step, InputStepType } from 'models'
|
||||||
|
import { parseTestTypebot } from './utils'
|
||||||
|
|
||||||
export const userIds = ['user1', 'user2']
|
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 { createTypebotWithStep } from 'cypress/plugins/data'
|
||||||
import {
|
import { preventUserFromRefreshing } from 'cypress/plugins/utils'
|
||||||
parseTestTypebot,
|
|
||||||
preventUserFromRefreshing,
|
|
||||||
} from 'cypress/plugins/utils'
|
|
||||||
import { getIframeBody } from 'cypress/support'
|
import { getIframeBody } from 'cypress/support'
|
||||||
import { InputStep, InputStepType } from 'models'
|
import { InputStepType } from 'models'
|
||||||
|
|
||||||
describe('Text input', () => {
|
describe('Text input', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -262,50 +259,3 @@ describe('Button input', () => {
|
|||||||
getIframeBody().findByText('Cool!').should('exist')
|
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 [isTyping, setIsTyping] = useState(true)
|
||||||
|
|
||||||
const content = useMemo(
|
const content = useMemo(
|
||||||
() => parseVariables(step.content.html, typebot.variables),
|
() =>
|
||||||
|
parseVariables({ text: step.content.html, variables: typebot.variables }),
|
||||||
[typebot.variables]
|
[typebot.variables]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import {
|
|||||||
GoogleSheetsUpdateRowOptions,
|
GoogleSheetsUpdateRowOptions,
|
||||||
Cell,
|
Cell,
|
||||||
GoogleSheetsGetOptions,
|
GoogleSheetsGetOptions,
|
||||||
|
GoogleAnalyticsStep,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import { stringify } from 'qs'
|
import { stringify } from 'qs'
|
||||||
import { sendRequest } from 'utils'
|
import { sendRequest } from 'utils'
|
||||||
import { parseVariables } from './variable'
|
import { sendGaEvent } from '../../lib/gtag'
|
||||||
|
import { parseVariables, parseVariablesInObject } from './variable'
|
||||||
|
|
||||||
export const executeIntegration = (
|
export const executeIntegration = (
|
||||||
step: IntegrationStep,
|
step: IntegrationStep,
|
||||||
@@ -22,9 +24,21 @@ export const executeIntegration = (
|
|||||||
switch (step.type) {
|
switch (step.type) {
|
||||||
case IntegrationStepType.GOOGLE_SHEETS:
|
case IntegrationStepType.GOOGLE_SHEETS:
|
||||||
return executeGoogleSheetIntegration(step, variables, updateVariableValue)
|
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 (
|
const executeGoogleSheetIntegration = async (
|
||||||
step: GoogleSheetsStep,
|
step: GoogleSheetsStep,
|
||||||
variables: Table<Variable>,
|
variables: Table<Variable>,
|
||||||
@@ -73,7 +87,10 @@ const updateRowInGoogleSheets = async (
|
|||||||
values: parseCellValues(options.cellsToUpsert, variables),
|
values: parseCellValues(options.cellsToUpsert, variables),
|
||||||
referenceCell: {
|
referenceCell: {
|
||||||
column: options.referenceCell.column,
|
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,
|
credentialsId: options.credentialsId,
|
||||||
referenceCell: {
|
referenceCell: {
|
||||||
column: options.referenceCell.column,
|
column: options.referenceCell.column,
|
||||||
value: parseVariables(options.referenceCell.value ?? '', variables),
|
value: parseVariables({
|
||||||
|
text: options.referenceCell.value ?? '',
|
||||||
|
variables,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
columns: options.cellsToExtract.allIds.map(
|
columns: options.cellsToExtract.allIds.map(
|
||||||
(id) => options.cellsToExtract?.byId[id].column
|
(id) => options.cellsToExtract?.byId[id].column
|
||||||
@@ -117,5 +137,8 @@ const parseCellValues = (
|
|||||||
const cell = cells.byId[id]
|
const cell = cells.byId[id]
|
||||||
return !cell.column || !cell.value
|
return !cell.column || !cell.value
|
||||||
? row
|
? row
|
||||||
: { ...row, [cell.column]: parseVariables(cell.value, variables) }
|
: {
|
||||||
|
...row,
|
||||||
|
[cell.column]: parseVariables({ text: cell.value, variables }),
|
||||||
|
}
|
||||||
}, {})
|
}, {})
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const executeLogic = (
|
|||||||
return
|
return
|
||||||
const expression = step.options.expressionToEvaluate
|
const expression = step.options.expressionToEvaluate
|
||||||
const evaluatedExpression = isMathFormula(expression)
|
const evaluatedExpression = isMathFormula(expression)
|
||||||
? evaluateExpression(parseVariables(expression, variables))
|
? evaluateExpression(parseVariables({ text: expression, variables }))
|
||||||
: expression
|
: expression
|
||||||
updateVariableValue(step.options.variableId, evaluatedExpression)
|
updateVariableValue(step.options.variableId, evaluatedExpression)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -6,11 +6,14 @@ const safeEval = eval
|
|||||||
export const stringContainsVariable = (str: string): boolean =>
|
export const stringContainsVariable = (str: string): boolean =>
|
||||||
/\{\{(.*?)\}\}/g.test(str)
|
/\{\{(.*?)\}\}/g.test(str)
|
||||||
|
|
||||||
export const parseVariables = (
|
export const parseVariables = ({
|
||||||
text: string,
|
text,
|
||||||
|
variables,
|
||||||
|
}: {
|
||||||
|
text?: string
|
||||||
variables: Table<Variable>
|
variables: Table<Variable>
|
||||||
): string => {
|
}): string => {
|
||||||
if (text === '') return text
|
if (!text || text === '') return ''
|
||||||
return text.replace(/\{\{(.*?)\}\}/g, (_, fullVariableString) => {
|
return text.replace(/\{\{(.*?)\}\}/g, (_, fullVariableString) => {
|
||||||
const matchedVarName = fullVariableString.replace(/{{|}}/g, '')
|
const matchedVarName = fullVariableString.replace(/{{|}}/g, '')
|
||||||
const matchedVariableId = variables.allIds.find((variableId) => {
|
const matchedVariableId = variables.allIds.find((variableId) => {
|
||||||
@@ -44,3 +47,18 @@ const countDecimals = (value: number) => {
|
|||||||
if (value % 1 != 0) return value.toString().split('.')[1].length
|
if (value % 1 != 0) return value.toString().split('.')[1].length
|
||||||
return 0
|
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 { StepBase } from '.'
|
||||||
import { Table } 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 {
|
export enum IntegrationStepType {
|
||||||
GOOGLE_SHEETS = 'Google Sheets',
|
GOOGLE_SHEETS = 'Google Sheets',
|
||||||
|
GOOGLE_ANALYTICS = 'Google Analytics',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GoogleSheetsStep = StepBase & {
|
export type GoogleSheetsStep = StepBase & {
|
||||||
@@ -14,6 +17,19 @@ export type GoogleSheetsStep = StepBase & {
|
|||||||
options?: GoogleSheetsOptions
|
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 {
|
export enum GoogleSheetsAction {
|
||||||
GET = 'Get data from sheet',
|
GET = 'Get data from sheet',
|
||||||
INSERT_ROW = 'Insert a row',
|
INSERT_ROW = 'Insert a row',
|
||||||
|
|||||||
@@ -73,4 +73,4 @@ export const isConditionStep = (step: Step): step is ConditionStep =>
|
|||||||
step.type === LogicStepType.CONDITION
|
step.type === LogicStepType.CONDITION
|
||||||
|
|
||||||
export const isIntegrationStep = (step: Step): step is IntegrationStep =>
|
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