2
0

feat(integration): Add Google Sheets integration

This commit is contained in:
Baptiste Arnaud
2022-01-18 18:25:18 +01:00
parent 2814a352b2
commit f49b5143cf
67 changed files with 2560 additions and 391 deletions

View File

@ -10,12 +10,19 @@ EMAIL_SERVER_HOST=smtp.example.com
EMAIL_SERVER_PORT=587 EMAIL_SERVER_PORT=587
EMAIL_FROM=noreply@example.com EMAIL_FROM=noreply@example.com
# AUTH # Storage
# Used for uploading images, videos, etc...
S3_UPLOAD_KEY=
S3_UPLOAD_SECRET=
S3_UPLOAD_REGION=
S3_UPLOAD_BUCKET=
# Auth
# (Optional) Used to login using GitHub # (Optional) Used to login using GitHub
GITHUB_CLIENT_ID= GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET= GITHUB_CLIENT_SECRET=
# (Optional) Used to login using Google # (Optional) Used to login using Google AND Google Sheets integration
GOOGLE_CLIENT_ID= GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET= GOOGLE_CLIENT_SECRET=
@ -27,9 +34,3 @@ FACEBOOK_CLIENT_SECRET=
NEXT_PUBLIC_STRIPE_PUBLIC_KEY= NEXT_PUBLIC_STRIPE_PUBLIC_KEY=
STRIPE_SECRET_KEY= STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET= STRIPE_WEBHOOK_SECRET=
# Used for uploading images, videos, etc...
S3_UPLOAD_KEY=
S3_UPLOAD_SECRET=
S3_UPLOAD_REGION=
S3_UPLOAD_BUCKET=

View File

@ -243,3 +243,10 @@ export const FilterIcon = (props: IconProps) => (
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon> <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
</Icon> </Icon>
) )
export const UserIcon = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</Icon>
)

View File

@ -67,3 +67,180 @@ export const FacebookLogo = (props: IconProps) => (
/> />
</Icon> </Icon>
) )
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

@ -1,5 +1,5 @@
import { Button, ButtonProps, Flex, HStack } from '@chakra-ui/react' import { Button, ButtonProps, Flex, HStack } from '@chakra-ui/react'
import { BubbleStepType, InputStepType, StepType, LogicStepType } from 'models' import { StepType, DraggableStepType } from 'models'
import { useDnd } from 'contexts/DndContext' import { useDnd } from 'contexts/DndContext'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { StepIcon } from './StepIcon' import { StepIcon } from './StepIcon'
@ -9,11 +9,8 @@ export const StepCard = ({
type, type,
onMouseDown, onMouseDown,
}: { }: {
type: BubbleStepType | InputStepType | LogicStepType type: DraggableStepType
onMouseDown: ( onMouseDown: (e: React.MouseEvent, type: DraggableStepType) => void
e: React.MouseEvent,
type: BubbleStepType | InputStepType | LogicStepType
) => void
}) => { }) => {
const { draggedStepType } = useDnd() const { draggedStepType } = useDnd()
const [isMouseDown, setIsMouseDown] = useState(false) const [isMouseDown, setIsMouseDown] = useState(false)

View File

@ -12,48 +12,45 @@ import {
PhoneIcon, PhoneIcon,
TextIcon, TextIcon,
} from 'assets/icons' } from 'assets/icons'
import { BubbleStepType, InputStepType, LogicStepType, StepType } from 'models' import { GoogleSheetsLogo } from 'assets/logos'
import {
BubbleStepType,
InputStepType,
IntegrationStepType,
LogicStepType,
StepType,
} from 'models'
import React from 'react' import React from 'react'
type StepIconProps = { type: StepType } & IconProps type StepIconProps = { type: StepType } & IconProps
export const StepIcon = ({ type, ...props }: StepIconProps) => { export const StepIcon = ({ type, ...props }: StepIconProps) => {
switch (type) { switch (type) {
case BubbleStepType.TEXT: { case BubbleStepType.TEXT:
return <ChatIcon {...props} /> return <ChatIcon {...props} />
} case InputStepType.TEXT:
case InputStepType.TEXT: {
return <TextIcon {...props} /> return <TextIcon {...props} />
} case InputStepType.NUMBER:
case InputStepType.NUMBER: {
return <NumberIcon {...props} /> return <NumberIcon {...props} />
} case InputStepType.EMAIL:
case InputStepType.EMAIL: {
return <EmailIcon {...props} /> return <EmailIcon {...props} />
} case InputStepType.URL:
case InputStepType.URL: {
return <GlobeIcon {...props} /> return <GlobeIcon {...props} />
} case InputStepType.DATE:
case InputStepType.DATE: {
return <CalendarIcon {...props} /> return <CalendarIcon {...props} />
} case InputStepType.PHONE:
case InputStepType.PHONE: {
return <PhoneIcon {...props} /> return <PhoneIcon {...props} />
} case InputStepType.CHOICE:
case InputStepType.CHOICE: {
return <CheckSquareIcon {...props} /> return <CheckSquareIcon {...props} />
} case LogicStepType.SET_VARIABLE:
case LogicStepType.SET_VARIABLE: {
return <EditIcon {...props} /> return <EditIcon {...props} />
} case LogicStepType.CONDITION:
case LogicStepType.CONDITION: {
return <FilterIcon {...props} /> return <FilterIcon {...props} />
} case IntegrationStepType.GOOGLE_SHEETS:
case 'start': { return <GoogleSheetsLogo {...props} />
case 'start':
return <FlagIcon {...props} /> return <FlagIcon {...props} />
} default:
default: {
return <></> return <></>
}
} }
} }

View File

