2
0

♻️ (builder) Change to features-centric folder structure

This commit is contained in:
Baptiste Arnaud
2022-11-15 09:35:48 +01:00
committed by Baptiste Arnaud
parent 3686465a85
commit 643571fe7d
683 changed files with 3907 additions and 3643 deletions

View File

@ -0,0 +1,178 @@
import { IconProps, Icon } from '@chakra-ui/react'
export const GoogleSheetsLogo = (props: IconProps) => (
<Icon viewBox="0 0 49 67" {...props}>
<title>Sheets-icon</title>
<desc>Created with Sketch.</desc>
<defs>
<path
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
id="path-1"
></path>
<path
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
id="path-3"
></path>
<path
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
id="path-5"
></path>
<linearGradient
x1="50.0053945%"
y1="8.58610612%"
x2="50.0053945%"
y2="100.013939%"
id="linearGradient-7"
>
<stop stopColor="#263238" stopOpacity="0.2" offset="0%"></stop>
<stop stopColor="#263238" stopOpacity="0.02" offset="100%"></stop>
</linearGradient>
<path
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
id="path-8"
></path>
<path
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
id="path-10"
></path>
<path
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
id="path-12"
></path>
<path
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
id="path-14"
></path>
<radialGradient
cx="3.16804688%"
cy="2.71744318%"
fx="3.16804688%"
fy="2.71744318%"
r="161.248516%"
gradientTransform="translate(0.031680,0.027174),scale(1.000000,0.727273),translate(-0.031680,-0.027174)"
id="radialGradient-16"
>
<stop stopColor="#FFFFFF" stopOpacity="0.1" offset="0%"></stop>
<stop stopColor="#FFFFFF" stopOpacity="0" offset="100%"></stop>
</radialGradient>
</defs>
<g id="Page-1" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g
id="Consumer-Apps-Sheets-Large-VD-R8-"
transform="translate(-451.000000, -451.000000)"
>
<g id="Hero" transform="translate(0.000000, 63.000000)">
<g id="Personal" transform="translate(277.000000, 299.000000)">
<g id="Sheets-icon" transform="translate(174.833333, 89.958333)">
<g id="Group">
<g id="Clipped">
<mask id="mask-2" fill="white">
<use xlinkHref="#path-1"></use>
</mask>
<g id="SVGID_1_"></g>
<path
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L36.9791667,10.3541667 L29.5833333,0 Z"
id="Path"
fill="#0F9D58"
fillRule="nonzero"
mask="url(#mask-2)"
></path>
</g>
<g id="Clipped">
<mask id="mask-4" fill="white">
<use xlinkHref="#path-3"></use>
</mask>
<g id="SVGID_1_"></g>
<path
d="M11.8333333,31.8020833 L11.8333333,53.25 L35.5,53.25 L35.5,31.8020833 L11.8333333,31.8020833 Z M22.1875,50.2916667 L14.7916667,50.2916667 L14.7916667,46.59375 L22.1875,46.59375 L22.1875,50.2916667 Z M22.1875,44.375 L14.7916667,44.375 L14.7916667,40.6770833 L22.1875,40.6770833 L22.1875,44.375 Z M22.1875,38.4583333 L14.7916667,38.4583333 L14.7916667,34.7604167 L22.1875,34.7604167 L22.1875,38.4583333 Z M32.5416667,50.2916667 L25.1458333,50.2916667 L25.1458333,46.59375 L32.5416667,46.59375 L32.5416667,50.2916667 Z M32.5416667,44.375 L25.1458333,44.375 L25.1458333,40.6770833 L32.5416667,40.6770833 L32.5416667,44.375 Z M32.5416667,38.4583333 L25.1458333,38.4583333 L25.1458333,34.7604167 L32.5416667,34.7604167 L32.5416667,38.4583333 Z"
id="Shape"
fill="#F1F1F1"
fillRule="nonzero"
mask="url(#mask-4)"
></path>
</g>
<g id="Clipped">
<mask id="mask-6" fill="white">
<use xlinkHref="#path-5"></use>
</mask>
<g id="SVGID_1_"></g>
<polygon
id="Path"
fill="url(#linearGradient-7)"
fillRule="nonzero"
mask="url(#mask-6)"
points="30.8813021 16.4520313 47.3333333 32.9003646 47.3333333 17.75"
></polygon>
</g>
<g id="Clipped">
<mask id="mask-9" fill="white">
<use xlinkHref="#path-8"></use>
</mask>
<g id="SVGID_1_"></g>
<g id="Group" mask="url(#mask-9)">
<g transform="translate(26.625000, -2.958333)">
<path
d="M2.95833333,2.95833333 L2.95833333,16.2708333 C2.95833333,18.7225521 4.94411458,20.7083333 7.39583333,20.7083333 L20.7083333,20.7083333 L2.95833333,2.95833333 Z"
id="Path"
fill="#87CEAC"
fillRule="nonzero"
></path>
</g>
</g>
</g>
<g id="Clipped">
<mask id="mask-11" fill="white">
<use xlinkHref="#path-10"></use>
</mask>
<g id="SVGID_1_"></g>
<path
d="M4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,4.80729167 C0,2.36666667 1.996875,0.369791667 4.4375,0.369791667 L29.5833333,0.369791667 L29.5833333,0 L4.4375,0 Z"
id="Path"
fillOpacity="0.2"
fill="#FFFFFF"
fillRule="nonzero"
mask="url(#mask-11)"
></path>
</g>
<g id="Clipped">
<mask id="mask-13" fill="white">
<use xlinkHref="#path-12"></use>
</mask>
<g id="SVGID_1_"></g>
<path
d="M42.8958333,64.7135417 L4.4375,64.7135417 C1.996875,64.7135417 0,62.7166667 0,60.2760417 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,60.2760417 C47.3333333,62.7166667 45.3364583,64.7135417 42.8958333,64.7135417 Z"
id="Path"
fillOpacity="0.2"
fill="#263238"
fillRule="nonzero"
mask="url(#mask-13)"
></path>
</g>
<g id="Clipped">
<mask id="mask-15" fill="white">
<use xlinkHref="#path-14"></use>
</mask>
<g id="SVGID_1_"></g>
<path
d="M34.0208333,17.75 C31.5691146,17.75 29.5833333,15.7642188 29.5833333,13.3125 L29.5833333,13.6822917 C29.5833333,16.1340104 31.5691146,18.1197917 34.0208333,18.1197917 L47.3333333,18.1197917 L47.3333333,17.75 L34.0208333,17.75 Z"
id="Path"
fillOpacity="0.1"
fill="#263238"
fillRule="nonzero"
mask="url(#mask-15)"
></path>
</g>
</g>
<path
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
id="Path"
fill="url(#radialGradient-16)"
fillRule="nonzero"
></path>
</g>
</g>
</g>
</g>
</g>
</Icon>
)

