feat(integration): ✨ Add Google Analytics integration
This commit is contained in:
@ -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>
|
||||
)
|
||||
|
Reference in New Issue
Block a user