@ -1,5 +1,11 @@
import { Text } from '@chakra-ui/react' import { Text } from '@chakra-ui/react'
import { BubbleStepType, InputStepType, LogicStepType, StepType } from 'models' import {
BubbleStepType,
InputStepType,
IntegrationStepType,
LogicStepType,
StepType,
} from 'models'
import React from 'react' import React from 'react'
type Props = { type: StepType } type Props = { type: StepType }
@ -34,6 +40,9 @@ export const StepTypeLabel = ({ type }: Props) => {
case LogicStepType.CONDITION: { case LogicStepType.CONDITION: {
return <Text>Condition</Text> return <Text>Condition</Text>
} }
case IntegrationStepType.GOOGLE_SHEETS: {
return <Text>Sheets</Text>
}
default: { default: {
return <></> return <></>
} }

View File

@ -5,7 +5,13 @@ import {
SimpleGrid, SimpleGrid,
useEventListener, useEventListener,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { BubbleStepType, InputStepType, LogicStepType } from 'models' import {
BubbleStepType,
DraggableStepType,
InputStepType,
IntegrationStepType,
LogicStepType,
} from 'models'
import { useDnd } from 'contexts/DndContext' import { useDnd } from 'contexts/DndContext'
import React, { useState } from 'react' import React, { useState } from 'react'
import { StepCard, StepCardOverlay } from './StepCard' import { StepCard, StepCardOverlay } from './StepCard'
@ -29,10 +35,7 @@ export const StepTypesList = () => {
} }
useEventListener('mousemove', handleMouseMove) useEventListener('mousemove', handleMouseMove)
const handleMouseDown = ( const handleMouseDown = (e: React.MouseEvent, type: DraggableStepType) => {
e: React.MouseEvent,
type: BubbleStepType | InputStepType | LogicStepType
) => {
const element = e.currentTarget as HTMLDivElement const element = e.currentTarget as HTMLDivElement
const rect = element.getBoundingClientRect() const rect = element.getBoundingClientRect()
const relativeX = e.clientX - rect.left const relativeX = e.clientX - rect.left
@ -94,6 +97,15 @@ export const StepTypesList = () => {
<StepCard key={type} type={type} onMouseDown={handleMouseDown} /> <StepCard key={type} type={type} onMouseDown={handleMouseDown} />
))} ))}
</SimpleGrid> </SimpleGrid>
<Text fontSize="sm" fontWeight="semibold" color="gray.600">
Integrations
</Text>
<SimpleGrid columns={2} spacing="2">
{Object.values(IntegrationStepType).map((type) => (
<StepCard key={type} type={type} onMouseDown={handleMouseDown} />
))}
</SimpleGrid>
{draggedStepType && ( {draggedStepType && (
<StepCardOverlay <StepCardOverlay
type={draggedStepType} type={draggedStepType}

View File

@ -3,17 +3,16 @@ import {
PopoverArrow, PopoverArrow,
PopoverBody, PopoverBody,
useEventListener, useEventListener,
Portal,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { import {
ChoiceInputOptions,
ConditionOptions,
InputStep, InputStep,
InputStepType, InputStepType,
IntegrationStepType,
LogicStepType, LogicStepType,
SetVariableOptions,
Step, Step,
TextInputOptions, StepOptions,
} from 'models' } from 'models'
import { useRef } from 'react' import { useRef } from 'react'
import { import {
@ -25,6 +24,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 { GoogleSheetsSettingsBody } from './bodies/GoogleSheetsSettingsBody'
import { PhoneNumberSettingsBody } from './bodies/PhoneNumberSettingsBody' import { PhoneNumberSettingsBody } from './bodies/PhoneNumberSettingsBody'
import { SetVariableSettingsBody } from './bodies/SetVariableSettingsBody' import { SetVariableSettingsBody } from './bodies/SetVariableSettingsBody'
@ -41,24 +41,21 @@ export const SettingsPopoverContent = ({ step }: Props) => {
} }
useEventListener('wheel', handleMouseWheel, ref.current) useEventListener('wheel', handleMouseWheel, ref.current)
return ( return (
<PopoverContent onMouseDown={handleMouseDown}> <Portal>
<PopoverArrow /> <PopoverContent onMouseDown={handleMouseDown}>
<PopoverBody p="6" overflowY="scroll" maxH="400px" ref={ref}> <PopoverArrow />
<SettingsPopoverBodyContent step={step} /> <PopoverBody p="6" overflowY="scroll" maxH="400px" ref={ref}>
</PopoverBody> <SettingsPopoverBodyContent step={step} />
</PopoverContent> </PopoverBody>
</PopoverContent>
</Portal>
) )
} }
const SettingsPopoverBodyContent = ({ step }: Props) => { const SettingsPopoverBodyContent = ({ step }: Props) => {
const { updateStep } = useTypebot() const { updateStep } = useTypebot()
const handleOptionsChange = ( const handleOptionsChange = (options: StepOptions) =>
options: updateStep(step.id, { options } as Partial<InputStep>)
| TextInputOptions
| ChoiceInputOptions
| SetVariableOptions
| ConditionOptions
) => updateStep(step.id, { options } as Partial<InputStep>)
switch (step.type) { switch (step.type) {
case InputStepType.TEXT: { case InputStepType.TEXT: {
@ -133,6 +130,15 @@ const SettingsPopoverBodyContent = ({ step }: Props) => {
/> />
) )
} }
case IntegrationStepType.GOOGLE_SHEETS: {
return (
<GoogleSheetsSettingsBody
options={step.options}
onOptionsChange={handleOptionsChange}
stepId={step.id}
/>
)
}
default: { default: {
return <></> return <></>
} }

View File

@ -0,0 +1,136 @@
import { Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react'
import { PlusIcon, TrashIcon } from 'assets/icons'
import { DropdownList } from 'components/shared/DropdownList'
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
import { ExtractingCell, Table, Variable } from 'models'
import React, { useEffect, useState } from 'react'
import { Sheet } from 'services/integrations'
import { generate } from 'short-uuid'
import { useImmer } from 'use-immer'
type Props = {
sheet: Sheet
initialCells?: Table<ExtractingCell>
onCellsChange: (cells: Table<ExtractingCell>) => void
}
const id = generate()
const defaultCells: Table<ExtractingCell> = {
byId: { [id]: {} },
allIds: [id],
}
export const ExtractCellList = ({
sheet,
initialCells,
onCellsChange,
}: Props) => {
const [cells, setCells] = useImmer(initialCells ?? defaultCells)
const [showDeleteId, setShowDeleteId] = useState<string | undefined>()
useEffect(() => {
onCellsChange(cells)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cells])
const createCell = () => {
setCells((cells) => {
const id = generate()
cells.byId[id] = {}
cells.allIds.push(id)
})
}
const updateCell = (cellId: string, updates: Partial<ExtractingCell>) =>
setCells((cells) => {
cells.byId[cellId] = {
...cells.byId[cellId],
...updates,
}
})
const deleteCell = (cellId: string) => () => {
setCells((cells) => {
delete cells.byId[cellId]
const index = cells.allIds.indexOf(cellId)
if (index !== -1) cells.allIds.splice(index, 1)
})
}
const handleMouseEnter = (cellId: string) => () => {
setShowDeleteId(cellId)
}
const handleCellChange = (cellId: string) => (cell: ExtractingCell) =>
updateCell(cellId, cell)
const handleMouseLeave = () => setShowDeleteId(undefined)
return (
<Stack spacing="4">
{cells.allIds.map((cellId) => (
<>
<Flex
pos="relative"
onMouseEnter={handleMouseEnter(cellId)}
onMouseLeave={handleMouseLeave}
>
<CellWithVariableIdStack
key={cellId}
cell={cells.byId[cellId]}
columns={sheet.columns}
onCellChange={handleCellChange(cellId)}
/>
<Fade in={showDeleteId === cellId}>
<IconButton
icon={<TrashIcon />}
aria-label="Remove cell"
onClick={deleteCell(cellId)}
pos="absolute"
left="-10px"
top="-10px"
size="sm"
/>
</Fade>
</Flex>
</>
))}
<Button leftIcon={<PlusIcon />} onClick={createCell} flexShrink={0}>
Add
</Button>
</Stack>
)
}
export const CellWithVariableIdStack = ({
cell,
columns,
onCellChange,
}: {
cell: ExtractingCell
columns: string[]
onCellChange: (cell: ExtractingCell) => void
}) => {
const handleColumnSelect = (column: string) => {
onCellChange({ ...cell, column })
}
const handleVariableIdChange = (variable: Variable) => {
onCellChange({ ...cell, variableId: variable.id })
}
return (
<Stack bgColor="blue.50" p="4" rounded="md" flex="1">
<DropdownList<string>
currentItem={cell.column}
onItemSelect={handleColumnSelect}
items={columns}
bgColor="white"
placeholder="Select a column"
/>
<VariableSearchInput
initialVariableId={cell.variableId}
onSelectVariable={handleVariableIdChange}
placeholder="Select a variable"
/>
</Stack>
)
}

View File

@ -0,0 +1,181 @@
import { Divider, Stack, Text } from '@chakra-ui/react'
import { CredentialsDropdown } from 'components/shared/CredentialsDropdown'
import { DropdownList } from 'components/shared/DropdownList'
import { useTypebot } from 'contexts/TypebotContext'
import { CredentialsType } from 'db'
import {
Cell,
ExtractingCell,
GoogleSheetsAction,
GoogleSheetsOptions,
Table,
} from 'models'
import React, { useMemo } from 'react'
import {
getGoogleSheetsConsentScreenUrl,
Sheet,
useSheets,
} from 'services/integrations'
import { isDefined } from 'utils'
import { ExtractCellList } from './ExtractCellList'
import { SheetsDropdown } from './SheetsDropdown'
import { SpreadsheetsDropdown } from './SpreadsheetDropdown'
import { CellWithValueStack, UpdateCellList } from './UpdateCellList'
type Props = {
options?: GoogleSheetsOptions
onOptionsChange: (options: GoogleSheetsOptions) => void
stepId: string
}
export const GoogleSheetsSettingsBody = ({
options,
onOptionsChange,
stepId,
}: Props) => {
const { save, hasUnsavedChanges } = useTypebot()
const { sheets, isLoading } = useSheets({
credentialsId: options?.credentialsId,
spreadsheetId: options?.spreadsheetId,
})
const sheet = useMemo(
() => sheets?.find((s) => s.id === options?.sheetId),
[sheets, options?.sheetId]
)
const handleCredentialsIdChange = (credentialsId: string) =>
onOptionsChange({ ...options, credentialsId })
const handleSpreadsheetIdChange = (spreadsheetId: string) =>
onOptionsChange({ ...options, spreadsheetId })
const handleSheetIdChange = (sheetId: string) =>
onOptionsChange({ ...options, sheetId })
const handleActionChange = (action: GoogleSheetsAction) =>
onOptionsChange({ ...options, action })
const handleCreateNewClick = async () => {
if (hasUnsavedChanges) {
const errorToastId = await save()
if (errorToastId) return
}
const linkElement = document.createElement('a')
linkElement.href = getGoogleSheetsConsentScreenUrl(
window.location.href,
stepId
)
linkElement.click()
}
return (
<Stack>
<CredentialsDropdown
type={CredentialsType.GOOGLE_SHEETS}
currentCredentialsId={options?.credentialsId}
onCredentialsSelect={handleCredentialsIdChange}
onCreateNewClick={handleCreateNewClick}
/>
{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={options.action}
onItemSelect={handleActionChange}
items={Object.values(GoogleSheetsAction)}
placeholder="Select an operation"
/>
</>
)}
{sheet && options?.action && (
<ActionOptions
options={options}
sheet={sheet}
onOptionsChange={onOptionsChange}
/>
)}
</Stack>
)
}
const ActionOptions = ({
options,
sheet,
onOptionsChange,
}: {
options: GoogleSheetsOptions
sheet: Sheet
onOptionsChange: (options: GoogleSheetsOptions) => void
}) => {
const handleInsertColumnsChange = (cellsToInsert: Table<Cell>) =>
onOptionsChange({ ...options, cellsToInsert } as GoogleSheetsOptions)
const handleUpsertColumnsChange = (cellsToUpsert: Table<Cell>) =>
onOptionsChange({ ...options, cellsToUpsert } as GoogleSheetsOptions)
const handleReferenceCellChange = (referenceCell: Cell) =>
onOptionsChange({ ...options, referenceCell } as GoogleSheetsOptions)
const handleExtractingCellsChange = (cellsToExtract: Table<ExtractingCell>) =>
onOptionsChange({ ...options, cellsToExtract } as GoogleSheetsOptions)
switch (options.action) {
case GoogleSheetsAction.INSERT_ROW:
return (
<UpdateCellList
initialCells={options.cellsToInsert}
sheet={sheet}
onCellsChange={handleInsertColumnsChange}
/>
)
case GoogleSheetsAction.UPDATE_ROW:
return (
<Stack>
<Text>Row to select</Text>
<CellWithValueStack
cell={options.referenceCell ?? {}}
columns={sheet.columns}
onCellChange={handleReferenceCellChange}
/>
<Text>Cells to update</Text>
<UpdateCellList
initialCells={options.cellsToUpsert}
sheet={sheet}
onCellsChange={handleUpsertColumnsChange}
/>
</Stack>
)
case GoogleSheetsAction.GET:
return (
<Stack>
<Text>Row to select</Text>
<CellWithValueStack
cell={options.referenceCell ?? {}}
columns={sheet.columns}
onCellChange={handleReferenceCellChange}
/>
<Text>Cells to extract</Text>
<ExtractCellList
initialCells={options.cellsToExtract}
sheet={sheet}
onCellsChange={handleExtractingCellsChange}
/>
</Stack>
)
default:
return <></>
}
}

View File

@ -0,0 +1,37 @@
import { SearchableDropdown } from 'components/shared/SearchableDropdown'
import { useMemo } from 'react'
import { Sheet } from 'services/integrations'
import { isDefined } from 'utils'
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)
}
return (
<SearchableDropdown
selectedItem={currentSheet?.name}
items={(sheets ?? []).map((s) => s.name)}
onSelectItem={handleSpreadsheetSelect}
placeholder={isLoading ? 'Loading...' : 'Select the sheet'}
isDisabled={isLoading}
/>
)
}

View File

@ -0,0 +1,35 @@
import { SearchableDropdown } from 'components/shared/SearchableDropdown'
import { useMemo } from 'react'
import { useSpreadsheets } from 'services/integrations'
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)
}
return (
<SearchableDropdown
selectedItem={currentSpreadsheet?.name}
items={(spreadsheets ?? []).map((s) => s.name)}
onSelectItem={handleSpreadsheetSelect}
placeholder={isLoading ? 'Loading...' : 'Search for spreadsheet'}
isDisabled={isLoading}
/>
)
}

View File

@ -0,0 +1,136 @@
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 { Cell, Table } from 'models'
import React, { useEffect, useState } from 'react'
import { Sheet } from 'services/integrations'
import { generate } from 'short-uuid'
import { useImmer } from 'use-immer'
type Props = {
sheet: Sheet
initialCells?: Table<Cell>
onCellsChange: (cells: Table<Cell>) => void
}
const id = generate()
const defaultCells: Table<Cell> = {
byId: { [id]: {} },
allIds: [id],
}
export const UpdateCellList = ({
sheet,
initialCells,
onCellsChange,
}: Props) => {
const [cells, setCells] = useImmer(initialCells ?? defaultCells)
const [showDeleteId, setShowDeleteId] = useState<string | undefined>()
useEffect(() => {
onCellsChange(cells)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cells])
const createCell = () => {
setCells((cells) => {
const id = generate()
cells.byId[id] = {}
cells.allIds.push(id)
})
}
const updateCell = (cellId: string, updates: Partial<Cell>) =>
setCells((cells) => {
cells.byId[cellId] = {
...cells.byId[cellId],
...updates,
}
})
const deleteCell = (cellId: string) => () => {
setCells((cells) => {
delete cells.byId[cellId]
const index = cells.allIds.indexOf(cellId)
if (index !== -1) cells.allIds.splice(index, 1)
})
}
const handleMouseEnter = (cellId: string) => () => {
setShowDeleteId(cellId)
}
const handleCellChange = (cellId: string) => (cell: Cell) =>
updateCell(cellId, cell)
const handleMouseLeave = () => setShowDeleteId(undefined)
return (
<Stack spacing="4">
{cells.allIds.map((cellId) => (
<>
<Flex
pos="relative"
onMouseEnter={handleMouseEnter(cellId)}
onMouseLeave={handleMouseLeave}
>
<CellWithValueStack
key={cellId}
cell={cells.byId[cellId]}
columns={sheet.columns}
onCellChange={handleCellChange(cellId)}
/>
<Fade in={showDeleteId === cellId}>
<IconButton
icon={<TrashIcon />}
aria-label="Remove cell"
onClick={deleteCell(cellId)}
pos="absolute"
left="-10px"
top="-10px"
size="sm"
/>
</Fade>
</Flex>
</>
))}
<Button leftIcon={<PlusIcon />} onClick={createCell} flexShrink={0}>
Add
</Button>
</Stack>
)
}
export const CellWithValueStack = ({
cell,
columns,
onCellChange,
}: {
cell: Cell
columns: string[]
onCellChange: (column: Cell) => void
}) => {
const handleColumnSelect = (column: string) => {
onCellChange({ ...cell, column })
}
const handleValueChange = (value: string) => {
onCellChange({ ...cell, value })
}
return (
<Stack bgColor="blue.50" p="4" rounded="md" flex="1">
<DropdownList<string>
currentItem={cell.column}
onItemSelect={handleColumnSelect}
items={columns}
bgColor="white"
placeholder="Select a column"
/>
<InputWithVariable
initialValue={cell.value ?? ''}
onValueChange={handleValueChange}
placeholder="Type a value..."
/>
</Stack>
)
}

View File

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

View File

@ -7,21 +7,27 @@ import {
useEventListener, useEventListener,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import React, { useEffect, useMemo, useState } from 'react' import React, { useEffect, useMemo, useState } from 'react'
import { Block, Step } from 'models' import { Block, DraggableStep, Step } from 'models'
import { useGraph } from 'contexts/GraphContext' import { useGraph } from 'contexts/GraphContext'
import { StepIcon } from 'components/board/StepTypesList/StepIcon' import { StepIcon } from 'components/board/StepTypesList/StepIcon'
import { isDefined, isInputStep, isLogicStep, isTextBubbleStep } from 'utils' import {
isDefined,
isInputStep,
isLogicStep,
isTextBubbleStep,
isIntegrationStep,
} from 'utils'
import { Coordinates } from '@dnd-kit/core/dist/types' import { Coordinates } from '@dnd-kit/core/dist/types'
import { TextEditor } from './TextEditor/TextEditor' import { TextEditor } from './TextEditor/TextEditor'
import { StepNodeContent } from './StepNodeContent' import { StepNodeContent } from './StepNodeContent'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { ContextMenu } from 'components/shared/ContextMenu' import { ContextMenu } from 'components/shared/ContextMenu'
import { SettingsPopoverContent } from './SettingsPopoverContent' import { SettingsPopoverContent } from './SettingsPopoverContent'
import { DraggableStep } from 'contexts/DndContext'
import { StepNodeContextMenu } from './StepNodeContextMenu' import { StepNodeContextMenu } from './StepNodeContextMenu'
import { SourceEndpoint } from './SourceEndpoint' import { SourceEndpoint } from './SourceEndpoint'
import { hasDefaultConnector } from 'services/typebots' import { hasDefaultConnector } from 'services/typebots'
import { TargetEndpoint } from './TargetEndpoint' import { TargetEndpoint } from './TargetEndpoint'
import { useRouter } from 'next/router'
export const StepNode = ({ export const StepNode = ({
step, step,
@ -39,6 +45,7 @@ export const StepNode = ({
step: DraggableStep step: DraggableStep
) => void ) => void
}) => { }) => {
const { query } = useRouter()
const { setConnectingIds, connectingIds } = useGraph() const { setConnectingIds, connectingIds } = useGraph()
const { moveStep, typebot } = useTypebot() const { moveStep, typebot } = useTypebot()
const [isConnecting, setIsConnecting] = useState(false) const [isConnecting, setIsConnecting] = useState(false)
@ -152,7 +159,11 @@ export const StepNode = ({
renderMenu={() => <StepNodeContextMenu stepId={step.id} />} renderMenu={() => <StepNodeContextMenu stepId={step.id} />}
> >
{(ref, isOpened) => ( {(ref, isOpened) => (
<Popover placement="left" isLazy> <Popover
placement="left"
isLazy
defaultIsOpen={query.stepId?.toString() === step.id}
>
<PopoverTrigger> <PopoverTrigger>
<Flex <Flex
pos="relative" pos="relative"
@ -226,11 +237,12 @@ export const StepNode = ({
)} )}
</Flex> </Flex>
</PopoverTrigger> </PopoverTrigger>
{(isInputStep(step) || isLogicStep(step)) && ( {hasPopover(step) && <SettingsPopoverContent step={step} />}
<SettingsPopoverContent step={step} />
)}
</Popover> </Popover>
)} )}
</ContextMenu> </ContextMenu>
) )
} }
const hasPopover = (step: Step) =>
isInputStep(step) || isLogicStep(step) || isIntegrationStep(step)

View File