View File

@ -0,0 +1,13 @@
import React from 'react'
import { Text } from '@chakra-ui/react'
import { GoogleSheetsAction } from 'models'
type Props = {
action?: GoogleSheetsAction
}
export const GoogleSheetsNodeContent = ({ action }: Props) => (
<Text color={action ? 'currentcolor' : 'gray.500'} noOfLines={1}>
{action ?? 'Configure...'}
</Text>
)

View File

@ -0,0 +1,35 @@
import { Stack } from '@chakra-ui/react'
import { DropdownList } from '@/components/DropdownList'
import { Cell } from 'models'
import { TableListItemProps } from '@/components/TableList'
import { Input } from '@/components/inputs'
export const CellWithValueStack = ({
item,
onItemChange,
columns,
}: TableListItemProps<Cell> & { columns: string[] }) => {
const handleColumnSelect = (column: string) => {
if (item.column === column) return
onItemChange({ ...item, column })
}
const handleValueChange = (value: string) => {
if (item.value === value) return
onItemChange({ ...item, value })
}
return (
<Stack p="4" rounded="md" flex="1" borderWidth="1px" w="full">
<DropdownList<string>
currentItem={item.column}
onItemSelect={handleColumnSelect}
items={columns}
placeholder="Select a column"
/>
<Input
defaultValue={item.value ?? ''}
onChange={handleValueChange}
placeholder="Type a value..."
/>
</Stack>
)
}

