♻️ (builder) Change to features-centric folder structure
This commit is contained in:
committed by
Baptiste Arnaud
parent
3686465a85
commit
643571fe7d
@ -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>
|
||||
)
|
@ -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>
|
||||
)
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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 <></>
|
||||
}
|
||||
}
|
@ -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'}
|
||||
/>
|
||||
)
|
||||
}
|
@ -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'}
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { GoogleSheetsSettingsBody } from './GoogleSheetsSettingsBody'
|
@ -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}"`)
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export { GoogleSheetsSettingsBody } from './components/GoogleSheetsSettingsBody'
|
||||
export { GoogleSheetsNodeContent } from './components/GoogleSheetsNodeContent'
|
||||
export { GoogleSheetsLogo } from './components/GoogleSheetsLogo'
|
@ -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',
|
||||
})
|
||||
}
|
@ -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}`
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export type Sheet = { id: string; name: string; columns: string[] }
|
||||
|
||||
export type Spreadsheet = { id: string; name: string }
|
Reference in New Issue
Block a user