@ -8,6 +8,7 @@ import {
LogicStepType, LogicStepType,
SetVariableStep, SetVariableStep,
ConditionStep, ConditionStep,
IntegrationStepType,
} from 'models' } from 'models'
import { ChoiceItemsList } from './ChoiceInputStepNode/ChoiceItemsList' import { ChoiceItemsList } from './ChoiceInputStepNode/ChoiceItemsList'
import { SourceEndpoint } from './SourceEndpoint' import { SourceEndpoint } from './SourceEndpoint'
@ -84,6 +85,10 @@ export const StepNodeContent = ({ step }: Props) => {
case LogicStepType.CONDITION: { case LogicStepType.CONDITION: {
return <ConditionNodeContent step={step} /> return <ConditionNodeContent step={step} />
} }
case IntegrationStepType.GOOGLE_SHEETS: {
if (!step.options) return <Text color={'gray.500'}>Configure...</Text>
return <Text>{step.options?.action}</Text>
}
case 'start': { case 'start': {
return <Text>{step.label}</Text> return <Text>{step.label}</Text>
} }

View File

@ -1,6 +1,6 @@
import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react' import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react'
import { Step, Table } from 'models' import { DraggableStep, Step, Table } from 'models'
import { DraggableStep, useDnd } from 'contexts/DndContext' import { useDnd } from 'contexts/DndContext'
import { Coordinates } from 'contexts/GraphContext' import { Coordinates } from 'contexts/GraphContext'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { StepNode, StepNodeOverlay } from './StepNode' import { StepNode, StepNodeOverlay } from './StepNode'

View File

@ -2,10 +2,11 @@ import { Flex, FlexProps, useEventListener } from '@chakra-ui/react'
import React, { useRef, useMemo } from 'react' import React, { useRef, useMemo } from 'react'
import { blockWidth, useGraph } from 'contexts/GraphContext' import { blockWidth, useGraph } from 'contexts/GraphContext'
import { BlockNode } from './BlockNode/BlockNode' import { BlockNode } from './BlockNode/BlockNode'
import { DraggableStepType, useDnd } from 'contexts/DndContext' import { useDnd } from 'contexts/DndContext'
import { Edges } from './Edges' import { Edges } from './Edges'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader' import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
import { DraggableStepType } from 'models'
const Graph = ({ ...props }: FlexProps) => { const Graph = ({ ...props }: FlexProps) => {
const { draggedStepType, setDraggedStepType, draggedStep, setDraggedStep } = const { draggedStepType, setDraggedStepType, draggedStep, setDraggedStep } =

View File

@ -0,0 +1,104 @@
import {
Button,
Menu,
MenuButton,
MenuButtonProps,
MenuItem,
MenuList,
Stack,
Text,
} from '@chakra-ui/react'
import { ChevronLeftIcon, PlusIcon } from 'assets/icons'
import React, { useEffect, useMemo } from 'react'
import { useUser } from 'contexts/UserContext'
import { CredentialsType } from 'db'
import { useRouter } from 'next/router'
type Props = Omit<MenuButtonProps, 'type'> & {
type: CredentialsType
currentCredentialsId?: string
onCredentialsSelect: (credentialId: string) => void
onCreateNewClick: () => void
}
export const CredentialsDropdown = ({
type,
currentCredentialsId,
onCredentialsSelect,
onCreateNewClick,
...props
}: Props) => {
const router = useRouter()
const { credentials } = useUser()
const credentialsList = useMemo(() => {
return credentials.filter((credential) => credential.type === type)
}, [type, credentials])
const currentCredential = useMemo(
() => credentials.find((c) => c.id === currentCredentialsId),
[currentCredentialsId, credentials]
)
const handleMenuItemClick = (credentialId: string) => () => {
onCredentialsSelect(credentialId)
}
useEffect(() => {
if (!router.isReady) return
if (router.query.credentialsId) {
handleMenuItemClick(router.query.credentialsId.toString())()
clearQueryParams()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [router.isReady])
const clearQueryParams = () => {
const hasQueryParams = router.asPath.includes('?')
if (hasQueryParams)
router.push(router.asPath.split('?')[0], undefined, { shallow: true })
}
return (
<Menu isLazy placement="bottom-end" matchWidth>
<MenuButton
as={Button}
rightIcon={<ChevronLeftIcon transform={'rotate(-90deg)'} />}
colorScheme="gray"
justifyContent="space-between"
textAlign="left"
{...props}
>
<Text isTruncated overflowY="visible" h="20px">
{currentCredential ? currentCredential.name : 'Select an account'}
</Text>
</MenuButton>
<MenuList maxW="500px">
<Stack maxH={'35vh'} overflowY="scroll" spacing="0">
{credentialsList.map((credentials) => (
<MenuItem
key={credentials.id}
maxW="500px"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
onClick={handleMenuItemClick(credentials.id)}
>
{credentials.name}
</MenuItem>
))}
<MenuItem
maxW="500px"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
icon={<PlusIcon />}
onClick={onCreateNewClick}
>
Connect new
</MenuItem>
</Stack>
</MenuList>
</Menu>
)
}

View File

@ -1,5 +1,11 @@
import { Input, InputProps } from '@chakra-ui/react' import { Input, InputProps } from '@chakra-ui/react'
import { ChangeEvent, useEffect, useState } from 'react' import {
ChangeEvent,
ForwardedRef,
forwardRef,
useEffect,
useState,
} from 'react'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
type Props = Omit<InputProps, 'onChange' | 'value'> & { type Props = Omit<InputProps, 'onChange' | 'value'> & {
@ -8,24 +14,31 @@ type Props = Omit<InputProps, 'onChange' | 'value'> & {
onChange: (debouncedValue: string) => void onChange: (debouncedValue: string) => void
} }
export const DebouncedInput = ({ export const DebouncedInput = forwardRef(
delay, (
onChange, { delay, onChange, initialValue, ...props }: Props,
initialValue, ref: ForwardedRef<HTMLInputElement>
...props ) => {
}: Props) => { const [currentValue, setCurrentValue] = useState(initialValue)
const [currentValue, setCurrentValue] = useState(initialValue) const [currentValueDebounced] = useDebounce(currentValue, delay)
const [currentValueDebounced] = useDebounce(currentValue, delay)
useEffect(() => { useEffect(() => {
if (currentValueDebounced === initialValue) return if (currentValueDebounced === initialValue) return
onChange(currentValueDebounced) onChange(currentValueDebounced)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentValueDebounced]) }, [currentValueDebounced])
const handleChange = (e: ChangeEvent<HTMLInputElement>) => { const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setCurrentValue(e.target.value) setCurrentValue(e.target.value)
}
return (
<Input
{...props}
ref={ref}
value={currentValue}
onChange={handleChange}
/>
)
} }
)
return <Input {...props} value={currentValue} onChange={handleChange} />
}

View File

@ -11,15 +11,17 @@ import { ChevronLeftIcon } from 'assets/icons'
import React from 'react' import React from 'react'
type Props<T> = { type Props<T> = {
currentItem: T currentItem?: T
onItemSelect: (item: T) => void onItemSelect: (item: T) => void
items: T[] items: T[]
placeholder?: string
} }
export const DropdownList = <T,>({ export const DropdownList = <T,>({
currentItem, currentItem,
onItemSelect, onItemSelect,
items, items,
placeholder = '',
...props ...props
}: Props<T> & MenuButtonProps) => { }: Props<T> & MenuButtonProps) => {
const handleMenuItemClick = (operator: T) => () => { const handleMenuItemClick = (operator: T) => () => {
@ -27,7 +29,7 @@ export const DropdownList = <T,>({
} }
return ( return (
<> <>
<Menu isLazy placement="bottom-end"> <Menu isLazy placement="bottom-end" matchWidth>
<MenuButton <MenuButton
as={Button} as={Button}
rightIcon={<ChevronLeftIcon transform={'rotate(-90deg)'} />} rightIcon={<ChevronLeftIcon transform={'rotate(-90deg)'} />}
@ -37,9 +39,9 @@ export const DropdownList = <T,>({
textAlign="left" textAlign="left"
{...props} {...props}
> >
{currentItem} {currentItem ?? placeholder}
</MenuButton> </MenuButton>
<MenuList maxW="500px"> <MenuList maxW="500px" shadow="lg">
<Stack maxH={'35vh'} overflowY="scroll" spacing="0"> <Stack maxH={'35vh'} overflowY="scroll" spacing="0">
{items.map((item) => ( {items.map((item) => (
<MenuItem <MenuItem

View File

@ -0,0 +1,105 @@
import {
IconButton,
Input,
InputGroup,
InputProps,
InputRightElement,
Popover,
PopoverContent,
PopoverTrigger,
} from '@chakra-ui/react'
import { UserIcon } from 'assets/icons'
import { Variable } from 'models'
import React, { useEffect, useRef, useState } from 'react'
import { useDebounce } from 'use-debounce'
import { VariableSearchInput } from './VariableSearchInput'
export const InputWithVariable = ({
initialValue,
noAbsolute,
onValueChange,
...props
}: {
initialValue: string
onValueChange: (value: string) => void
noAbsolute?: boolean
} & InputProps) => {
const inputRef = useRef<HTMLInputElement | null>(null)
const [value, setValue] = useState(initialValue)
const [debouncedValue] = useDebounce(value, 100)
const [carretPosition, setCarretPosition] = useState<number>(0)
useEffect(() => {
onValueChange(debouncedValue)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedValue])
const handleVariableSelected = (variable: Variable) => {
if (!inputRef.current) return
const cursorPosition = carretPosition
const textBeforeCursorPosition = inputRef.current.value.substring(
0,
cursorPosition
)
const textAfterCursorPosition = inputRef.current.value.substring(
cursorPosition,
inputRef.current.value.length
)
setValue(
textBeforeCursorPosition +
`{{${variable.name}}}` +
textAfterCursorPosition
)
inputRef.current.focus()
setTimeout(() => {
if (!inputRef.current) return
inputRef.current.selectionStart = inputRef.current.selectionEnd =
carretPosition + `{{${variable.name}}}`.length
}, 100)
}
const handleKeyUp = () => {
if (!inputRef.current?.selectionStart) return
setCarretPosition(inputRef.current.selectionStart)
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
setValue(e.target.value)
return (
<InputGroup>
<Input
ref={inputRef}
onKeyUp={handleKeyUp}
onClick={handleKeyUp}
value={value}
onChange={handleChange}
{...props}
bgColor={'white'}
/>
<InputRightElement
pos={noAbsolute ? 'relative' : 'absolute'}
zIndex={noAbsolute ? 'unset' : '1'}
>
<Popover matchWidth isLazy>
<PopoverTrigger>
<IconButton
aria-label="Insert a variable"
icon={<UserIcon />}
size="sm"
pos="relative"
/>
</PopoverTrigger>
<PopoverContent w="full">
<VariableSearchInput
onSelectVariable={handleVariableSelected}
placeholder="Search for a variable"
shadow="lg"
isDefaultOpen
/>
</PopoverContent>
</Popover>
</InputRightElement>
</InputGroup>
)
}

View File

@ -8,18 +8,21 @@ import {
PopoverContent, PopoverContent,
Button, Button,
Text, Text,
InputProps,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { useState, useRef, useEffect, ChangeEvent } from 'react' import { useState, useRef, useEffect, ChangeEvent } from 'react'
type Props = {
selectedItem?: string
items: string[]
onSelectItem: (value: string) => void
} & InputProps
export const SearchableDropdown = ({ export const SearchableDropdown = ({
selectedItem, selectedItem,
items, items,
onSelectItem, onSelectItem,
}: { ...inputProps
selectedItem?: string }: Props) => {
items: string[]
onSelectItem: (value: string) => void
}) => {
const { onOpen, onClose, isOpen } = useDisclosure() const { onOpen, onClose, isOpen } = useDisclosure()
const [inputValue, setInputValue] = useState(selectedItem) const [inputValue, setInputValue] = useState(selectedItem)
const [filteredItems, setFilteredItems] = useState([ const [filteredItems, setFilteredItems] = useState([
@ -64,19 +67,38 @@ export const SearchableDropdown = ({
]) ])
} }
const handleItemClick = (item: string) => () => {
setInputValue(item)
onSelectItem(item)
onClose()
}
return ( return (
<Flex ref={dropdownRef}> <Flex ref={dropdownRef} w="full">
<Popover isOpen={isOpen} initialFocusRef={inputRef}> <Popover
isOpen={isOpen}
initialFocusRef={inputRef}
matchWidth
offset={[0, 0]}
isLazy
>
<PopoverTrigger> <PopoverTrigger>
<Input <Input
ref={inputRef} ref={inputRef}
value={inputValue} value={inputValue}
onChange={onInputChange} onChange={onInputChange}
onClick={onOpen} onClick={onOpen}
w="300px" {...inputProps}
/> />
</PopoverTrigger> </PopoverTrigger>
<PopoverContent maxH="35vh" overflowY="scroll" spacing="0" w="300px"> <PopoverContent
maxH="35vh"
overflowY="scroll"
spacing="0"
role="menu"
w="inherit"
shadow="lg"
>
{filteredItems.length > 0 ? ( {filteredItems.length > 0 ? (
<> <>
{filteredItems.map((item, idx) => { {filteredItems.map((item, idx) => {
@ -84,15 +106,12 @@ export const SearchableDropdown = ({
<Button <Button
minH="40px" minH="40px"
key={idx} key={idx}
onClick={() => { onClick={handleItemClick(item)}
setInputValue(item)
onSelectItem(item)
onClose()
}}
fontSize="16px" fontSize="16px"
fontWeight="normal" fontWeight="normal"
rounded="none" rounded="none"
colorScheme="gray" colorScheme="gray"
role="menuitem"
variant="ghost" variant="ghost"
justifyContent="flex-start" justifyContent="flex-start"
> >

View File

@ -1,12 +1,4 @@
import { import { ChoiceItem, DraggableStep, DraggableStepType } from 'models'
BubbleStep,
BubbleStepType,
ChoiceItem,
InputStep,
InputStepType,
LogicStepType,
LogicStep,
} from 'models'
import { import {
createContext, createContext,
Dispatch, Dispatch,
@ -16,9 +8,6 @@ import {
useState, useState,
} from 'react' } from 'react'
export type DraggableStep = BubbleStep | InputStep | LogicStep
export type DraggableStepType = BubbleStepType | InputStepType | LogicStepType
const dndContext = createContext<{ const dndContext = createContext<{
draggedStepType?: DraggableStepType draggedStepType?: DraggableStepType
setDraggedStepType: Dispatch<SetStateAction<DraggableStepType | undefined>> setDraggedStepType: Dispatch<SetStateAction<DraggableStepType | undefined>>

View File

@ -1,4 +1,4 @@
import { useToast } from '@chakra-ui/react' import { ToastId, useToast } from '@chakra-ui/react'
import { PublicTypebot, Settings, Theme, Typebot } from 'models' import { PublicTypebot, Settings, Theme, Typebot } from 'models'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { import {
@ -43,7 +43,7 @@ const typebotContext = createContext<
isPublishing: boolean isPublishing: boolean
hasUnsavedChanges: boolean hasUnsavedChanges: boolean
isSavingLoading: boolean isSavingLoading: boolean
save: () => void save: () => Promise<ToastId | undefined>
undo: () => void undo: () => void
updateTypebot: (updates: UpdateTypebotPayload) => void updateTypebot: (updates: UpdateTypebotPayload) => void
publishTypebot: () => void publishTypebot: () => void

View File

@ -1,13 +1,6 @@
import { Coordinates } from 'contexts/GraphContext' import { Coordinates } from 'contexts/GraphContext'
import { WritableDraft } from 'immer/dist/internal' import { WritableDraft } from 'immer/dist/internal'
import { import { Block, DraggableStep, DraggableStepType, Typebot } from 'models'
Block,
BubbleStepType,
InputStepType,
LogicStepType,
Step,
Typebot,
} from 'models'
import { parseNewBlock } from 'services/typebots' import { parseNewBlock } from 'services/typebots'
import { Updater } from 'use-immer' import { Updater } from 'use-immer'
import { createStepDraft, deleteStepDraft } from './steps' import { createStepDraft, deleteStepDraft } from './steps'
@ -15,7 +8,7 @@ import { createStepDraft, deleteStepDraft } from './steps'
export type BlocksActions = { export type BlocksActions = {
createBlock: ( createBlock: (
props: Coordinates & { props: Coordinates & {
step: BubbleStepType | InputStepType | LogicStepType | Step step: DraggableStep | DraggableStepType
} }
) => void ) => void
updateBlock: (blockId: string, updates: Partial<Omit<Block, 'id'>>) => void updateBlock: (blockId: string, updates: Partial<Omit<Block, 'id'>>) => void
@ -28,7 +21,7 @@ export const blocksActions = (setTypebot: Updater<Typebot>): BlocksActions => ({
y, y,
step, step,
}: Coordinates & { }: Coordinates & {
step: BubbleStepType | InputStepType | LogicStepType | Step step: DraggableStep | DraggableStepType
}) => { }) => {
setTypebot((typebot) => { setTypebot((typebot) => {
const newBlock = parseNewBlock({ const newBlock = parseNewBlock({

View File

@ -1,10 +1,9 @@
import { import {
BubbleStepType,
ChoiceInputStep, ChoiceInputStep,
InputStepType,
Step, Step,
Typebot, Typebot,
LogicStepType, DraggableStep,
DraggableStepType,
} from 'models' } from 'models'
import { parseNewStep } from 'services/typebots' import { parseNewStep } from 'services/typebots'
import { Updater } from 'use-immer' import { Updater } from 'use-immer'
@ -16,7 +15,7 @@ import { isChoiceInput } from 'utils'
export type StepsActions = { export type StepsActions = {
createStep: ( createStep: (
blockId: string, blockId: string,
step: BubbleStepType | InputStepType | LogicStepType | Step, step: DraggableStep | DraggableStepType,
index?: number index?: number
) => void ) => void
updateStep: ( updateStep: (
@ -30,7 +29,7 @@ export type StepsActions = {
export const stepsAction = (setTypebot: Updater<Typebot>): StepsActions => ({ export const stepsAction = (setTypebot: Updater<Typebot>): StepsActions => ({
createStep: ( createStep: (
blockId: string, blockId: string,
step: BubbleStepType | InputStepType | LogicStepType | Step, step: DraggableStep | DraggableStepType,
index?: number index?: number
) => { ) => {
setTypebot((typebot) => { setTypebot((typebot) => {
@ -76,7 +75,7 @@ export const deleteStepDraft = (
export const createStepDraft = ( export const createStepDraft = (
typebot: WritableDraft<Typebot>, typebot: WritableDraft<Typebot>,
step: BubbleStepType | InputStepType | LogicStepType | Step, step: DraggableStep | DraggableStepType,
blockId: string, blockId: string,
index?: number index?: number
) => { ) => {

View File

@ -1,4 +1,3 @@
import { User } from 'db'
import { useSession } from 'next-auth/react' import { useSession } from 'next-auth/react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { import {
@ -13,6 +12,8 @@ import { isDefined } from 'utils'
import { updateUser as updateUserInDb } from 'services/user' import { updateUser as updateUserInDb } from 'services/user'
import { useToast } from '@chakra-ui/react' import { useToast } from '@chakra-ui/react'
import { deepEqual } from 'fast-equals' import { deepEqual } from 'fast-equals'
import { useCredentials } from 'services/credentials'
import { Credentials, User } from 'db'
const userContext = createContext<{ const userContext = createContext<{
user?: User user?: User
@ -20,6 +21,7 @@ const userContext = createContext<{
isSaving: boolean isSaving: boolean
hasUnsavedChanges: boolean hasUnsavedChanges: boolean
isOAuthProvider: boolean isOAuthProvider: boolean
credentials: Credentials[]
updateUser: (newUser: Partial<User>) => void updateUser: (newUser: Partial<User>) => void
saveUser: () => void saveUser: () => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -29,8 +31,12 @@ const userContext = createContext<{
export const UserContext = ({ children }: { children: ReactNode }) => { export const UserContext = ({ children }: { children: ReactNode }) => {
const router = useRouter() const router = useRouter()
const { data: session, status } = useSession() const { data: session, status } = useSession()
const [user, setUser] = useState<User | undefined>()
const [user, setUser] = useState<User>() const { credentials } = useCredentials({
userId: user?.id,
onError: (error) =>
toast({ title: error.name, description: error.message }),
})
const [isSaving, setIsSaving] = useState(false) const [isSaving, setIsSaving] = useState(false)
const isOAuthProvider = useMemo( const isOAuthProvider = useMemo(
() => (session?.providerType as boolean | undefined) ?? false, () => (session?.providerType as boolean | undefined) ?? false,
@ -69,9 +75,13 @@ export const UserContext = ({ children }: { children: ReactNode }) => {
setIsSaving(true) setIsSaving(true)
const { error } = await updateUserInDb(user.id, user) const { error } = await updateUserInDb(user.id, user)
if (error) toast({ title: error.name, description: error.message }) if (error) toast({ title: error.name, description: error.message })
await refreshUser()
setIsSaving(false)
}
const refreshUser = async () => {
await fetch('/api/auth/session?update') await fetch('/api/auth/session?update')
reloadSession() reloadSession()
setIsSaving(false)
} }
return ( return (
@ -84,6 +94,7 @@ export const UserContext = ({ children }: { children: ReactNode }) => {
isLoading: status === 'loading', isLoading: status === 'loading',
hasUnsavedChanges, hasUnsavedChanges,
isOAuthProvider, isOAuthProvider,
credentials: credentials ?? [],
}} }}
> >
{children} {children}

View File

@ -0,0 +1,80 @@
{
"id": "typebot4",
"createdAt": "2022-01-17T14:37:01.826Z",
"updatedAt": "2022-01-17T14:37:01.826Z",
"name": "My typebot",
"ownerId": "user2",
"publishedTypebotId": null,
"folderId": null,
"blocks": {
"byId": {
"bec5A5bLwenmZpCJc8FRaM": {
"id": "bec5A5bLwenmZpCJc8FRaM",
"title": "Start",
"stepIds": ["uDMB7a2ucg17WGvbQJjeRn"],
"graphCoordinates": { "x": 0, "y": 0 }
},
"bcyiT7P6E99YnHKnpxs4Yux": {
"id": "bcyiT7P6E99YnHKnpxs4Yux",
"title": "Block #2",
"stepIds": ["step1"],
"graphCoordinates": { "x": 411, "y": 108 }
},
"bgpNxHtBBXWrP1QMe2A8hZ9": {
"id": "bgpNxHtBBXWrP1QMe2A8hZ9",
"title": "Block #3",
"graphCoordinates": { "x": 1, "y": 236 },
"stepIds": ["sj72oDhJEe4N92KTt64GKWs"]
}
},
"allIds": [
"bec5A5bLwenmZpCJc8FRaM",
"bcyiT7P6E99YnHKnpxs4Yux",
"bgpNxHtBBXWrP1QMe2A8hZ9"
]
},
"steps": {
"byId": {
"step1": {
"id": "step1",
"type": "Google Sheets",
"blockId": "bcyiT7P6E99YnHKnpxs4Yux"
},
"uDMB7a2ucg17WGvbQJjeRn": {
"id": "uDMB7a2ucg17WGvbQJjeRn",
"type": "start",
"label": "Start",
"target": { "blockId": "bgpNxHtBBXWrP1QMe2A8hZ9" },
"blockId": "bec5A5bLwenmZpCJc8FRaM"
},
"sj72oDhJEe4N92KTt64GKWs": {
"id": "sj72oDhJEe4N92KTt64GKWs",
"blockId": "bgpNxHtBBXWrP1QMe2A8hZ9",
"type": "email input",
"target": { "blockId": "bcyiT7P6E99YnHKnpxs4Yux" },
"options": { "variableId": "8H3aQsNji2Gyfpp3RPozNN" }
}
},
"allIds": ["uDMB7a2ucg17WGvbQJjeRn", "step1", "sj72oDhJEe4N92KTt64GKWs"]
},
"choiceItems": { "byId": {}, "allIds": [] },
"variables": {
"byId": {
"8H3aQsNji2Gyfpp3RPozNN": {
"id": "8H3aQsNji2Gyfpp3RPozNN",
"name": "Email"
}
},
"allIds": ["8H3aQsNji2Gyfpp3RPozNN"]
},
"theme": {
"general": {
"font": "Open Sans",
"background": { "type": "None", "content": "#ffffff" }
}
},
"settings": {
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
},
"publicId": null
}

View File

@ -0,0 +1,110 @@
{
"id": "typebot4",
"createdAt": "2022-01-17T14:37:01.826Z",
"updatedAt": "2022-01-17T14:37:01.826Z",
"name": "My typebot",
"ownerId": "user2",
"publishedTypebotId": null,
"folderId": null,
"blocks": {
"byId": {
"bec5A5bLwenmZpCJc8FRaM": {
"id": "bec5A5bLwenmZpCJc8FRaM",
"title": "Start",
"stepIds": ["uDMB7a2ucg17WGvbQJjeRn"],
"graphCoordinates": { "x": 0, "y": 0 }
},
"bcyiT7P6E99YnHKnpxs4Yux": {
"id": "bcyiT7P6E99YnHKnpxs4Yux",
"title": "Block #2",
"stepIds": ["step1"],
"graphCoordinates": { "x": 411, "y": 108 }
},
"bgpNxHtBBXWrP1QMe2A8hZ9": {
"id": "bgpNxHtBBXWrP1QMe2A8hZ9",
"title": "Block #3",
"stepIds": ["sj72oDhJEe4N92KTt64GKWs"],
"graphCoordinates": { "x": 1, "y": 236 }
},
"buwMV9tx2EFcMbRkNTELt3J": {
"id": "buwMV9tx2EFcMbRkNTELt3J",
"title": "Block #4",
"graphCoordinates": { "x": 441, "y": 330 },
"stepIds": ["s2R2bk7qfSRVgTyRmPhVw7p"]
}
},
"allIds": [
"bec5A5bLwenmZpCJc8FRaM",
"bcyiT7P6E99YnHKnpxs4Yux",
"bgpNxHtBBXWrP1QMe2A8hZ9",
"buwMV9tx2EFcMbRkNTELt3J"
]
},
"steps": {
"byId": {
"step1": {
"id": "step1",
"type": "Google Sheets",
"blockId": "bcyiT7P6E99YnHKnpxs4Yux",
"target": { "blockId": "buwMV9tx2EFcMbRkNTELt3J" }
},
"uDMB7a2ucg17WGvbQJjeRn": {
"id": "uDMB7a2ucg17WGvbQJjeRn",
"type": "start",
"label": "Start",
"target": { "blockId": "bgpNxHtBBXWrP1QMe2A8hZ9" },
"blockId": "bec5A5bLwenmZpCJc8FRaM"
},
"sj72oDhJEe4N92KTt64GKWs": {
"id": "sj72oDhJEe4N92KTt64GKWs",
"type": "email input",
"target": { "blockId": "bcyiT7P6E99YnHKnpxs4Yux" },
"blockId": "bgpNxHtBBXWrP1QMe2A8hZ9",
"options": { "variableId": "8H3aQsNji2Gyfpp3RPozNN" }
},
"s2R2bk7qfSRVgTyRmPhVw7p": {
"id": "s2R2bk7qfSRVgTyRmPhVw7p",
"blockId": "buwMV9tx2EFcMbRkNTELt3J",
"type": "text",
"content": {
"html": "<div>Your name is: {{First name}} {{Last name}}</div>",
"richText": [
{
"type": "p",
"children": [
{ "text": "Your name is: {{First name}} {{Last name}}" }
]
}
],
"plainText": "Your name is: {{First name}} {{Last name}}"
}
}
},
"allIds": [
"uDMB7a2ucg17WGvbQJjeRn",
"step1",
"sj72oDhJEe4N92KTt64GKWs",
"s2R2bk7qfSRVgTyRmPhVw7p"
]
},
"choiceItems": { "byId": {}, "allIds": [] },
"variables": {
"byId": {
"8H3aQsNji2Gyfpp3RPozNN": {
"id": "8H3aQsNji2Gyfpp3RPozNN",
"name": "Email"
}
},
"allIds": ["8H3aQsNji2Gyfpp3RPozNN"]
},
"theme": {
"general": {
"font": "Open Sans",
"background": { "type": "None", "content": "#ffffff" }
}
},
"settings": {
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
},
"publicId": null
}

View File

@ -1,5 +1,5 @@
import { InputStepType, PublicTypebot, Typebot } from 'models' import { InputStepType, PublicTypebot, Typebot } from 'models'
import { Plan, PrismaClient } from 'db' import { CredentialsType, Plan, PrismaClient } from 'db'
import { parseTestTypebot } from './utils' import { parseTestTypebot } from './utils'
import { userIds } from './data' import { userIds } from './data'
@ -7,9 +7,10 @@ const prisma = new PrismaClient()
const teardownTestData = async () => prisma.user.deleteMany() const teardownTestData = async () => prisma.user.deleteMany()
export const seedDb = async () => { export const seedDb = async (googleRefreshToken: string) => {
await teardownTestData() await teardownTestData()
await createUsers() await createUsers()
await createCredentials(googleRefreshToken)
await createFolders() await createFolders()
await createTypebots() await createTypebots()
await createResults() await createResults()
@ -33,6 +34,23 @@ const createUsers = () =>
], ],
}) })
const createCredentials = (refresh_token: string) =>
prisma.credentials.createMany({
data: [
{
name: 'test2@gmail.com',
ownerId: userIds[1],
type: CredentialsType.GOOGLE_SHEETS,
data: {
expiry_date: 1642441058842,
access_token:
'ya29.A0ARrdaM--PV_87ebjywDJpXKb77NBFJl16meVUapYdfNv6W6ZzqqC47fNaPaRjbDbOIIcp6f49cMaX5ndK9TAFnKwlVqz3nrK9nLKqgyDIhYsIq47smcAIZkK56SWPx3X3DwAFqRu2UPojpd2upWwo-3uJrod',
refresh_token,
},
},
],
})
const createFolders = () => const createFolders = () =>
prisma.dashboardFolder.createMany({ prisma.dashboardFolder.createMany({
data: [{ ownerId: userIds[1], name: 'Folder #1', id: 'folder1' }], data: [{ ownerId: userIds[1], name: 'Folder #1', id: 'folder1' }],

View File

@ -280,7 +280,9 @@ const createTypebotWithStep = (step: Omit<InputStep, 'id' | 'blockId'>) => {
//@ts-ignore //@ts-ignore
options: options:
step.type === InputStepType.CHOICE step.type === InputStepType.CHOICE
? { itemIds: ['item1'] } ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
{ itemIds: ['item1'] }
: undefined, : undefined,
}, },
}, },

View File

@ -0,0 +1,139 @@
import { preventUserFromRefreshing } from 'cypress/plugins/utils'
import { getIframeBody } from 'cypress/support'
describe('Google sheets', () => {
beforeEach(() => {
cy.task('seed', Cypress.env('GOOGLE_SHEETS_REFRESH_TOKEN'))
cy.signOut()
})
afterEach(() => {
cy.window().then((win) => {
win.removeEventListener('beforeunload', preventUserFromRefreshing)
})
})
it('Insert row should work', () => {
cy.intercept({
url: '/api/integrations/google-sheets/spreadsheets/1k_pIDw3YHl9tlZusbBVSBRY0PeRPd2H6t4Nj7rwnOtM/sheets/0',
method: 'POST',
}).as('insertRowInGoogleSheets')
cy.loadTypebotFixtureInDatabase('typebots/integrations/googleSheets.json')
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/edit')
fillInSpreadsheetInfo()
cy.findByRole('button', { name: 'Select an operation' }).click()
cy.findByRole('menuitem', { name: 'Insert a row' }).click({ force: true })
cy.findByRole('button', { name: 'Select a column' }).click()
cy.findByRole('menuitem', { name: 'Email' }).click()
cy.findByRole('button', { name: 'Insert a variable' }).click()
cy.findByRole('menuitem', { name: 'Email' }).click()
cy.findByRole('button', { name: 'Add' }).click()
cy.findByRole('button', { name: 'Select a column' }).click()
cy.findByRole('menuitem', { name: 'First name' }).click()
cy.findAllByPlaceholderText('Type a value...').last().type('Georges')
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody()
.findByPlaceholderText('Type your email...')
.type('georges@gmail.com{enter}')
cy.wait('@insertRowInGoogleSheets')
.then((interception) => {
return interception.response?.statusCode
})
.should('eq', 200)
})
it('Update row should work', () => {
cy.intercept({
url: '/api/integrations/google-sheets/spreadsheets/1k_pIDw3YHl9tlZusbBVSBRY0PeRPd2H6t4Nj7rwnOtM/sheets/0',
method: 'PATCH',
}).as('updateRowInGoogleSheets')
cy.loadTypebotFixtureInDatabase('typebots/integrations/googleSheets.json')
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/edit')
fillInSpreadsheetInfo()
cy.findByRole('button', { name: 'Select an operation' }).click()
cy.findByRole('menuitem', { name: 'Update a row' }).click({ force: true })
cy.findAllByRole('button', { name: 'Select a column' }).first().click()
cy.findByRole('menuitem', { name: 'Email' }).click()
cy.findAllByRole('button', { name: 'Insert a variable' }).first().click()
cy.findByRole('menuitem', { name: 'Email' }).click()
cy.findByRole('button', { name: 'Select a column' }).click()
cy.findByRole('menuitem', { name: 'Last name' }).click()
cy.findAllByPlaceholderText('Type a value...').last().type('Last name')
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody()
.findByPlaceholderText('Type your email...')
.type('test@test.com{enter}')
cy.wait('@updateRowInGoogleSheets')
.then((interception) => {
return interception.response?.statusCode
})
.should('eq', 200)
})
it('Get row should work', () => {
cy.loadTypebotFixtureInDatabase(
'typebots/integrations/googleSheetsGet.json'
)
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/edit')
fillInSpreadsheetInfo()
cy.findByRole('button', { name: 'Select an operation' }).click()
cy.findByRole('menuitem', { name: 'Get data from sheet' }).click({
force: true,
})
cy.findAllByRole('button', { name: 'Select a column' }).first().click()
cy.findByRole('menuitem', { name: 'Email' }).click()
cy.findByRole('button', { name: 'Insert a variable' }).click()
cy.findByRole('menuitem', { name: 'Email' }).click()
cy.findByRole('button', { name: 'Select a column' }).click()
cy.findByRole('menuitem', { name: 'First name' }).click()
createNewVar('First name')
cy.findByRole('button', { name: 'Add' }).click()
cy.findByRole('button', { name: 'Select a column' }).click()
cy.findByRole('menuitem', { name: 'Last name' }).click()
createNewVar('Last name')
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody()
.findByPlaceholderText('Type your email...')
.type('test2@test.com{enter}')
getIframeBody().findByText('Your name is: John Smith').should('exist')
})
})
const fillInSpreadsheetInfo = () => {
cy.findByTestId('step-step1').click()
cy.findByRole('button', { name: 'Select an account' }).click()
cy.findByRole('menuitem', { name: 'test2@gmail.com' }).click()
cy.findByPlaceholderText('Search for spreadsheet').type('CR')
cy.findByRole('menuitem', { name: 'CRM' }).click()
cy.findByPlaceholderText('Select the sheet').type('Sh')
cy.findByRole('menuitem', { name: 'Sheet1' }).click()
}
const createNewVar = (name: string) => {
cy.findAllByTestId('variables-input').last().type(name)
cy.findByRole('menuitem', { name: `Create "${name}"` }).click()
}

View File

@ -0,0 +1,28 @@
import { Prisma, Credentials as CredentialsFromDb } from 'db'
import { OAuth2Client, Credentials } from 'google-auth-library'
import prisma from './prisma'
export const oauth2Client = new OAuth2Client(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
`${process.env.NEXTAUTH_URL}/api/credentials/google-sheets/callback`
)
export const getAuthenticatedGoogleClient = async (
userId: string,
credentialsId: string
): Promise<OAuth2Client> => {
const credentials = (await prisma.credentials.findFirst({
where: { id: credentialsId, ownerId: userId },
})) as CredentialsFromDb
oauth2Client.setCredentials(credentials.data as Credentials)
oauth2Client.on('tokens', updateTokens(credentialsId))
return oauth2Client
}
const updateTokens =
(credentialsId: string) => async (credentials: Credentials) =>
prisma.credentials.update({
where: { id: credentialsId },
data: { data: credentials as Prisma.InputJsonValue },
})

View File

@ -1,12 +1,5 @@
import { Link } from '@chakra-ui/react' import { Link } from '@chakra-ui/react'
import { import {
AutoformatRule,
createAutoformatPlugin,
} from '@udecode/plate-autoformat'
import {
MARK_BOLD,
MARK_UNDERLINE,
MARK_ITALIC,
createBoldPlugin, createBoldPlugin,
createItalicPlugin, createItalicPlugin,
createUnderlinePlugin, createUnderlinePlugin,
@ -21,40 +14,12 @@ export const editorStyle: React.CSSProperties = {
borderRadius: '0.25rem', borderRadius: '0.25rem',
} }
export const autoFormatRules: AutoformatRule[] = [
{
mode: 'mark',
type: MARK_BOLD,
match: '**',
},
{
mode: 'mark',
type: MARK_UNDERLINE,
match: '__',
},
{
mode: 'mark',
type: MARK_ITALIC,
match: '*',
},
{
mode: 'mark',
type: MARK_ITALIC,
match: '_',
},
]
export const platePlugins = createPlugins( export const platePlugins = createPlugins(
[ [
createBoldPlugin(), createBoldPlugin(),
createItalicPlugin(), createItalicPlugin(),
createUnderlinePlugin(), createUnderlinePlugin(),
createLinkPlugin(), createLinkPlugin(),
createAutoformatPlugin({
options: {
rules: autoFormatRules,
},
}),
], ],
{ components: { [ELEMENT_LINK]: Link } } { components: { [ELEMENT_LINK]: Link } }
) )

View File

@ -16,8 +16,8 @@
"@dnd-kit/sortable": "^5.1.0", "@dnd-kit/sortable": "^5.1.0",
"@emotion/react": "^11.7.1", "@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0", "@emotion/styled": "^11.6.0",
"@googleapis/drive": "^2.1.0",
"@next-auth/prisma-adapter": "next", "@next-auth/prisma-adapter": "next",
"@udecode/plate-autoformat": "^9.0.0",
"@udecode/plate-basic-marks": "^9.0.0", "@udecode/plate-basic-marks": "^9.0.0",
"@udecode/plate-common": "^7.0.2", "@udecode/plate-common": "^7.0.2",
"@udecode/plate-core": "^9.0.0", "@udecode/plate-core": "^9.0.0",
@ -30,6 +30,8 @@
"fast-equals": "^2.0.4", "fast-equals": "^2.0.4",
"focus-visible": "^5.2.0", "focus-visible": "^5.2.0",
"framer-motion": "^4", "framer-motion": "^4",
"google-auth-library": "^7.11.0",
"google-spreadsheet": "^3.2.0",
"htmlparser2": "^7.2.0", "htmlparser2": "^7.2.0",
"immer": "^9.0.7", "immer": "^9.0.7",
"kbar": "^0.1.0-beta.24", "kbar": "^0.1.0-beta.24",
@ -61,6 +63,7 @@
}, },
"devDependencies": { "devDependencies": {
"@testing-library/cypress": "^8.0.2", "@testing-library/cypress": "^8.0.2",
"@types/google-spreadsheet": "^3.1.5",
"@types/micro-cors": "^0.1.2", "@types/micro-cors": "^0.1.2",
"@types/node": "^16.11.9", "@types/node": "^16.11.9",
"@types/nprogress": "^0.2.0", "@types/nprogress": "^0.2.0",

View File

@ -0,0 +1,62 @@
import { oauth2Client } from 'libs/google-sheets'
import { NextApiRequest, NextApiResponse } from 'next'
import { getSession } from 'next-auth/react'
import { CredentialsType, Prisma, User } from 'db'
import prisma from 'libs/prisma'
import { googleSheetsScopes } from './consent-url'
import { stringify } from 'querystring'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getSession({ req })
const { redirectUrl, stepId } = JSON.parse(
Buffer.from(req.query.state.toString(), 'base64').toString()
)
if (req.method === 'GET') {
const code = req.query.code.toString()
if (!code)
return res.status(400).send({ message: "Bad request, couldn't get code" })
if (!session?.user)
return res.status(401).json({ message: 'Not authenticated' })
const user = session.user as User
const { tokens } = await oauth2Client.getToken(code)
if (!tokens?.access_token) {
console.error('Error getting oAuth tokens:')
throw new Error('ERROR')
}
oauth2Client.setCredentials(tokens)
const { email, scopes } = await oauth2Client.getTokenInfo(
tokens.access_token
)
if (!email)
return res
.status(400)
.send({ message: "Couldn't get email from getTokenInfo" })
if (googleSheetsScopes.some((scope) => !scopes.includes(scope)))
return res
.status(400)
.send({ message: "User didn't accepted required scopes" })
const credentials = {
name: email,
type: CredentialsType.GOOGLE_SHEETS,
ownerId: user.id,
data: tokens as Prisma.InputJsonValue,
}
const { id: credentialsId } = await prisma.credentials.upsert({
create: credentials,
update: credentials,
where: {
name_type_ownerId: {
name: credentials.name,
type: credentials.type,
ownerId: user.id,
},
},
})
const queryParams = stringify({ stepId, credentialsId })
return res.redirect(
`${redirectUrl}?${queryParams}` ?? `${process.env.NEXTAUTH_URL}`
)
}
}
export default handler

View File

@ -0,0 +1,22 @@
import { oauth2Client } from 'libs/google-sheets'
import { NextApiRequest, NextApiResponse } from 'next'
export const googleSheetsScopes = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/spreadsheets',
'https://www.googleapis.com/auth/drive.readonly',
]
const handler = (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
const url = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: googleSheetsScopes,
prompt: 'consent',
state: Buffer.from(JSON.stringify(req.query)).toString('base64'),
})
return res.status(301).redirect(url)
}
}
export default handler

View File

@ -0,0 +1,30 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { drive } from '@googleapis/drive'
import { getAuthenticatedGoogleClient } from 'libs/google-sheets'
import { methodNotAllowed } from 'utils'
import { getSession } from 'next-auth/react'
import { User } from 'db'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getSession({ req })
if (!session?.user)
return res.status(401).json({ message: 'Not authenticated' })
const user = session.user as User
if (req.method === 'GET') {
const credentialsId = req.query.credentialsId.toString()
const auth = await getAuthenticatedGoogleClient(user.id, credentialsId)
const { data } = await drive({
version: 'v3',
auth,
}).files.list({
q: "mimeType='application/vnd.google-apps.spreadsheet'",
fields: 'nextPageToken, files(id, name)',
})
return res.send(data)
}
return methodNotAllowed(res)
}
export default handler

View File

@ -0,0 +1,41 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { GoogleSpreadsheet } from 'google-spreadsheet'
import { getAuthenticatedGoogleClient } from 'libs/google-sheets'
import { methodNotAllowed } from 'utils'
import { getSession } from 'next-auth/react'
import { User } from 'db'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getSession({ req })
if (!session?.user)
return res.status(401).json({ message: 'Not authenticated' })
const user = session.user as User
if (req.method === 'GET') {
const credentialsId = req.query.credentialsId.toString()
const spreadsheetId = req.query.id.toString()
const doc = new GoogleSpreadsheet(spreadsheetId)
doc.useOAuth2Client(
await getAuthenticatedGoogleClient(user.id, credentialsId)
)
await doc.loadInfo()
return res.send({
sheets: await Promise.all(
Array.from(Array(doc.sheetCount)).map(async (_, idx) => {
const sheet = doc.sheetsByIndex[idx]
await sheet.loadHeaderRow()
return {
id: sheet.sheetId,
name: sheet.title,
columns: sheet.headerValues,
}
})
),
})
}
return methodNotAllowed(res)
}
export default handler

View File

@ -0,0 +1,25 @@
import { User } from 'db'
import prisma from 'libs/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getSession } from 'next-auth/react'
import { methodNotAllowed } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getSession({ req })
if (!session?.user)
return res.status(401).json({ message: 'Not authenticated' })
const user = session.user as User
const id = req.query.id.toString()
if (user.id !== id) return res.status(401).send({ message: 'Forbidden' })
if (req.method === 'GET') {
const credentials = await prisma.credentials.findMany({
where: { ownerId: user.id },
})
return res.send({ credentials })
}
return methodNotAllowed(res)
}
export default handler

View File

@ -0,0 +1,22 @@
import { Credentials } from 'db'
import useSWR from 'swr'
import { fetcher } from './utils'
export const useCredentials = ({
userId,
onError,
}: {
userId?: string
onError: (error: Error) => void
}) => {
const { data, error, mutate } = useSWR<{ credentials: Credentials[] }, Error>(
userId ? `/api/users/${userId}/credentials` : null,
fetcher
)
if (error) onError(error)
return {
credentials: data?.credentials,
isLoading: !error && !data,
mutate,
}
}

View File

@ -1,7 +1,8 @@
import { DashboardFolder } from '.prisma/client' import { DashboardFolder } from '.prisma/client'
import useSWR from 'swr' import useSWR from 'swr'
import { fetcher, sendRequest } from './utils' import { fetcher } from './utils'
import { stringify } from 'qs' import { stringify } from 'qs'
import { sendRequest } from 'utils'
export const useFolders = ({ export const useFolders = ({
parentId, parentId,

View File

@ -0,0 +1,66 @@
import { sendRequest } from 'utils'
import { stringify } from 'qs'
import useSWR from 'swr'
import { fetcher } from './utils'
export const getGoogleSheetsConsentScreenUrl = (
redirectUrl: string,
stepId: string
) => {
const queryParams = stringify({ redirectUrl, stepId })
return `/api/credentials/google-sheets/consent-url?${queryParams}`
}
export const createSheetsAccount = async (code: string) => {
const queryParams = stringify({ code })
return sendRequest({
url: `/api/credentials/google-sheets/callback?${queryParams}`,
method: 'GET',
})
}
export type Spreadsheet = { id: string; name: string }
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,
}
}
export type Sheet = { id: string; name: string; columns: string[] }
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

@ -1,10 +1,9 @@
import { PublicTypebot, Typebot } from 'models' import { PublicTypebot, Typebot } from 'models'
import { sendRequest } from './utils'
import shortId from 'short-uuid' import shortId from 'short-uuid'
import { HStack, Text } from '@chakra-ui/react' import { HStack, Text } from '@chakra-ui/react'
import { CalendarIcon } from 'assets/icons' import { CalendarIcon } from 'assets/icons'
import { StepIcon } from 'components/board/StepTypesList/StepIcon' import { StepIcon } from 'components/board/StepTypesList/StepIcon'
import { isInputStep } from 'utils' import { isInputStep, sendRequest } from 'utils'
export const parseTypebotToPublicTypebot = ( export const parseTypebotToPublicTypebot = (
typebot: Typebot typebot: Typebot

View File

@ -1,8 +1,9 @@
import { Result } from 'models' import { Result } from 'models'
import useSWRInfinite from 'swr/infinite' import useSWRInfinite from 'swr/infinite'
import { fetcher, sendRequest } from './utils' import { fetcher } from './utils'
import { stringify } from 'qs' import { stringify } from 'qs'
import { Answer } from 'db' import { Answer } from 'db'
import { sendRequest } from 'utils'
const getKey = ( const getKey = (
typebotId: string, typebotId: string,

View File

@ -6,25 +6,24 @@ import {
Settings, Settings,
StartStep, StartStep,
Theme, Theme,
BubbleStep,
InputStep,
BubbleStepType, BubbleStepType,
InputStepType, InputStepType,
ChoiceInputStep, ChoiceInputStep,
LogicStepType, LogicStepType,
LogicStep,
Step, Step,
ConditionStep, ConditionStep,
ComparisonOperators, ComparisonOperators,
LogicalOperator, LogicalOperator,
DraggableStepType,
DraggableStep,
} from 'models' } from 'models'
import shortId, { generate } from 'short-uuid' import shortId, { generate } from 'short-uuid'
import { Typebot } from 'models' import { Typebot } from 'models'
import useSWR from 'swr' import useSWR from 'swr'
import { fetcher, sendRequest, toKebabCase } from './utils' import { fetcher, toKebabCase } from './utils'
import { deepEqual } from 'fast-equals' import { deepEqual } from 'fast-equals'
import { stringify } from 'qs' import { stringify } from 'qs'
import { isChoiceInput, isConditionStep } from 'utils' import { isChoiceInput, isConditionStep, sendRequest } from 'utils'
export const useTypebots = ({ export const useTypebots = ({
folderId, folderId,
@ -114,9 +113,9 @@ export const parseNewBlock = ({
} }
export const parseNewStep = ( export const parseNewStep = (
type: BubbleStepType | InputStepType | LogicStepType, type: DraggableStepType,
blockId: string blockId: string
): BubbleStep | InputStep | LogicStep => { ): DraggableStep => {
const id = `s${shortId.generate()}` const id = `s${shortId.generate()}`
switch (type) { switch (type) {
case BubbleStepType.TEXT: { case BubbleStepType.TEXT: {

View File

@ -1,5 +1,5 @@
import { User } from 'db' import { User } from 'db'
import { sendRequest } from './utils' import { sendRequest } from 'utils'
export const updateUser = async (id: string, user: User) => export const updateUser = async (id: string, user: User) =>
sendRequest({ sendRequest({

View File

@ -10,36 +10,6 @@ export const isMobile =
typeof window !== 'undefined' && typeof window !== 'undefined' &&
window.matchMedia('only screen and (max-width: 760px)').matches window.matchMedia('only screen and (max-width: 760px)').matches
export const sendRequest = async <ResponseData>({
url,
method,
body,
}: {
url: string
method: string
body?: Record<string, unknown>
}): Promise<{ data?: ResponseData; error?: Error }> => {
try {
const response = await fetch(url, {
method,
mode: 'cors',
body: body ? JSON.stringify(body) : undefined,
})
if (!response.ok) throw new Error(response.statusText)
const data = await response.json()
return { data }
} catch (e) {
console.error(e)
return { error: e as Error }
}
}
export const insertItemInList = <T>(
arr: T[],
index: number,
newItem: T
): T[] => [...arr.slice(0, index), newItem, ...arr.slice(index)]
export const preventUserFromRefreshing = (e: BeforeUnloadEvent) => { export const preventUserFromRefreshing = (e: BeforeUnloadEvent) => {
e.preventDefault() e.preventDefault()
e.returnValue = '' e.returnValue = ''

View File

@ -0,0 +1,27 @@
import { Prisma, Credentials as CredentialsFromDb } from 'db'
import { OAuth2Client, Credentials } from 'google-auth-library'
import prisma from './prisma'
export const oauth2Client = new OAuth2Client(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
`${process.env.NEXTAUTH_URL}/api/credentials/google-sheets/callback`
)
export const getAuthenticatedGoogleClient = async (
credentialsId: string
): Promise<OAuth2Client> => {
const credentials = (await prisma.credentials.findFirst({
where: { id: credentialsId },
})) as CredentialsFromDb
oauth2Client.setCredentials(credentials.data as Credentials)
oauth2Client.on('tokens', updateTokens(credentialsId))
return oauth2Client
}
const updateTokens =
(credentialsId: string) => async (credentials: Credentials) =>
prisma.credentials.update({
where: { id: credentialsId },
data: { data: credentials as Prisma.InputJsonValue },
})

View File

@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"bot-engine": "*", "bot-engine": "*",
"db": "*", "db": "*",
"google-spreadsheet": "^3.2.0",
"models": "*", "models": "*",
"next": "^12.0.7", "next": "^12.0.7",
"react": "^17.0.2", "react": "^17.0.2",
@ -18,6 +19,7 @@
"utils": "*" "utils": "*"
}, },
"devDependencies": { "devDependencies": {
"@types/google-spreadsheet": "^3.1.5",
"@types/node": "^17.0.4", "@types/node": "^17.0.4",
"@types/react": "^17.0.38", "@types/react": "^17.0.38",
"@typescript-eslint/eslint-plugin": "^5.9.0", "@typescript-eslint/eslint-plugin": "^5.9.0",

View File

@ -0,0 +1,75 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed } from 'utils'
import { GoogleSpreadsheet } from 'google-spreadsheet'
import { getAuthenticatedGoogleClient } from 'libs/google-sheets'
import { Cell } from 'models'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
const spreadsheetId = req.query.spreadsheetId.toString()
const sheetId = req.query.sheetId.toString()
const credentialsId = req.query.credentialsId.toString()
const referenceCell = {
column: req.query['referenceCell[column]'],
value: req.query['referenceCell[value]'],
} as Cell
console.log(referenceCell)
const extractingColumns = req.query.columns as string[]
const doc = new GoogleSpreadsheet(spreadsheetId)
doc.useOAuth2Client(await getAuthenticatedGoogleClient(credentialsId))
await doc.loadInfo()
const sheet = doc.sheetsById[sheetId]
const rows = await sheet.getRows()
const row = rows.find(
(row) => row[referenceCell.column as string] === referenceCell.value
)
if (!row) return res.status(404).send({ message: "Couldn't find row" })
return res.send({
...extractingColumns.reduce(
(obj, column) => ({ ...obj, [column]: row[column] }),
{}
),
})
}
if (req.method === 'POST') {
const spreadsheetId = req.query.spreadsheetId.toString()
const sheetId = req.query.sheetId.toString()
const { credentialsId, values } = JSON.parse(req.body) as {
credentialsId: string
values: { [key: string]: string }
}
const doc = new GoogleSpreadsheet(spreadsheetId)
doc.useOAuth2Client(await getAuthenticatedGoogleClient(credentialsId))
await doc.loadInfo()
const sheet = doc.sheetsById[sheetId]
await sheet.addRow(values)
return res.send({ message: 'Success' })
}
if (req.method === 'PATCH') {
const spreadsheetId = req.query.spreadsheetId.toString()
const sheetId = req.query.sheetId.toString()
const { credentialsId, values, referenceCell } = JSON.parse(req.body) as {
credentialsId: string
referenceCell: Cell
values: { [key: string]: string }
}
const doc = new GoogleSpreadsheet(spreadsheetId)
doc.useOAuth2Client(await getAuthenticatedGoogleClient(credentialsId))
await doc.loadInfo()
const sheet = doc.sheetsById[sheetId]
const rows = await sheet.getRows()
const updatingRowIndex = rows.findIndex(
(row) => row[referenceCell.column as string] === referenceCell.value
)
if (updatingRowIndex === -1)
return res.status(404).send({ message: "Couldn't find row to update" })
for (const key in values) {
rows[updatingRowIndex][key] = values[key]
}
await rows[updatingRowIndex].save()
return res.send({ message: 'Success' })
}
return methodNotAllowed(res)
}
export default handler

View File

@ -16,7 +16,7 @@
}, },
"devDependencies": { "devDependencies": {
"dotenv-cli": "^4.1.1", "dotenv-cli": "^4.1.1",
"turbo": "^1.0.24" "turbo": "^1.0.28"
}, },
"turbo": { "turbo": {
"baseBranch": "origin/main", "baseBranch": "origin/main",

View File

@ -9,6 +9,7 @@
"db": "*", "db": "*",
"fast-equals": "^2.0.4", "fast-equals": "^2.0.4",
"models": "*", "models": "*",
"qs": "^6.10.3",
"react-frame-component": "5.2.2-alpha.0", "react-frame-component": "5.2.2-alpha.0",
"react-phone-number-input": "^3.1.44", "react-phone-number-input": "^3.1.44",
"react-scroll": "^1.8.4", "react-scroll": "^1.8.4",

View File

@ -4,29 +4,18 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group'
import { ChatStep } from './ChatStep' import { ChatStep } from './ChatStep'
import { AvatarSideContainer } from './AvatarSideContainer' import { AvatarSideContainer } from './AvatarSideContainer'
import { HostAvatarsContext } from '../../contexts/HostAvatarsContext' import { HostAvatarsContext } from '../../contexts/HostAvatarsContext'
import { import { Step, Target } from 'models'
ChoiceInputStep,
ComparisonOperators,
ConditionStep,
LogicalOperator,
LogicStep,
LogicStepType,
Step,
Target,
} from 'models'
import { useTypebot } from '../../contexts/TypebotContext' import { useTypebot } from '../../contexts/TypebotContext'
import { import {
isChoiceInput, isChoiceInput,
isDefined,
isInputStep, isInputStep,
isIntegrationStep,
isLogicStep, isLogicStep,
isTextBubbleStep, isTextBubbleStep,
} from 'utils' } from 'utils'
import { import { executeLogic } from 'services/logic'
evaluateExpression, import { getSingleChoiceTargetId } from 'services/inputs'
isMathFormula, import { executeIntegration } from 'services/integration'
parseVariables,
} from 'services/variable'
type ChatBlockProps = { type ChatBlockProps = {
stepIds: string[] stepIds: string[]
@ -50,12 +39,29 @@ export const ChatBlock = ({
useEffect(() => { useEffect(() => {
autoScrollToBottom() autoScrollToBottom()
onNewStepDisplayed()
}, [displayedSteps])
const onNewStepDisplayed = async () => {
const currentStep = [...displayedSteps].pop() const currentStep = [...displayedSteps].pop()
if (currentStep && isLogicStep(currentStep)) { if (!currentStep) return
const target = executeLogic(currentStep) if (isLogicStep(currentStep)) {
const target = executeLogic(
currentStep,
typebot.variables,
updateVariableValue
)
target ? onBlockEnd(target) : displayNextStep() target ? onBlockEnd(target) : displayNextStep()
} }
}, [displayedSteps]) if (isIntegrationStep(currentStep)) {
const target = await executeIntegration(
currentStep,
typebot.variables,
updateVariableValue
)
target ? onBlockEnd(target) : displayNextStep()
}
}
const autoScrollToBottom = () => { const autoScrollToBottom = () => {
scroll.scrollToBottom({ scroll.scrollToBottom({
@ -77,7 +83,13 @@ export const ChatBlock = ({
const isSingleChoiceStep = const isSingleChoiceStep =
isChoiceInput(currentStep) && !currentStep.options.isMultipleChoice isChoiceInput(currentStep) && !currentStep.options.isMultipleChoice
if (isSingleChoiceStep) if (isSingleChoiceStep)
return onBlockEnd(getSingleChoiceTargetId(currentStep, answerContent)) return onBlockEnd(
getSingleChoiceTargetId(
currentStep,
typebot.choiceItems,
answerContent
)
)
if ( if (
currentStep?.target?.blockId || currentStep?.target?.blockId ||
displayedSteps.length === stepIds.length displayedSteps.length === stepIds.length
@ -88,67 +100,6 @@ export const ChatBlock = ({
if (nextStep) setDisplayedSteps([...displayedSteps, nextStep]) if (nextStep) setDisplayedSteps([...displayedSteps, nextStep])
} }
const executeLogic = (step: LogicStep): Target | undefined => {
switch (step.type) {
case LogicStepType.SET_VARIABLE: {
if (!step.options?.variableId || !step.options.expressionToEvaluate)
return
const expression = step.options.expressionToEvaluate
const evaluatedExpression = isMathFormula(expression)
? evaluateExpression(parseVariables(expression, typebot.variables))
: expression
updateVariableValue(step.options.variableId, evaluatedExpression)
return
}
case LogicStepType.CONDITION: {
const isConditionPassed =
step.options?.logicalOperator === LogicalOperator.AND
? step.options?.comparisons.allIds.every(executeComparison(step))
: step.options?.comparisons.allIds.some(executeComparison(step))
return isConditionPassed ? step.trueTarget : step.falseTarget
}
}
}
const executeComparison = (step: ConditionStep) => (comparisonId: string) => {
const comparison = step.options?.comparisons.byId[comparisonId]
if (!comparison?.variableId) return false
const inputValue = typebot.variables.byId[comparison.variableId].value ?? ''
const { value } = comparison
if (!isDefined(value)) return false
switch (comparison.comparisonOperator) {
case ComparisonOperators.CONTAINS: {
return inputValue.includes(value)
}
case ComparisonOperators.EQUAL: {
return inputValue === value
}
case ComparisonOperators.NOT_EQUAL: {
return inputValue !== value
}
case ComparisonOperators.GREATER: {
return parseFloat(inputValue) >= parseFloat(value)
}
case ComparisonOperators.LESS: {
return parseFloat(inputValue) <= parseFloat(value)
}
case ComparisonOperators.IS_SET: {
return isDefined(inputValue) && inputValue.length > 0
}
}
}
const getSingleChoiceTargetId = (
currentStep: ChoiceInputStep,
answerContent?: string
): Target | undefined => {
const itemId = currentStep.options.itemIds.find(
(itemId) => typebot.choiceItems.byId[itemId].content === answerContent
)
if (!itemId) throw new Error('itemId should exist')
return typebot.choiceItems.byId[itemId].target ?? currentStep.target
}
return ( return (
<div className="flex"> <div className="flex">
<HostAvatarsContext> <HostAvatarsContext>

View File

@ -0,0 +1,13 @@
import { ChoiceInputStep, ChoiceItem, Table, Target } from 'models'
export const getSingleChoiceTargetId = (
currentStep: ChoiceInputStep,
choiceItems: Table<ChoiceItem>,
answerContent?: string
): Target | undefined => {
const itemId = currentStep.options.itemIds.find(
(itemId) => choiceItems.byId[itemId].content === answerContent
)
if (!itemId) throw new Error('itemId should exist')
return choiceItems.byId[itemId].target ?? currentStep.target
}

View File

@ -0,0 +1,121 @@
import {
IntegrationStep,
IntegrationStepType,
GoogleSheetsStep,
GoogleSheetsAction,
GoogleSheetsInsertRowOptions,
Variable,
Table,
GoogleSheetsUpdateRowOptions,
Cell,
GoogleSheetsGetOptions,
} from 'models'
import { stringify } from 'qs'
import { sendRequest } from 'utils'
import { parseVariables } from './variable'
export const executeIntegration = (
step: IntegrationStep,
variables: Table<Variable>,
updateVariableValue: (variableId: string, value: string) => void
) => {
switch (step.type) {
case IntegrationStepType.GOOGLE_SHEETS:
return executeGoogleSheetIntegration(step, variables, updateVariableValue)
}
}
const executeGoogleSheetIntegration = async (
step: GoogleSheetsStep,
variables: Table<Variable>,
updateVariableValue: (variableId: string, value: string) => void
) => {
if (!step.options) return step.target
switch (step.options?.action) {
case GoogleSheetsAction.INSERT_ROW:
await insertRowInGoogleSheets(step.options, variables)
break
case GoogleSheetsAction.UPDATE_ROW:
await updateRowInGoogleSheets(step.options, variables)
break
case GoogleSheetsAction.GET:
await getRowFromGoogleSheets(step.options, variables, updateVariableValue)
break
}
return step.target
}
const insertRowInGoogleSheets = async (
options: GoogleSheetsInsertRowOptions,
variables: Table<Variable>
) => {
if (!options.cellsToInsert) return
return sendRequest({
url: `http://localhost:3001/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}`,
method: 'POST',
body: {
credentialsId: options.credentialsId,
values: parseCellValues(options.cellsToInsert, variables),
},
})
}
const updateRowInGoogleSheets = async (
options: GoogleSheetsUpdateRowOptions,
variables: Table<Variable>
) => {
if (!options.cellsToUpsert || !options.referenceCell) return
return sendRequest({
url: `http://localhost:3001/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}`,
method: 'PATCH',
body: {
credentialsId: options.credentialsId,
values: parseCellValues(options.cellsToUpsert, variables),
referenceCell: {
column: options.referenceCell.column,
value: parseVariables(options.referenceCell.value ?? '', variables),
},
},
})
}
const getRowFromGoogleSheets = async (
options: GoogleSheetsGetOptions,
variables: Table<Variable>,
updateVariableValue: (variableId: string, value: string) => void
) => {
if (!options.referenceCell || !options.cellsToExtract) return
const queryParams = stringify(
{
credentialsId: options.credentialsId,
referenceCell: {
column: options.referenceCell.column,
value: parseVariables(options.referenceCell.value ?? '', variables),
},
columns: options.cellsToExtract.allIds.map(
(id) => options.cellsToExtract?.byId[id].column
),
},
{ indices: false }
)
const { data } = await sendRequest<{ [key: string]: string }>({
url: `http://localhost:3001/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}?${queryParams}`,
method: 'GET',
})
if (!data) return
options.cellsToExtract.allIds.forEach((cellId) => {
const cell = options.cellsToExtract?.byId[cellId]
if (!cell) return
updateVariableValue(cell.variableId ?? '', data[cell.column ?? ''])
})
}
const parseCellValues = (
cells: Table<Cell>,
variables: Table<Variable>
): { [key: string]: string } =>
cells.allIds.reduce((row, id) => {
const cell = cells.byId[id]
return !cell.column || !cell.value
? row
: { ...row, [cell.column]: parseVariables(cell.value, variables) }
}, {})

View File

@ -0,0 +1,72 @@
import {
LogicStep,
Target,
LogicStepType,
LogicalOperator,
ConditionStep,
Table,
Variable,
ComparisonOperators,
} from 'models'
import { isDefined } from 'utils'
import { isMathFormula, evaluateExpression, parseVariables } from './variable'
export const executeLogic = (
step: LogicStep,
variables: Table<Variable>,
updateVariableValue: (variableId: string, expression: string) => void
): Target | undefined => {
switch (step.type) {
case LogicStepType.SET_VARIABLE: {
if (!step.options?.variableId || !step.options.expressionToEvaluate)
return
const expression = step.options.expressionToEvaluate
const evaluatedExpression = isMathFormula(expression)
? evaluateExpression(parseVariables(expression, variables))
: expression
updateVariableValue(step.options.variableId, evaluatedExpression)
return
}
case LogicStepType.CONDITION: {
const isConditionPassed =
step.options?.logicalOperator === LogicalOperator.AND
? step.options?.comparisons.allIds.every(
executeComparison(step, variables)
)
: step.options?.comparisons.allIds.some(
executeComparison(step, variables)
)
return isConditionPassed ? step.trueTarget : step.falseTarget
}
}
}
const executeComparison =
(step: ConditionStep, variables: Table<Variable>) =>
(comparisonId: string) => {
const comparison = step.options?.comparisons.byId[comparisonId]
if (!comparison?.variableId) return false
const inputValue = variables.byId[comparison.variableId].value ?? ''
const { value } = comparison
if (!isDefined(value)) return false
switch (comparison.comparisonOperator) {
case ComparisonOperators.CONTAINS: {
return inputValue.includes(value)
}
case ComparisonOperators.EQUAL: {
return inputValue === value
}
case ComparisonOperators.NOT_EQUAL: {
return inputValue !== value
}
case ComparisonOperators.GREATER: {
return parseFloat(inputValue) >= parseFloat(value)
}
case ComparisonOperators.LESS: {
return parseFloat(inputValue) <= parseFloat(value)
}
case ComparisonOperators.IS_SET: {
return isDefined(inputValue) && inputValue.length > 0
}
}
}

View File

@ -49,6 +49,22 @@ model User {
folders DashboardFolder[] folders DashboardFolder[]
plan Plan @default(FREE) plan Plan @default(FREE)
stripeId String? @unique stripeId String? @unique
credentials Credentials[]
}
model Credentials {
id String @id @default(cuid())
ownerId String
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
data Json
name String
type CredentialsType
@@unique([name, type, ownerId])
}
enum CredentialsType {
GOOGLE_SHEETS
} }
enum Plan { enum Plan {

View File

@ -0,0 +1,12 @@
import { StepBase } from '.'
export type BubbleStep = TextStep
export enum BubbleStepType {
TEXT = 'text',
}
export type TextStep = StepBase & {
type: BubbleStepType.TEXT
content: { html: string; richText: unknown[]; plainText: string }
}

View File

@ -1,3 +1,5 @@
export * from './steps' export * from './steps'
export * from './bubble'
export * from './inputs' export * from './inputs'
export * from './logic' export * from './logic'
export * from './integration'

View File

@ -20,6 +20,13 @@ export enum InputStepType {
CHOICE = 'choice input', CHOICE = 'choice input',
} }
export type InputStepOptions =
| TextInputOptions
| NumberInputOptions
| EmailInputOptions
| DateInputOptions
| UrlInputOptions
export type TextInputStep = StepBase & { export type TextInputStep = StepBase & {
type: InputStepType.TEXT type: InputStepType.TEXT
options?: TextInputOptions options?: TextInputOptions

View File

@ -0,0 +1,52 @@
import { StepBase } from '.'
import { Table } from '../..'
export type IntegrationStep = GoogleSheetsStep
export type IntegrationStepOptions = GoogleSheetsOptions
export enum IntegrationStepType {
GOOGLE_SHEETS = 'Google Sheets',
}
export type GoogleSheetsStep = StepBase & {
type: IntegrationStepType.GOOGLE_SHEETS
options?: GoogleSheetsOptions
}
export enum GoogleSheetsAction {
GET = 'Get data from sheet',
INSERT_ROW = 'Insert a row',
UPDATE_ROW = 'Update a row',
}
export type GoogleSheetsOptions =
| GoogleSheetsGetOptions
| GoogleSheetsInsertRowOptions
| GoogleSheetsUpdateRowOptions
type GoogleSheetsOptionsBase = {
credentialsId?: string
spreadsheetId?: string
sheetId?: string
}
export type Cell = { column?: string; value?: string }
export type ExtractingCell = { column?: string; variableId?: string }
export type GoogleSheetsGetOptions = GoogleSheetsOptionsBase & {
action?: GoogleSheetsAction.GET
referenceCell?: Cell
cellsToExtract?: Table<ExtractingCell>
}
export type GoogleSheetsInsertRowOptions = GoogleSheetsOptionsBase & {
action?: GoogleSheetsAction.INSERT_ROW
cellsToInsert?: Table<Cell>
}
export type GoogleSheetsUpdateRowOptions = GoogleSheetsOptionsBase & {
action?: GoogleSheetsAction.UPDATE_ROW
referenceCell?: Cell
cellsToUpsert?: Table<Cell>
}

View File

@ -8,6 +8,8 @@ export enum LogicStepType {
CONDITION = 'Condition', CONDITION = 'Condition',
} }
export type LogicStepOptions = SetVariableOptions | ConditionOptions
export type SetVariableStep = StepBase & { export type SetVariableStep = StepBase & {
type: LogicStepType.SET_VARIABLE type: LogicStepType.SET_VARIABLE
options?: SetVariableOptions options?: SetVariableOptions

View File

@ -1,15 +1,40 @@
import {
InputStepOptions,
IntegrationStepOptions,
IntegrationStepType,
LogicStepOptions,
} from '.'
import { BubbleStep, BubbleStepType } from './bubble'
import { InputStep, InputStepType } from './inputs' import { InputStep, InputStepType } from './inputs'
import { IntegrationStep } from './integration'
import { LogicStep, LogicStepType } from './logic' import { LogicStep, LogicStepType } from './logic'
export type Step = StartStep | BubbleStep | InputStep | LogicStep export type Step =
| StartStep
| BubbleStep
| InputStep
| LogicStep
| IntegrationStep
export type BubbleStep = TextStep export type DraggableStep = BubbleStep | InputStep | LogicStep | IntegrationStep
export type StepType = 'start' | BubbleStepType | InputStepType | LogicStepType export type StepType =
| 'start'
| BubbleStepType
| InputStepType
| LogicStepType
| IntegrationStepType
export enum BubbleStepType { export type DraggableStepType =
TEXT = 'text', | BubbleStepType
} | InputStepType
| LogicStepType
| IntegrationStepType
export type StepOptions =
| InputStepOptions
| LogicStepOptions
| IntegrationStepOptions
export type StepBase = { id: string; blockId: string; target?: Target } export type StepBase = { id: string; blockId: string; target?: Target }
@ -18,9 +43,4 @@ export type StartStep = StepBase & {
label: string label: string
} }
export type TextStep = StepBase & {
type: BubbleStepType.TEXT
content: { html: string; richText: unknown[]; plainText: string }
}
export type Target = { blockId: string; stepId?: string } export type Target = { blockId: string; stepId?: string }

View File

@ -5,6 +5,8 @@ import {
ConditionStep, ConditionStep,
InputStep, InputStep,
InputStepType, InputStepType,
IntegrationStep,
IntegrationStepType,
LogicStep, LogicStep,
LogicStepType, LogicStepType,
Step, Step,
@ -69,3 +71,6 @@ export const isSingleChoiceInput = (step: Step): step is ChoiceInputStep =>
export const isConditionStep = (step: Step): step is ConditionStep => export const isConditionStep = (step: Step): step is ConditionStep =>
step.type === LogicStepType.CONDITION step.type === LogicStepType.CONDITION
export const isIntegrationStep = (step: Step): step is IntegrationStep =>
step.type === IntegrationStepType.GOOGLE_SHEETS

371
yarn.lock
View File

@ -939,6 +939,13 @@
minimatch "^3.0.4" minimatch "^3.0.4"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@googleapis/drive@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@googleapis/drive/-/drive-2.1.0.tgz#2c62ce6184784f433725810edc5dc1798f9da4a3"
integrity sha512-k+4URSgxStPUW01XyzYOf6pNfPC9sT/GN0RbsI3DwaR05bGimkXYDdMe8ATcrfFbr/rs6D1eGuN8uxfn1MiVZA==
dependencies:
googleapis-common "^5.0.1"
"@hapi/accept@5.0.2": "@hapi/accept@5.0.2":
version "5.0.2" version "5.0.2"
resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.2.tgz#ab7043b037e68b722f93f376afb05e85c0699523" resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.2.tgz#ab7043b037e68b722f93f376afb05e85c0699523"
@ -1377,6 +1384,11 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
"@types/google-spreadsheet@^3.1.5":
version "3.1.5"
resolved "https://registry.yarnpkg.com/@types/google-spreadsheet/-/google-spreadsheet-3.1.5.tgz#2bdc6f9f5372551e0506cb6ef3f562adcf44fc2e"
integrity sha512-7N+mDtZ1pmya2RRFPPl4KYc2TRgiqCNBLUZfyrKfER+u751JgCO+C24/LzF70UmUm/zhHUbzRZ5mtfaxekQ1ZQ==
"@types/is-hotkey@^0.1.1": "@types/is-hotkey@^0.1.1":
version "0.1.7" version "0.1.7"
resolved "https://registry.yarnpkg.com/@types/is-hotkey/-/is-hotkey-0.1.7.tgz#30ec6d4234895230b576728ef77e70a52962f3b3" resolved "https://registry.yarnpkg.com/@types/is-hotkey/-/is-hotkey-0.1.7.tgz#30ec6d4234895230b576728ef77e70a52962f3b3"
@ -1677,13 +1689,6 @@
"@typescript-eslint/types" "5.9.0" "@typescript-eslint/types" "5.9.0"
eslint-visitor-keys "^3.0.0" eslint-visitor-keys "^3.0.0"
"@udecode/plate-autoformat@^9.0.0":
version "9.0.0"
resolved "https://registry.yarnpkg.com/@udecode/plate-autoformat/-/plate-autoformat-9.0.0.tgz#12ecaace78bd0f202cfbc0c3ea03f4b8ce20dc09"
integrity sha512-u4TFs/nWIFN74er/IY052A3fCeOAfOY37CfU2dze5HOAZI4biMrvhK+kdFZStq1HVTElrpiDlFNxbht/IOhJAQ==
dependencies:
"@udecode/plate-core" "9.0.0"
"@udecode/plate-basic-marks@^9.0.0": "@udecode/plate-basic-marks@^9.0.0":
version "9.0.0" version "9.0.0"
resolved "https://registry.yarnpkg.com/@udecode/plate-basic-marks/-/plate-basic-marks-9.0.0.tgz#42abc3a2671c6ba2cf3914f2a6cca3d2ce0d5503" resolved "https://registry.yarnpkg.com/@udecode/plate-basic-marks/-/plate-basic-marks-9.0.0.tgz#42abc3a2671c6ba2cf3914f2a6cca3d2ce0d5503"
@ -1785,6 +1790,13 @@
resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d" resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d"
integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ== integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==
abort-controller@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
dependencies:
event-target-shim "^5.0.0"
acorn-jsx@^5.3.1: acorn-jsx@^5.3.1:
version "5.3.2" version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@ -1829,6 +1841,13 @@ agent-base@5:
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c"
integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
dependencies:
debug "4"
aggregate-error@^3.0.0: aggregate-error@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
@ -1997,6 +2016,11 @@ array.prototype.flatmap@^1.2.5:
define-properties "^1.1.3" define-properties "^1.1.3"
es-abstract "^1.19.0" es-abstract "^1.19.0"
arrify@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
asn1.js@^5.2.0: asn1.js@^5.2.0:
version "5.4.1" version "5.4.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
@ -2106,6 +2130,13 @@ axe-core@^4.3.5:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5"
integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA== integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA==
axios@^0.21.4:
version "0.21.4"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==
dependencies:
follow-redirects "^1.14.0"
axobject-query@^2.2.0: axobject-query@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@ -2140,7 +2171,7 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base64-js@^1.0.2: base64-js@^1.0.2, base64-js@^1.3.0:
version "1.5.1" version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
@ -2157,6 +2188,11 @@ big.js@^5.2.2:
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
bignumber.js@^9.0.0:
version "9.0.2"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.2.tgz#71c6c6bed38de64e24a65ebe16cfcf23ae693673"
integrity sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==
binary-extensions@^2.0.0: binary-extensions@^2.0.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
@ -2295,6 +2331,11 @@ buffer-crc32@~0.2.3:
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
buffer-equal-constant-time@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
buffer-from@^1.0.0: buffer-from@^1.0.0:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
@ -3205,6 +3246,13 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0" jsbn "~0.1.0"
safer-buffer "^2.1.0" safer-buffer "^2.1.0"
ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
dependencies:
safe-buffer "^5.0.1"
electron-to-chromium@^1.3.723, electron-to-chromium@^1.4.17: electron-to-chromium@^1.3.723, electron-to-chromium@^1.4.17:
version "1.4.36" version "1.4.36"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.36.tgz#446c6184dbe5baeb5eae9a875490831e4bc5319a" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.36.tgz#446c6184dbe5baeb5eae9a875490831e4bc5319a"
@ -3615,6 +3663,11 @@ etag@1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
event-target-shim@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
eventemitter2@^6.4.3: eventemitter2@^6.4.3:
version "6.4.5" version "6.4.5"
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.5.tgz#97380f758ae24ac15df8353e0cc27f8b95644655" resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.5.tgz#97380f758ae24ac15df8353e0cc27f8b95644655"
@ -3665,7 +3718,7 @@ executable@^4.1.1:
dependencies: dependencies:
pify "^2.2.0" pify "^2.2.0"
extend@~3.0.2: extend@^3.0.2, extend@~3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
@ -3742,6 +3795,11 @@ fast-shallow-equal@^1.0.0:
resolved "https://registry.yarnpkg.com/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b" resolved "https://registry.yarnpkg.com/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b"
integrity sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw== integrity sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==
fast-text-encoding@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53"
integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==
fastest-stable-stringify@^2.0.2: fastest-stable-stringify@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz#3757a6774f6ec8de40c4e86ec28ea02417214c76" resolved "https://registry.yarnpkg.com/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz#3757a6774f6ec8de40c4e86ec28ea02417214c76"
@ -3836,6 +3894,11 @@ focus-visible@^5.2.0:
resolved "https://registry.yarnpkg.com/focus-visible/-/focus-visible-5.2.0.tgz#3a9e41fccf587bd25dcc2ef045508284f0a4d6b3" resolved "https://registry.yarnpkg.com/focus-visible/-/focus-visible-5.2.0.tgz#3a9e41fccf587bd25dcc2ef045508284f0a4d6b3"
integrity sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ== integrity sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==
follow-redirects@^1.14.0:
version "1.14.7"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685"
integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==
foreach@^2.0.5: foreach@^2.0.5:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
@ -3910,6 +3973,25 @@ functional-red-black-tree@^1.0.1:
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
gaxios@^4.0.0:
version "4.3.2"
resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-4.3.2.tgz#845827c2dc25a0213c8ab4155c7a28910f5be83f"
integrity sha512-T+ap6GM6UZ0c4E6yb1y/hy2UB6hTrqhglp3XfmU9qbLCGRYhLVV5aRPpC4EmoG8N8zOnkYCgoBz+ScvGAARY6Q==
dependencies:
abort-controller "^3.0.0"
extend "^3.0.2"
https-proxy-agent "^5.0.0"
is-stream "^2.0.0"
node-fetch "^2.6.1"
gcp-metadata@^4.2.0:
version "4.3.1"
resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-4.3.1.tgz#fb205fe6a90fef2fd9c85e6ba06e5559ee1eefa9"
integrity sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==
dependencies:
gaxios "^4.0.0"
json-bigint "^1.0.0"
generic-names@^4.0.0: generic-names@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-4.0.0.tgz#0bd8a2fd23fe8ea16cbd0a279acd69c06933d9a3" resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-4.0.0.tgz#0bd8a2fd23fe8ea16cbd0a279acd69c06933d9a3"
@ -4041,11 +4123,78 @@ globby@^11.0.4:
merge2 "^1.3.0" merge2 "^1.3.0"
slash "^3.0.0" slash "^3.0.0"
google-auth-library@^6.1.3:
version "6.1.6"
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-6.1.6.tgz#deacdcdb883d9ed6bac78bb5d79a078877fdf572"
integrity sha512-Q+ZjUEvLQj/lrVHF/IQwRo6p3s8Nc44Zk/DALsN+ac3T4HY/g/3rrufkgtl+nZ1TW7DNAw5cTChdVp4apUXVgQ==
dependencies:
arrify "^2.0.0"
base64-js "^1.3.0"
ecdsa-sig-formatter "^1.0.11"
fast-text-encoding "^1.0.0"
gaxios "^4.0.0"
gcp-metadata "^4.2.0"
gtoken "^5.0.4"
jws "^4.0.0"
lru-cache "^6.0.0"
google-auth-library@^7.0.2, google-auth-library@^7.11.0:
version "7.11.0"
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-7.11.0.tgz#b63699c65037310a424128a854ba7e736704cbdb"
integrity sha512-3S5jn2quRumvh9F/Ubf7GFrIq71HZ5a6vqosgdIu105kkk0WtSqc2jGCRqtWWOLRS8SX3AHACMOEDxhyWAQIcg==
dependencies:
arrify "^2.0.0"
base64-js "^1.3.0"
ecdsa-sig-formatter "^1.0.11"
fast-text-encoding "^1.0.0"
gaxios "^4.0.0"
gcp-metadata "^4.2.0"
gtoken "^5.0.4"
jws "^4.0.0"
lru-cache "^6.0.0"
google-p12-pem@^3.0.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-3.1.3.tgz#5497998798ee86c2fc1f4bb1f92b7729baf37537"
integrity sha512-MC0jISvzymxePDVembypNefkAQp+DRP7dBE+zNUPaIjEspIlYg0++OrsNr248V9tPbz6iqtZ7rX1hxWA5B8qBQ==
dependencies:
node-forge "^1.0.0"
google-spreadsheet@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/google-spreadsheet/-/google-spreadsheet-3.2.0.tgz#ce8aa75c15705aa950ad52b091a6fc4d33dcb329"
integrity sha512-z7XMaqb+26rdo8p51r5O03u8aPLAPzn5YhOXYJPcf2hdMVr0dUbIARgdkRdmGiBeoV/QoU/7VNhq1MMCLZv3kQ==
dependencies:
axios "^0.21.4"
google-auth-library "^6.1.3"
lodash "^4.17.21"
googleapis-common@^5.0.1:
version "5.0.5"
resolved "https://registry.yarnpkg.com/googleapis-common/-/googleapis-common-5.0.5.tgz#4c7160be1ed7e4cc8cdbcdb6eac8a4b3a61dd782"
integrity sha512-o2dgoW4x4fLIAN+IVAOccz3mEH8Lj1LP9c9BSSvkNJEn+U7UZh0WSr4fdH08x5VH7+sstIpd1lOYFZD0g7j4pw==
dependencies:
extend "^3.0.2"
gaxios "^4.0.0"
google-auth-library "^7.0.2"
qs "^6.7.0"
url-template "^2.0.8"
uuid "^8.0.0"
graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4:
version "4.2.9" version "4.2.9"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96"
integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==
gtoken@^5.0.4:
version "5.3.1"
resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.3.1.tgz#c1c2598a826f2b5df7c6bb53d7be6cf6d50c3c78"
integrity sha512-yqOREjzLHcbzz1UrQoxhBtpk8KjrVhuqPE7od1K2uhyxG2BHjKZetlbLw/SPZak/QqTIQW+addS+EcjqQsZbwQ==
dependencies:
gaxios "^4.0.0"
google-p12-pem "^3.0.3"
jws "^4.0.0"
has-bigints@^1.0.1: has-bigints@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
@ -4181,6 +4330,14 @@ https-proxy-agent@^4.0.0:
agent-base "5" agent-base "5"
debug "4" debug "4"
https-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
dependencies:
agent-base "6"
debug "4"
human-signals@^1.1.1: human-signals@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
@ -4628,6 +4785,13 @@ jsesc@^2.5.1:
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
json-bigint@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1"
integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==
dependencies:
bignumber.js "^9.0.0"
json-parse-better-errors@^1.0.1: json-parse-better-errors@^1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
@ -4697,6 +4861,23 @@ jsprim@^2.0.2:
array-includes "^3.1.3" array-includes "^3.1.3"
object.assign "^4.1.2" object.assign "^4.1.2"
jwa@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc"
integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==
dependencies:
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jws@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4"
integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==
dependencies:
jwa "^2.0.0"
safe-buffer "^5.0.1"
kbar@^0.1.0-beta.24: kbar@^0.1.0-beta.24:
version "0.1.0-beta.24" version "0.1.0-beta.24"
resolved "https://registry.yarnpkg.com/kbar/-/kbar-0.1.0-beta.24.tgz#5404c9817a0b7419b60b8378e45cffd7197970ee" resolved "https://registry.yarnpkg.com/kbar/-/kbar-0.1.0-beta.24.tgz#5404c9817a0b7419b60b8378e45cffd7197970ee"
@ -5170,6 +5351,18 @@ node-fetch@2.6.1:
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
node-fetch@^2.6.1:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"
node-forge@^1.0.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.2.1.tgz#82794919071ef2eb5c509293325cec8afd0fd53c"
integrity sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==
node-html-parser@1.4.9: node-html-parser@1.4.9:
version "1.4.9" version "1.4.9"
resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-1.4.9.tgz#3c8f6cac46479fae5800725edb532e9ae8fd816c" resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-1.4.9.tgz#3c8f6cac46479fae5800725edb532e9ae8fd816c"
@ -6087,6 +6280,13 @@ qs@^6.10.2, qs@^6.6.0:
dependencies: dependencies:
side-channel "^1.0.4" side-channel "^1.0.4"
qs@^6.10.3, qs@^6.7.0:
version "6.10.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e"
integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==
dependencies:
side-channel "^1.0.4"
qs@~6.5.2: qs@~6.5.2:
version "6.5.2" version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
@ -7313,6 +7513,11 @@ tr46@^1.0.1:
dependencies: dependencies:
punycode "^2.1.0" punycode "^2.1.0"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
ts-easing@^0.2.0: ts-easing@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec" resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec"
@ -7375,83 +7580,83 @@ tunnel-agent@^0.6.0:
dependencies: dependencies:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
turbo-darwin-64@1.0.24: turbo-darwin-64@1.0.28:
version "1.0.24" version "1.0.28"
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.0.24.tgz#f135baff0e44f9160c9b027e8c4dd2d5c8bb10a7" resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.0.28.tgz#662392368698c4e698b31871f0953836872f7b0e"
integrity sha512-A65Wxp+jBMfI3QX2uObX6DKvk+TxNXTf7ufQTHvRSLeAreB8QiVzJdYE0nC6YdrRwfPgFY3L72dhYd2v8ouXDg== integrity sha512-uvARrncW6HNTFi7PFe4sq4JqSOKs1vPgWjJjOEyVhsCFwBgYkXxYsJSdDfO8OhvJa3wv+eYFAK5RaUCk80Z8eg==
turbo-darwin-arm64@1.0.24: turbo-darwin-arm64@1.0.28:
version "1.0.24" version "1.0.28"
resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-1.0.24.tgz#c360d7cc6a7403855733e3aebb841b1227fbbb2e" resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-1.0.28.tgz#3ba1a6f9a960321391b8cf809ed8fab0276a499d"
integrity sha512-31zfexqUhvk/CIfAUk2mwjlpEjIURXu4QG8hoWlGxpcpAhlnkIX6CXle+LoQSnU3+4EbNe2SE92fYXsT/SnHAg== integrity sha512-d/ANU+RIq4Fx/MphkqFThvwOpb+NYDuR+07aV5w8cwI7ljw7hPAe3EW3CSlkPJhvjs6P/oh+F86jhh1Q581mVA==
turbo-freebsd-64@1.0.24: turbo-freebsd-64@1.0.28:
version "1.0.24" version "1.0.28"
resolved "https://registry.yarnpkg.com/turbo-freebsd-64/-/turbo-freebsd-64-1.0.24.tgz#9ef8914e7d1aaa995a8001a0ad81f7cc4520d332" resolved "https://registry.yarnpkg.com/turbo-freebsd-64/-/turbo-freebsd-64-1.0.28.tgz#f94e39dc455573c0a42f96f1a84649b252bf0571"
integrity sha512-vZYbDkOHH5eeQrxsAYldrh2nDY884irtmgJdGbpjryJgnJx+xzriZfoFalm/d1ZfG3ArENRJqGU+k6BriefZzw== integrity sha512-JMJWftuWhJan+Momc39vbbwaLYEcMpYyBxIrumyIrIkQVaiSKs/6oEFzh1YA+KE16kAgzTPJPXFDkmsY3idAQg==
turbo-freebsd-arm64@1.0.24: turbo-freebsd-arm64@1.0.28:
version "1.0.24" version "1.0.28"
resolved "https://registry.yarnpkg.com/turbo-freebsd-arm64/-/turbo-freebsd-arm64-1.0.24.tgz#12644e8f1b077f9d7afb367f2b8c2a2e0592ca72" resolved "https://registry.yarnpkg.com/turbo-freebsd-arm64/-/turbo-freebsd-arm64-1.0.28.tgz#5551f5c67a82a16ce1ba3193410764bae0849d1a"
integrity sha512-TDIu1PlyusY8AB69KGM4wGrCjtfbzmVF4Hlgf9mVeSWVKzqkRASorOEq1k8KvfZ+sBTS2GBMpqwpa1KVkYpVhw== integrity sha512-fGJNE8qJUhosaIK5sGBheeve9y074FLWv8KfYuXMyV/6+dxpNV60HoAFvw8tL3q8TNp47pU6x8e8h+u1/rn1wQ==
turbo-linux-32@1.0.24: turbo-linux-32@1.0.28:
version "1.0.24" version "1.0.28"
resolved "https://registry.yarnpkg.com/turbo-linux-32/-/turbo-linux-32-1.0.24.tgz#6129f7560f5c48214c1724ae7e8196dedc56de21" resolved "https://registry.yarnpkg.com/turbo-linux-32/-/turbo-linux-32-1.0.28.tgz#742340e6ca7d77c4fb884159bbc8c7faa8d61026"
integrity sha512-lhhK7914sUtuWYcDO8LV7NQkvTIwpAZlYH0XEOC/OTiYRQJvtKbEySLvefvtwuGjx7cGNI6OYraUsY3WWoK3FA== integrity sha512-fE0qIExxYuVFo5WlVWY0DJ1YZ/w+EC9RheT9nc1tU2EK83XPE1CZFW4lFIsWsXnIy9337zUeNDFVoVxOxCBSUQ==
turbo-linux-64@1.0.24: turbo-linux-64@1.0.28:
version "1.0.24" version "1.0.28"
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.0.24.tgz#221e3e14037e8fc3108e12a62de209d8a47f0348" resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.0.28.tgz#167acbd2899c0da9252f755ae74d619aaf99efe6"
integrity sha512-EbfdrkwVsHDG7AIVQ1enWHoD6riAApx4VRAuFcQHTvJU9e+BuOQBMjb7e9jO4mUrpumtN3n20tP+86odRwsk5g== integrity sha512-e+f/O1MlcKCMhJf10q1x+1KSImHwuFUW2+A6DbLk+ekBUW5RELC2qF7hGypCzcpm8xIqtj5A0kP3blFy60AMxw==
turbo-linux-arm64@1.0.24: turbo-linux-arm64@1.0.28:
version "1.0.24" version "1.0.28"
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.0.24.tgz#95891e7d4375ccbf2478677568557948be33717a" resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.0.28.tgz#785415e2a04125a69c7b1d45073150dbab3985f6"
integrity sha512-H4rqlgP2L7G3iAB/un/7DclExzLUkQ1NoZ0p/1Oa7Wb8H1YUlc8GkwUmpIFd5AOFSPL75DjYvlS8T5Tm23i+1A== integrity sha512-zN0nQClxp4nP4edinbdTd/9CpPjgNPsULc8LgunuJD+B9A0NRcRP5NCDo8/6ctTWs456sE3UhUF3t2b+uEgDzw==
turbo-linux-arm@1.0.24: turbo-linux-arm@1.0.28:
version "1.0.24" version "1.0.28"
resolved "https://registry.yarnpkg.com/turbo-linux-arm/-/turbo-linux-arm-1.0.24.tgz#f5acb74170a8b5a787915e799e7b52840c7c6982" resolved "https://registry.yarnpkg.com/turbo-linux-arm/-/turbo-linux-arm-1.0.28.tgz#f6de485aa732ba14cc1c851ba30b0f0a901507f3"
integrity sha512-lCNDVEkwxcn0acyPFVJgV5N5vKAP4LfXb+8uW/JpGHVoPHSONKtzYQG05J1KbHXpIjUT+DNgFtshtsdZYOewZQ== integrity sha512-PbB/RzN4W9M6sNZTvcjmc3PZ2S4CeFyQv/53tSs82pIlwM7NKVJzxVC0j3xCtoqoDDgXoJBhCpPV7MUEjCARQg==
turbo-linux-mips64le@1.0.24: turbo-linux-mips64le@1.0.28:
version "1.0.24" version "1.0.28"
resolved "https://registry.yarnpkg.com/turbo-linux-mips64le/-/turbo-linux-mips64le-1.0.24.tgz#f2cc99570222ac42fdcc0d0638f13bc0176859f9" resolved "https://registry.yarnpkg.com/turbo-linux-mips64le/-/turbo-linux-mips64le-1.0.28.tgz#4f993f387d90fa99037ad9592e98648fdb9ca608"
integrity sha512-AmrgQUDIe9AdNyh5YrI6pfMTUHD/gYfbylNmedLuN5Al3xINdZObcISzd/7VWd+V8wNW/1b9lUnt70Rv/KExfA== integrity sha512-7LKmFS9M+AKW5slTHLz00Y4ovZh2CpjgMUkNNC6qtJB8YyWwXwoU0U7Yz28q3+rNVkcEiqWWx4l1Tj1AotTlaA==
turbo-linux-ppc64le@1.0.24: turbo-linux-ppc64le@1.0.28:
version "1.0.24" version "1.0.28"
resolved "https://registry.yarnpkg.com/turbo-linux-ppc64le/-/turbo-linux-ppc64le-1.0.24.tgz#4d9508290d24cfdbaca24e57d8bcd0127281e2ed" resolved "https://registry.yarnpkg.com/turbo-linux-ppc64le/-/turbo-linux-ppc64le-1.0.28.tgz#053163e9042790102ceb1d299a3b79b1aa197be4"
integrity sha512-+6ESjsfrvRUr1AsurNcRTrqYr+XHG8g763+hXLog1MP9mn1cufZqWlAyE4G8/MLXDHsEKgK+tXqPLIyLBRjLEw== integrity sha512-R382Op75XxcIiY1pWPnVnefxOeVbrVAeABIHLL1hKetbu9UPNzKAnQKqJYGzKIdTRKtPh5CQuErEFzs/Ky2ZgA==
turbo-windows-32@1.0.24: turbo-windows-32@1.0.28:
version "1.0.24" version "1.0.28"
resolved "https://registry.yarnpkg.com/turbo-windows-32/-/turbo-windows-32-1.0.24.tgz#2bf906c0cc9d675afc4693221fc339ade29e6c13" resolved "https://registry.yarnpkg.com/turbo-windows-32/-/turbo-windows-32-1.0.28.tgz#b2b735fa56182aaf4095c9e4555fcf39ba050561"
integrity sha512-pqRys+FfHxuLVmW/AariITL5qpItp4WPAsYnWLx4u7VpCOO/qmTAI/SL7/jnTm4gxjBv3uf//lisu0AvEZd+TA== integrity sha512-SjDgimlD5TMvkrFRtsJb4uVP7T44gwr0HqiIpAuWj1m5d8Pz/OisOoUkM/ISPKqVycIU5JF8wx0+CTnxC7YNhQ==
turbo-windows-64@1.0.24: turbo-windows-64@1.0.28:
version "1.0.24" version "1.0.28"
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.0.24.tgz#5dd30b10110f2bb69caa479ddd72b4c471fb0dea" resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.0.28.tgz#7a2ad6f8416f04f20a1445c8b7123b80c6b16583"
integrity sha512-YHAWha5XkW0Ate1HtwhzFD32kZFXtC8KB4ReEvHc9GM2inQob1ZinvktS0xi5MC5Sxl9+bObOWmsxeZPOgNCFA== integrity sha512-nT7bgcdl/9QNGBiwCYwTQ2VszcsqJ4NqT4YkE954KFZYxgSwMjjVTdoXcsnXMHpWiMiYfFF7HZLecUNnDm1uUA==
turbo@^1.0.24: turbo@^1.0.28:
version "1.0.24" version "1.0.28"
resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.0.24.tgz#5efdeb44aab2f5e97b24a3e0ed4a159bfcd0a877" resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.0.28.tgz#fb33cecd2d025e3140ddbcc8c274f0ed7328f09d"
integrity sha512-bfOr7iW48+chDl+yKiZ5FIWzXOF6xOIyrAGPaWI+I5CdD27IZCEGvqvTV/weaHvjLbV7otybHQ56XCybBlVjoA== integrity sha512-5xmyVabNYqA0sCAU4VLdUS2A6GwIyy8FTszB/Fx4eNHwHudQwo00F2qORcDFwBHE4MqtnRoBFhL3ZJzo8c9A2w==
optionalDependencies: optionalDependencies:
turbo-darwin-64 "1.0.24" turbo-darwin-64 "1.0.28"
turbo-darwin-arm64 "1.0.24" turbo-darwin-arm64 "1.0.28"
turbo-freebsd-64 "1.0.24" turbo-freebsd-64 "1.0.28"
turbo-freebsd-arm64 "1.0.24" turbo-freebsd-arm64 "1.0.28"
turbo-linux-32 "1.0.24" turbo-linux-32 "1.0.28"
turbo-linux-64 "1.0.24" turbo-linux-64 "1.0.28"
turbo-linux-arm "1.0.24" turbo-linux-arm "1.0.28"
turbo-linux-arm64 "1.0.24" turbo-linux-arm64 "1.0.28"
turbo-linux-mips64le "1.0.24" turbo-linux-mips64le "1.0.28"
turbo-linux-ppc64le "1.0.24" turbo-linux-ppc64le "1.0.28"
turbo-windows-32 "1.0.24" turbo-windows-32 "1.0.28"
turbo-windows-64 "1.0.24" turbo-windows-64 "1.0.28"
tweetnacl@^0.14.3, tweetnacl@~0.14.0: tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5" version "0.14.5"
@ -7522,6 +7727,11 @@ uri-js@^4.2.2:
dependencies: dependencies:
punycode "^2.1.0" punycode "^2.1.0"
url-template@^2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21"
integrity sha1-/FZaPMy/93MMd19WQflVV5FDnyE=
url@0.10.3: url@0.10.3:
version "0.10.3" version "0.10.3"
resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64"
@ -7597,7 +7807,7 @@ uuid@3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
uuid@^8.3.2: uuid@^8.0.0, uuid@^8.3.2:
version "8.3.2" version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
@ -7644,11 +7854,24 @@ watchpack@2.3.0:
glob-to-regexp "^0.4.1" glob-to-regexp "^0.4.1"
graceful-fs "^4.1.2" graceful-fs "^4.1.2"
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
webidl-conversions@^4.0.2: webidl-conversions@^4.0.2:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
whatwg-url@^7.0.0: whatwg-url@^7.0.0:
version "7.1.0" version "7.1.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"