View File

@ -0,0 +1,37 @@
import { Stack } from '@chakra-ui/react'
import { DropdownList } from '@/components/DropdownList'
import { ExtractingCell, Variable } from 'models'
import { TableListItemProps } from '@/components/TableList'
import { VariableSearchInput } from '@/components/VariableSearchInput'
export const CellWithVariableIdStack = ({
item,
onItemChange,
columns,
}: TableListItemProps<ExtractingCell> & { columns: string[] }) => {
const handleColumnSelect = (column: string) => {
if (item.column === column) return
onItemChange({ ...item, column })
}
const handleVariableIdChange = (variable?: Variable) => {
if (item.variableId === variable?.id) return
onItemChange({ ...item, variableId: variable?.id })
}
return (
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
<DropdownList<string>
currentItem={item.column}
onItemSelect={handleColumnSelect}
items={columns}
placeholder="Select a column"
/>
<VariableSearchInput
initialVariableId={item.variableId}
onSelectVariable={handleVariableIdChange}
placeholder="Select a variable"
/>
</Stack>
)
}

View File

@ -0,0 +1,77 @@
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
Stack,
Text,
Image,
Button,
ModalFooter,
Flex,
} from '@chakra-ui/react'
import { useWorkspace } from '@/features/workspace'
import Link from 'next/link'
import React from 'react'
import { AlertInfo } from '@/components/AlertInfo'
import { GoogleLogo } from '@/components/GoogleLogo'
import { getGoogleSheetsConsentScreenUrlQuery } from '../../queries/getGoogleSheetsConsentScreenUrlQuery'
type Props = {
isOpen: boolean
blockId: string
onClose: () => void
}
export const GoogleSheetConnectModal = ({
blockId,
isOpen,
onClose,
}: Props) => {
const { workspace } = useWorkspace()
return (
<Modal isOpen={isOpen} onClose={onClose} size="lg">
<ModalOverlay />
<ModalContent>
<ModalHeader>Connect Spreadsheets</ModalHeader>
<ModalCloseButton />
<ModalBody as={Stack} spacing="6">
<AlertInfo>
Typebot needs access to Google Drive in order to list all your
spreadsheets. It also needs access to your spreadsheets in order to
fetch or inject data in it.
</AlertInfo>
<Text>
Make sure to check all the permissions so that the integration works
as expected:
</Text>
<Image
src="/images/google-spreadsheets-scopes.jpeg"
alt="Google Spreadsheets checkboxes"
/>
<Flex>
<Button
as={Link}
leftIcon={<GoogleLogo />}
data-testid="google"
isLoading={['loading', 'authenticated'].includes(status)}
variant="outline"
href={getGoogleSheetsConsentScreenUrlQuery(
window.location.href,
blockId,
workspace?.id
)}
mx="auto"
>
Continue with Google
</Button>
</Flex>
</ModalBody>
<ModalFooter />
</ModalContent>
</Modal>
)
}

View File

@ -0,0 +1,225 @@
import { Divider, Stack, Text, useDisclosure } from '@chakra-ui/react'
import { DropdownList } from '@/components/DropdownList'
import { useTypebot } from '@/features/editor'
import {
Cell,
CredentialsType,
ExtractingCell,
GoogleSheetsAction,
GoogleSheetsGetOptions,
GoogleSheetsInsertRowOptions,
GoogleSheetsOptions,
GoogleSheetsUpdateRowOptions,
} from 'models'
import React, { useMemo } from 'react'
import { isDefined, omit } from 'utils'
import { SheetsDropdown } from './SheetsDropdown'
import { SpreadsheetsDropdown } from './SpreadsheetDropdown'
import { CellWithValueStack } from './CellWithValueStack'
import { CellWithVariableIdStack } from './CellWithVariableIdStack'
import { GoogleSheetConnectModal } from './GoogleSheetsConnectModal'
import { TableListItemProps, TableList } from '@/components/TableList'
import { CredentialsDropdown } from '@/features/credentials'
import { useSheets } from '../../hooks/useSheets'
import { Sheet } from '../../types'
type Props = {
options: GoogleSheetsOptions
onOptionsChange: (options: GoogleSheetsOptions) => void
blockId: string
}
export const GoogleSheetsSettingsBody = ({
options,
onOptionsChange,
blockId,
}: Props) => {
const { save } = useTypebot()
const { sheets, isLoading } = useSheets({
credentialsId: options?.credentialsId,
spreadsheetId: options?.spreadsheetId,
})
const { isOpen, onOpen, onClose } = useDisclosure()
const sheet = useMemo(
() => sheets?.find((s) => s.id === options?.sheetId),
[sheets, options?.sheetId]
)
const handleCredentialsIdChange = (credentialsId?: string) =>
onOptionsChange({ ...omit(options, 'credentialsId'), credentialsId })
const handleSpreadsheetIdChange = (spreadsheetId: string) =>
onOptionsChange({ ...options, spreadsheetId })
const handleSheetIdChange = (sheetId: string) =>
onOptionsChange({ ...options, sheetId })
const handleActionChange = (action: GoogleSheetsAction) => {
switch (action) {
case GoogleSheetsAction.GET: {
const newOptions: GoogleSheetsGetOptions = {
...options,
action,
cellsToExtract: [],
}
return onOptionsChange({ ...newOptions })
}
case GoogleSheetsAction.INSERT_ROW: {
const newOptions: GoogleSheetsInsertRowOptions = {
...options,
action,
cellsToInsert: [],
}
return onOptionsChange({ ...newOptions })
}
case GoogleSheetsAction.UPDATE_ROW: {
const newOptions: GoogleSheetsUpdateRowOptions = {
...options,
action,
cellsToUpsert: [],
}
return onOptionsChange({ ...newOptions })
}
}
}
const handleCreateNewClick = async () => {
await save()
onOpen()
}
return (
<Stack>
<CredentialsDropdown
type={CredentialsType.GOOGLE_SHEETS}
currentCredentialsId={options?.credentialsId}
onCredentialsSelect={handleCredentialsIdChange}
onCreateNewClick={handleCreateNewClick}
/>
<GoogleSheetConnectModal
blockId={blockId}
isOpen={isOpen}
onClose={onClose}
/>
{options?.credentialsId && (
<SpreadsheetsDropdown
credentialsId={options.credentialsId}
spreadsheetId={options.spreadsheetId}
onSelectSpreadsheetId={handleSpreadsheetIdChange}
/>
)}
{options?.spreadsheetId && options.credentialsId && (
<SheetsDropdown
sheets={sheets ?? []}
isLoading={isLoading}
sheetId={options.sheetId}
onSelectSheetId={handleSheetIdChange}
/>
)}
{options?.spreadsheetId &&
options.credentialsId &&
isDefined(options.sheetId) && (
<>
<Divider />
<DropdownList<GoogleSheetsAction>
currentItem={'action' in options ? options.action : undefined}
onItemSelect={handleActionChange}
items={Object.values(GoogleSheetsAction)}
placeholder="Select an operation"
/>
</>
)}
{'action' in options && (
<ActionOptions
options={options}
sheet={sheet}
onOptionsChange={onOptionsChange}
/>
)}
</Stack>
)
}
const ActionOptions = ({
options,
sheet,
onOptionsChange,
}: {
options:
| GoogleSheetsGetOptions
| GoogleSheetsInsertRowOptions
| GoogleSheetsUpdateRowOptions
sheet?: Sheet
onOptionsChange: (options: GoogleSheetsOptions) => void
}) => {
const handleInsertColumnsChange = (cellsToInsert: Cell[]) =>
onOptionsChange({ ...options, cellsToInsert } as GoogleSheetsOptions)
const handleUpsertColumnsChange = (cellsToUpsert: Cell[]) =>
onOptionsChange({ ...options, cellsToUpsert } as GoogleSheetsOptions)
const handleReferenceCellChange = (referenceCell: Cell) =>
onOptionsChange({ ...options, referenceCell } as GoogleSheetsOptions)
const handleExtractingCellsChange = (cellsToExtract: ExtractingCell[]) =>
onOptionsChange({ ...options, cellsToExtract } as GoogleSheetsOptions)
const UpdatingCellItem = useMemo(
() => (props: TableListItemProps<Cell>) =>
<CellWithValueStack {...props} columns={sheet?.columns ?? []} />,
[sheet?.columns]
)
const ExtractingCellItem = useMemo(
() => (props: TableListItemProps<ExtractingCell>) =>
<CellWithVariableIdStack {...props} columns={sheet?.columns ?? []} />,
[sheet?.columns]
)
switch (options.action) {
case GoogleSheetsAction.INSERT_ROW:
return (
<TableList<Cell>
initialItems={options.cellsToInsert}
onItemsChange={handleInsertColumnsChange}
Item={UpdatingCellItem}
addLabel="Add a value"
/>
)
case GoogleSheetsAction.UPDATE_ROW:
return (
<Stack>
<Text>Row to select</Text>
<CellWithValueStack
columns={sheet?.columns ?? []}
item={options.referenceCell ?? { id: 'reference' }}
onItemChange={handleReferenceCellChange}
/>
<Text>Cells to update</Text>
<TableList<Cell>
initialItems={options.cellsToUpsert}
onItemsChange={handleUpsertColumnsChange}
Item={UpdatingCellItem}
addLabel="Add a value"
/>
</Stack>
)
case GoogleSheetsAction.GET:
return (
<Stack>
<Text>Row to select</Text>
<CellWithValueStack
columns={sheet?.columns ?? []}
item={options.referenceCell ?? { id: 'reference' }}
onItemChange={handleReferenceCellChange}
/>
<Text>Cells to extract</Text>
<TableList<ExtractingCell>
initialItems={options.cellsToExtract}
onItemsChange={handleExtractingCellsChange}
Item={ExtractingCellItem}
addLabel="Add a value"
/>
</Stack>
)
default:
return <></>
}
}

View File

@ -0,0 +1,50 @@
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { SearchableDropdown } from '@/components/SearchableDropdown'
import { HStack, Input } from '@chakra-ui/react'
import { useMemo } from 'react'
import { isDefined } from 'utils'
import { Sheet } from '../../types'
type Props = {
sheets: Sheet[]
isLoading: boolean
sheetId?: string
onSelectSheetId: (id: string) => void
}
export const SheetsDropdown = ({
sheets,
isLoading,
sheetId,
onSelectSheetId,
}: Props) => {
const currentSheet = useMemo(
() => sheets?.find((s) => s.id === sheetId),
[sheetId, sheets]
)
const handleSpreadsheetSelect = (name: string) => {
const id = sheets?.find((s) => s.name === name)?.id
if (isDefined(id)) onSelectSheetId(id)
}
if (isLoading) return <Input value="Loading..." isDisabled />
if (!sheets || sheets.length === 0)
return (
<HStack>
<Input value="No sheets found" isDisabled />
<MoreInfoTooltip>
Make sure your spreadsheet contains at least a sheet with a header
row.
</MoreInfoTooltip>
</HStack>
)
return (
<SearchableDropdown
selectedItem={currentSheet?.name}
items={(sheets ?? []).map((s) => s.name)}
onValueChange={handleSpreadsheetSelect}
placeholder={'Select the sheet'}
/>
)
}

View File

@ -0,0 +1,46 @@
import { SearchableDropdown } from '@/components/SearchableDropdown'
import { Input, Tooltip } from '@chakra-ui/react'
import { useMemo } from 'react'
import { useSpreadsheets } from '../../hooks/useSpreadsheets'
type Props = {
credentialsId: string
spreadsheetId?: string
onSelectSpreadsheetId: (id: string) => void
}
export const SpreadsheetsDropdown = ({
credentialsId,
spreadsheetId,
onSelectSpreadsheetId,
}: Props) => {
const { spreadsheets, isLoading } = useSpreadsheets({
credentialsId,
})
const currentSpreadsheet = useMemo(
() => spreadsheets?.find((s) => s.id === spreadsheetId),
[spreadsheetId, spreadsheets]
)
const handleSpreadsheetSelect = (name: string) => {
const id = spreadsheets?.find((s) => s.name === name)?.id
if (id) onSelectSpreadsheetId(id)
}
if (isLoading) return <Input value="Loading..." isDisabled />
if (!spreadsheets || spreadsheets.length === 0)
return (
<Tooltip label="No spreadsheets found, make sure you have at least one spreadsheet that contains a header row">
<span>
<Input value="No spreadsheets found" isDisabled />
</span>
</Tooltip>
)
return (
<SearchableDropdown
selectedItem={currentSpreadsheet?.name}
items={(spreadsheets ?? []).map((s) => s.name)}
onValueChange={handleSpreadsheetSelect}
placeholder={'Search for spreadsheet'}
/>
)
}

View File

@ -0,0 +1 @@
export { GoogleSheetsSettingsBody } from './GoogleSheetsSettingsBody'

View File

@ -0,0 +1,162 @@
import test, { expect, Page } from '@playwright/test'
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
import { typebotViewer } from 'utils/playwright/testHelpers'
import cuid from 'cuid'
import { getTestAsset } from '@/test/utils/playwright'
test.describe.parallel('Google sheets integration', () => {
test('Insert row should work', async ({ page }) => {
const typebotId = cuid()
await importTypebotInDatabase(
getTestAsset('typebots/integrations/googleSheets.json'),
{
id: typebotId,
}
)
await page.goto(`/typebots/${typebotId}/edit`)
await fillInSpreadsheetInfo(page)
await page.click('text=Select an operation')
await page.click('text=Insert a row')
await page.click('text=Add a value')
await page.click('text=Select a column')
await page.click('button >> text="Email"')
await page.click('[aria-label="Insert a variable"]')
await page.click('button >> text="Email" >> nth=1')
await page.click('text=Add a value')
await page.click('text=Select a column')
await page.click('text=First name')
await page.fill(
'input[placeholder="Type a value..."] >> nth = 1',
'Georges'
)
await page.click('text=Preview')
await typebotViewer(page)
.locator('input[placeholder="Type your email..."]')
.fill('georges@gmail.com')
await Promise.all([
page.waitForResponse(
(resp) =>
resp
.request()
.url()
.includes(
'/api/integrations/google-sheets/spreadsheets/1k_pIDw3YHl9tlZusbBVSBRY0PeRPd2H6t4Nj7rwnOtM/sheets/0'
) &&
resp.status() === 200 &&
resp.request().method() === 'POST'
),
typebotViewer(page)
.locator('input[placeholder="Type your email..."]')
.press('Enter'),
])
})
test('Update row should work', async ({ page }) => {
const typebotId = cuid()
await importTypebotInDatabase(
getTestAsset('typebots/integrations/googleSheets.json'),
{
id: typebotId,
}
)
await page.goto(`/typebots/${typebotId}/edit`)
await fillInSpreadsheetInfo(page)
await page.click('text=Select an operation')
await page.click('text=Update a row')
await page.click('text=Add a value')
await page.click('text=Select a column')
await page.click('button >> text="Email"')
await page.click('[aria-label="Insert a variable"]')
await page.click('button >> text="Email" >> nth=1')
await page.click('text=Add a value')
await page.click('text=Select a column')
await page.click('text=Last name')
await page.fill(
'input[placeholder="Type a value..."] >> nth = 1',
'Last name'
)
await page.click('text=Preview')
await typebotViewer(page)
.locator('input[placeholder="Type your email..."]')
.fill('test@test.com')
await Promise.all([
page.waitForResponse(
(resp) =>
resp
.request()
.url()
.includes(
'/api/integrations/google-sheets/spreadsheets/1k_pIDw3YHl9tlZusbBVSBRY0PeRPd2H6t4Nj7rwnOtM/sheets/0'
) &&
resp.status() === 200 &&
resp.request().method() === 'PATCH'
),
typebotViewer(page)
.locator('input[placeholder="Type your email..."]')
.press('Enter'),
])
})
test('Get row should work', async ({ page }) => {
const typebotId = cuid()
await importTypebotInDatabase(
getTestAsset('typebots/integrations/googleSheetsGet.json'),
{
id: typebotId,
}
)
await page.goto(`/typebots/${typebotId}/edit`)
await fillInSpreadsheetInfo(page)
await page.click('text=Select an operation')
await page.click('text=Get data from sheet')
await page.click('text=Select a column')
await page.click('button >> text="Email"')
await page.click('[aria-label="Insert a variable"]')
await page.click('button >> text="Email" >> nth=1')
await page.click('text=Add a value')
await page.click('text=Select a column')
await page.click('text="First name"')
await createNewVar(page, 'First name')
await page.click('text=Add a value')
await page.click('text=Select a column')
await page.click('text="Last name"')
await createNewVar(page, 'Last name')
await page.click('text=Preview')
await typebotViewer(page)
.locator('input[placeholder="Type your email..."]')
.fill('test2@test.com')
await typebotViewer(page)
.locator('input[placeholder="Type your email..."]')
.press('Enter')
await expect(
typebotViewer(page).locator('text=Your name is: John Smith')
).toBeVisible({ timeout: 30000 })
})
})
const fillInSpreadsheetInfo = async (page: Page) => {
await page.click('text=Configure...')
await page.click('text=Select an account')
await page.click('text=pro-user@email.com')
await page.fill('input[placeholder="Search for spreadsheet"]', 'CR')
await page.click('text=CRM')
await page.fill('input[placeholder="Select the sheet"]', 'Sh')
await page.click('text=Sheet1')
}
const createNewVar = async (page: Page, name: string) => {
await page.fill('input[placeholder="Select a variable"] >> nth=-1', name)
await page.click(`text=Create "${name}"`)
}

View File

@ -0,0 +1,28 @@
import { stringify } from 'qs'
import { fetcher } from '@/utils/helpers'
import useSWR from 'swr'
import { Sheet } from '../types'
export const useSheets = ({
credentialsId,
spreadsheetId,
onError,
}: {
credentialsId?: string
spreadsheetId?: string
onError?: (error: Error) => void
}) => {
const queryParams = stringify({ credentialsId })
const { data, error, mutate } = useSWR<{ sheets: Sheet[] }, Error>(
!credentialsId || !spreadsheetId
? null
: `/api/integrations/google-sheets/spreadsheets/${spreadsheetId}/sheets?${queryParams}`,
fetcher
)
if (error) onError && onError(error)
return {
sheets: data?.sheets,
isLoading: !error && !data,
mutate,
}
}

View File

@ -0,0 +1,24 @@
import { stringify } from 'qs'
import { fetcher } from '@/utils/helpers'
import useSWR from 'swr'
import { Spreadsheet } from '../types'
export const useSpreadsheets = ({
credentialsId,
onError,
}: {
credentialsId: string
onError?: (error: Error) => void
}) => {
const queryParams = stringify({ credentialsId })
const { data, error, mutate } = useSWR<{ files: Spreadsheet[] }, Error>(
`/api/integrations/google-sheets/spreadsheets?${queryParams}`,
fetcher
)
if (error) onError && onError(error)
return {
spreadsheets: data?.files,
isLoading: !error && !data,
mutate,
}
}

View File

@ -0,0 +1,3 @@
export { GoogleSheetsSettingsBody } from './components/GoogleSheetsSettingsBody'
export { GoogleSheetsNodeContent } from './components/GoogleSheetsNodeContent'
export { GoogleSheetsLogo } from './components/GoogleSheetsLogo'

View File

@ -0,0 +1,10 @@
import { stringify } from 'qs'
import { sendRequest } from 'utils'
export const createSheetsCredentialQuery = async (code: string) => {
const queryParams = stringify({ code })
return sendRequest({
url: `/api/credentials/google-sheets/callback?${queryParams}`,
method: 'GET',
})
}

View File

@ -0,0 +1,10 @@
import { stringify } from 'qs'
export const getGoogleSheetsConsentScreenUrlQuery = (
redirectUrl: string,
blockId: string,
workspaceId?: string
) => {
const queryParams = stringify({ redirectUrl, blockId, workspaceId })
return `/api/credentials/google-sheets/consent-url?${queryParams}`
}

View File

@ -0,0 +1,3 @@
export type Sheet = { id: string; name: string; columns: string[] }
export type Spreadsheet = { id: string; name: string }