feat(integration): ✨ Add Google Sheets integration
This commit is contained in:
17
.env.example
17
.env.example
@ -10,12 +10,19 @@ EMAIL_SERVER_HOST=smtp.example.com
|
||||
EMAIL_SERVER_PORT=587
|
||||
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
|
||||
GITHUB_CLIENT_ID=
|
||||
GITHUB_CLIENT_SECRET=
|
||||
|
||||
# (Optional) Used to login using Google
|
||||
# (Optional) Used to login using Google AND Google Sheets integration
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
@ -27,9 +34,3 @@ FACEBOOK_CLIENT_SECRET=
|
||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY=
|
||||
STRIPE_SECRET_KEY=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
|
||||
# Used for uploading images, videos, etc...
|
||||
S3_UPLOAD_KEY=
|
||||
S3_UPLOAD_SECRET=
|
||||
S3_UPLOAD_REGION=
|
||||
S3_UPLOAD_BUCKET=
|
||||
|
@ -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>
|
||||
</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>
|
||||
)
|
||||
|
@ -67,3 +67,180 @@ export const FacebookLogo = (props: IconProps) => (
|
||||
/>
|
||||
</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>
|
||||
)
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 React, { useEffect, useState } from 'react'
|
||||
import { StepIcon } from './StepIcon'
|
||||
@ -9,11 +9,8 @@ export const StepCard = ({
|
||||
type,
|
||||
onMouseDown,
|
||||
}: {
|
||||
type: BubbleStepType | InputStepType | LogicStepType
|
||||
onMouseDown: (
|
||||
e: React.MouseEvent,
|
||||
type: BubbleStepType | InputStepType | LogicStepType
|
||||
) => void
|
||||
type: DraggableStepType
|
||||
onMouseDown: (e: React.MouseEvent, type: DraggableStepType) => void
|
||||
}) => {
|
||||
const { draggedStepType } = useDnd()
|
||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||
|
@ -12,48 +12,45 @@ import {
|
||||
PhoneIcon,
|
||||
TextIcon,
|
||||
} 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'
|
||||
|
||||
type StepIconProps = { type: StepType } & IconProps
|
||||
|
||||
export const StepIcon = ({ type, ...props }: StepIconProps) => {
|
||||
switch (type) {
|
||||
case BubbleStepType.TEXT: {
|
||||
case BubbleStepType.TEXT:
|
||||
return <ChatIcon {...props} />
|
||||
}
|
||||
case InputStepType.TEXT: {
|
||||
case InputStepType.TEXT:
|
||||
return <TextIcon {...props} />
|
||||
}
|
||||
case InputStepType.NUMBER: {
|
||||
case InputStepType.NUMBER:
|
||||
return <NumberIcon {...props} />
|
||||
}
|
||||
case InputStepType.EMAIL: {
|
||||
case InputStepType.EMAIL:
|
||||
return <EmailIcon {...props} />
|
||||
}
|
||||
case InputStepType.URL: {
|
||||
case InputStepType.URL:
|
||||
return <GlobeIcon {...props} />
|
||||
}
|
||||
case InputStepType.DATE: {
|
||||
case InputStepType.DATE:
|
||||
return <CalendarIcon {...props} />
|
||||
}
|
||||
case InputStepType.PHONE: {
|
||||
case InputStepType.PHONE:
|
||||
return <PhoneIcon {...props} />
|
||||
}
|
||||
case InputStepType.CHOICE: {
|
||||
case InputStepType.CHOICE:
|
||||
return <CheckSquareIcon {...props} />
|
||||
}
|
||||
case LogicStepType.SET_VARIABLE: {
|
||||
case LogicStepType.SET_VARIABLE:
|
||||
return <EditIcon {...props} />
|
||||
}
|
||||
case LogicStepType.CONDITION: {
|
||||
case LogicStepType.CONDITION:
|
||||
return <FilterIcon {...props} />
|
||||
}
|
||||
case 'start': {
|
||||
case IntegrationStepType.GOOGLE_SHEETS:
|
||||
return <GoogleSheetsLogo {...props} />
|
||||
case 'start':
|
||||
return <FlagIcon {...props} />
|
||||
}
|
||||
default: {
|
||||
default:
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,11 @@
|
||||
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'
|
||||
|
||||
type Props = { type: StepType }
|
||||
@ -34,6 +40,9 @@ export const StepTypeLabel = ({ type }: Props) => {
|
||||
case LogicStepType.CONDITION: {
|
||||
return <Text>Condition</Text>
|
||||
}
|
||||
case IntegrationStepType.GOOGLE_SHEETS: {
|
||||
return <Text>Sheets</Text>
|
||||
}
|
||||
default: {
|
||||
return <></>
|
||||
}
|
||||
|
@ -5,7 +5,13 @@ import {
|
||||
SimpleGrid,
|
||||
useEventListener,
|
||||
} from '@chakra-ui/react'
|
||||
import { BubbleStepType, InputStepType, LogicStepType } from 'models'
|
||||
import {
|
||||
BubbleStepType,
|
||||
DraggableStepType,
|
||||
InputStepType,
|
||||
IntegrationStepType,
|
||||
LogicStepType,
|
||||
} from 'models'
|
||||
import { useDnd } from 'contexts/DndContext'
|
||||
import React, { useState } from 'react'
|
||||
import { StepCard, StepCardOverlay } from './StepCard'
|
||||
@ -29,10 +35,7 @@ export const StepTypesList = () => {
|
||||
}
|
||||
useEventListener('mousemove', handleMouseMove)
|
||||
|
||||
const handleMouseDown = (
|
||||
e: React.MouseEvent,
|
||||
type: BubbleStepType | InputStepType | LogicStepType
|
||||
) => {
|
||||
const handleMouseDown = (e: React.MouseEvent, type: DraggableStepType) => {
|
||||
const element = e.currentTarget as HTMLDivElement
|
||||
const rect = element.getBoundingClientRect()
|
||||
const relativeX = e.clientX - rect.left
|
||||
@ -94,6 +97,15 @@ export const StepTypesList = () => {
|
||||
<StepCard key={type} type={type} onMouseDown={handleMouseDown} />
|
||||
))}
|
||||
</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 && (
|
||||
<StepCardOverlay
|
||||
type={draggedStepType}
|
||||
|
@ -3,17 +3,16 @@ import {
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
useEventListener,
|
||||
Portal,
|
||||
} from '@chakra-ui/react'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import {
|
||||
ChoiceInputOptions,
|
||||
ConditionOptions,
|
||||
InputStep,
|
||||
InputStepType,
|
||||
IntegrationStepType,
|
||||
LogicStepType,
|
||||
SetVariableOptions,
|
||||
Step,
|
||||
TextInputOptions,
|
||||
StepOptions,
|
||||
} from 'models'
|
||||
import { useRef } from 'react'
|
||||
import {
|
||||
@ -25,6 +24,7 @@ import {
|
||||
} from './bodies'
|
||||
import { ChoiceInputSettingsBody } from './bodies/ChoiceInputSettingsBody'
|
||||
import { ConditionSettingsBody } from './bodies/ConditionSettingsBody'
|
||||
import { GoogleSheetsSettingsBody } from './bodies/GoogleSheetsSettingsBody'
|
||||
import { PhoneNumberSettingsBody } from './bodies/PhoneNumberSettingsBody'
|
||||
import { SetVariableSettingsBody } from './bodies/SetVariableSettingsBody'
|
||||
|
||||
@ -41,24 +41,21 @@ export const SettingsPopoverContent = ({ step }: Props) => {
|
||||
}
|
||||
useEventListener('wheel', handleMouseWheel, ref.current)
|
||||
return (
|
||||
<Portal>
|
||||
<PopoverContent onMouseDown={handleMouseDown}>
|
||||
<PopoverArrow />
|
||||
<PopoverBody p="6" overflowY="scroll" maxH="400px" ref={ref}>
|
||||
<SettingsPopoverBodyContent step={step} />
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
|
||||
const SettingsPopoverBodyContent = ({ step }: Props) => {
|
||||
const { updateStep } = useTypebot()
|
||||
const handleOptionsChange = (
|
||||
options:
|
||||
| TextInputOptions
|
||||
| ChoiceInputOptions
|
||||
| SetVariableOptions
|
||||
| ConditionOptions
|
||||
) => updateStep(step.id, { options } as Partial<InputStep>)
|
||||
const handleOptionsChange = (options: StepOptions) =>
|
||||
updateStep(step.id, { options } as Partial<InputStep>)
|
||||
|
||||
switch (step.type) {
|
||||
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: {
|
||||
return <></>
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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 <></>
|
||||
}
|
||||
}
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { GoogleSheetsSettingsBody } from './GoogleSheetsSettingsBody'
|
@ -7,21 +7,27 @@ import {
|
||||
useEventListener,
|
||||
} from '@chakra-ui/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 { 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 { TextEditor } from './TextEditor/TextEditor'
|
||||
import { StepNodeContent } from './StepNodeContent'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { ContextMenu } from 'components/shared/ContextMenu'
|
||||
import { SettingsPopoverContent } from './SettingsPopoverContent'
|
||||
import { DraggableStep } from 'contexts/DndContext'
|
||||
import { StepNodeContextMenu } from './StepNodeContextMenu'
|
||||
import { SourceEndpoint } from './SourceEndpoint'
|
||||
import { hasDefaultConnector } from 'services/typebots'
|
||||
import { TargetEndpoint } from './TargetEndpoint'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export const StepNode = ({
|
||||
step,
|
||||
@ -39,6 +45,7 @@ export const StepNode = ({
|
||||
step: DraggableStep
|
||||
) => void
|
||||
}) => {
|
||||
const { query } = useRouter()
|
||||
const { setConnectingIds, connectingIds } = useGraph()
|
||||
const { moveStep, typebot } = useTypebot()
|
||||
const [isConnecting, setIsConnecting] = useState(false)
|
||||
@ -152,7 +159,11 @@ export const StepNode = ({
|
||||
renderMenu={() => <StepNodeContextMenu stepId={step.id} />}
|
||||
>
|
||||
{(ref, isOpened) => (
|
||||
<Popover placement="left" isLazy>
|
||||
<Popover
|
||||
placement="left"
|
||||
isLazy
|
||||
defaultIsOpen={query.stepId?.toString() === step.id}
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<Flex
|
||||
pos="relative"
|
||||
@ -226,11 +237,12 @@ export const StepNode = ({
|
||||
)}
|
||||
</Flex>
|
||||
</PopoverTrigger>
|
||||
{(isInputStep(step) || isLogicStep(step)) && (
|
||||
<SettingsPopoverContent step={step} />
|
||||
)}
|
||||
{hasPopover(step) && <SettingsPopoverContent step={step} />}
|
||||
</Popover>
|
||||
)}
|
||||
</ContextMenu>
|
||||
)
|
||||
}
|
||||
|
||||
const hasPopover = (step: Step) =>
|
||||
isInputStep(step) || isLogicStep(step) || isIntegrationStep(step)
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
LogicStepType,
|
||||
SetVariableStep,
|
||||
ConditionStep,
|
||||
IntegrationStepType,
|
||||
} from 'models'
|
||||
import { ChoiceItemsList } from './ChoiceInputStepNode/ChoiceItemsList'
|
||||
import { SourceEndpoint } from './SourceEndpoint'
|
||||
@ -84,6 +85,10 @@ export const StepNodeContent = ({ step }: Props) => {
|
||||
case LogicStepType.CONDITION: {
|
||||
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': {
|
||||
return <Text>{step.label}</Text>
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react'
|
||||
import { Step, Table } from 'models'
|
||||
import { DraggableStep, useDnd } from 'contexts/DndContext'
|
||||
import { DraggableStep, Step, Table } from 'models'
|
||||
import { useDnd } from 'contexts/DndContext'
|
||||
import { Coordinates } from 'contexts/GraphContext'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { StepNode, StepNodeOverlay } from './StepNode'
|
||||
|
@ -2,10 +2,11 @@ import { Flex, FlexProps, useEventListener } from '@chakra-ui/react'
|
||||
import React, { useRef, useMemo } from 'react'
|
||||
import { blockWidth, useGraph } from 'contexts/GraphContext'
|
||||
import { BlockNode } from './BlockNode/BlockNode'
|
||||
import { DraggableStepType, useDnd } from 'contexts/DndContext'
|
||||
import { useDnd } from 'contexts/DndContext'
|
||||
import { Edges } from './Edges'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
|
||||
import { DraggableStepType } from 'models'
|
||||
|
||||
const Graph = ({ ...props }: FlexProps) => {
|
||||
const { draggedStepType, setDraggedStepType, draggedStep, setDraggedStep } =
|
||||
|
104
apps/builder/components/shared/CredentialsDropdown.tsx
Normal file
104
apps/builder/components/shared/CredentialsDropdown.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -1,5 +1,11 @@
|
||||
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'
|
||||
|
||||
type Props = Omit<InputProps, 'onChange' | 'value'> & {
|
||||
@ -8,12 +14,11 @@ type Props = Omit<InputProps, 'onChange' | 'value'> & {
|
||||
onChange: (debouncedValue: string) => void
|
||||
}
|
||||
|
||||
export const DebouncedInput = ({
|
||||
delay,
|
||||
onChange,
|
||||
initialValue,
|
||||
...props
|
||||
}: Props) => {
|
||||
export const DebouncedInput = forwardRef(
|
||||
(
|
||||
{ delay, onChange, initialValue, ...props }: Props,
|
||||
ref: ForwardedRef<HTMLInputElement>
|
||||
) => {
|
||||
const [currentValue, setCurrentValue] = useState(initialValue)
|
||||
const [currentValueDebounced] = useDebounce(currentValue, delay)
|
||||
|
||||
@ -27,5 +32,13 @@ export const DebouncedInput = ({
|
||||
setCurrentValue(e.target.value)
|
||||
}
|
||||
|
||||
return <Input {...props} value={currentValue} onChange={handleChange} />
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
ref={ref}
|
||||
value={currentValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -11,15 +11,17 @@ import { ChevronLeftIcon } from 'assets/icons'
|
||||
import React from 'react'
|
||||
|
||||
type Props<T> = {
|
||||
currentItem: T
|
||||
currentItem?: T
|
||||
onItemSelect: (item: T) => void
|
||||
items: T[]
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export const DropdownList = <T,>({
|
||||
currentItem,
|
||||
onItemSelect,
|
||||
items,
|
||||
placeholder = '',
|
||||
...props
|
||||
}: Props<T> & MenuButtonProps) => {
|
||||
const handleMenuItemClick = (operator: T) => () => {
|
||||
@ -27,7 +29,7 @@ export const DropdownList = <T,>({
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Menu isLazy placement="bottom-end">
|
||||
<Menu isLazy placement="bottom-end" matchWidth>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rightIcon={<ChevronLeftIcon transform={'rotate(-90deg)'} />}
|
||||
@ -37,9 +39,9 @@ export const DropdownList = <T,>({
|
||||
textAlign="left"
|
||||
{...props}
|
||||
>
|
||||
{currentItem}
|
||||
{currentItem ?? placeholder}
|
||||
</MenuButton>
|
||||
<MenuList maxW="500px">
|
||||
<MenuList maxW="500px" shadow="lg">
|
||||
<Stack maxH={'35vh'} overflowY="scroll" spacing="0">
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
|
105
apps/builder/components/shared/InputWithVariable.tsx
Normal file
105
apps/builder/components/shared/InputWithVariable.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -8,18 +8,21 @@ import {
|
||||
PopoverContent,
|
||||
Button,
|
||||
Text,
|
||||
InputProps,
|
||||
} from '@chakra-ui/react'
|
||||
import { useState, useRef, useEffect, ChangeEvent } from 'react'
|
||||
|
||||
type Props = {
|
||||
selectedItem?: string
|
||||
items: string[]
|
||||
onSelectItem: (value: string) => void
|
||||
} & InputProps
|
||||
export const SearchableDropdown = ({
|
||||
selectedItem,
|
||||
items,
|
||||
onSelectItem,
|
||||
}: {
|
||||
selectedItem?: string
|
||||
items: string[]
|
||||
onSelectItem: (value: string) => void
|
||||
}) => {
|
||||
...inputProps
|
||||
}: Props) => {
|
||||
const { onOpen, onClose, isOpen } = useDisclosure()
|
||||
const [inputValue, setInputValue] = useState(selectedItem)
|
||||
const [filteredItems, setFilteredItems] = useState([
|
||||
@ -64,19 +67,38 @@ export const SearchableDropdown = ({
|
||||
])
|
||||
}
|
||||
|
||||
const handleItemClick = (item: string) => () => {
|
||||
setInputValue(item)
|
||||
onSelectItem(item)
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex ref={dropdownRef}>
|
||||
<Popover isOpen={isOpen} initialFocusRef={inputRef}>
|
||||
<Flex ref={dropdownRef} w="full">
|
||||
<Popover
|
||||
isOpen={isOpen}
|
||||
initialFocusRef={inputRef}
|
||||
matchWidth
|
||||
offset={[0, 0]}
|
||||
isLazy
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={onInputChange}
|
||||
onClick={onOpen}
|
||||
w="300px"
|
||||
{...inputProps}
|
||||
/>
|
||||
</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.map((item, idx) => {
|
||||
@ -84,15 +106,12 @@ export const SearchableDropdown = ({
|
||||
<Button
|
||||
minH="40px"
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
setInputValue(item)
|
||||
onSelectItem(item)
|
||||
onClose()
|
||||
}}
|
||||
onClick={handleItemClick(item)}
|
||||
fontSize="16px"
|
||||
fontWeight="normal"
|
||||
rounded="none"
|
||||
colorScheme="gray"
|
||||
role="menuitem"
|
||||
variant="ghost"
|
||||
justifyContent="flex-start"
|
||||
>
|
||||
|
@ -1,12 +1,4 @@
|
||||
import {
|
||||
BubbleStep,
|
||||
BubbleStepType,
|
||||
ChoiceItem,
|
||||
InputStep,
|
||||
InputStepType,
|
||||
LogicStepType,
|
||||
LogicStep,
|
||||
} from 'models'
|
||||
import { ChoiceItem, DraggableStep, DraggableStepType } from 'models'
|
||||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
@ -16,9 +8,6 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
export type DraggableStep = BubbleStep | InputStep | LogicStep
|
||||
export type DraggableStepType = BubbleStepType | InputStepType | LogicStepType
|
||||
|
||||
const dndContext = createContext<{
|
||||
draggedStepType?: DraggableStepType
|
||||
setDraggedStepType: Dispatch<SetStateAction<DraggableStepType | undefined>>
|
||||
|
@ -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 { useRouter } from 'next/router'
|
||||
import {
|
||||
@ -43,7 +43,7 @@ const typebotContext = createContext<
|
||||
isPublishing: boolean
|
||||
hasUnsavedChanges: boolean
|
||||
isSavingLoading: boolean
|
||||
save: () => void
|
||||
save: () => Promise<ToastId | undefined>
|
||||
undo: () => void
|
||||
updateTypebot: (updates: UpdateTypebotPayload) => void
|
||||
publishTypebot: () => void
|
||||
|
@ -1,13 +1,6 @@
|
||||
import { Coordinates } from 'contexts/GraphContext'
|
||||
import { WritableDraft } from 'immer/dist/internal'
|
||||
import {
|
||||
Block,
|
||||
BubbleStepType,
|
||||
InputStepType,
|
||||
LogicStepType,
|
||||
Step,
|
||||
Typebot,
|
||||
} from 'models'
|
||||
import { Block, DraggableStep, DraggableStepType, Typebot } from 'models'
|
||||
import { parseNewBlock } from 'services/typebots'
|
||||
import { Updater } from 'use-immer'
|
||||
import { createStepDraft, deleteStepDraft } from './steps'
|
||||
@ -15,7 +8,7 @@ import { createStepDraft, deleteStepDraft } from './steps'
|
||||
export type BlocksActions = {
|
||||
createBlock: (
|
||||
props: Coordinates & {
|
||||
step: BubbleStepType | InputStepType | LogicStepType | Step
|
||||
step: DraggableStep | DraggableStepType
|
||||
}
|
||||
) => void
|
||||
updateBlock: (blockId: string, updates: Partial<Omit<Block, 'id'>>) => void
|
||||
@ -28,7 +21,7 @@ export const blocksActions = (setTypebot: Updater<Typebot>): BlocksActions => ({
|
||||
y,
|
||||
step,
|
||||
}: Coordinates & {
|
||||
step: BubbleStepType | InputStepType | LogicStepType | Step
|
||||
step: DraggableStep | DraggableStepType
|
||||
}) => {
|
||||
setTypebot((typebot) => {
|
||||
const newBlock = parseNewBlock({
|
||||
|
@ -1,10 +1,9 @@
|
||||
import {
|
||||
BubbleStepType,
|
||||
ChoiceInputStep,
|
||||
InputStepType,
|
||||
Step,
|
||||
Typebot,
|
||||
LogicStepType,
|
||||
DraggableStep,
|
||||
DraggableStepType,
|
||||
} from 'models'
|
||||
import { parseNewStep } from 'services/typebots'
|
||||
import { Updater } from 'use-immer'
|
||||
@ -16,7 +15,7 @@ import { isChoiceInput } from 'utils'
|
||||
export type StepsActions = {
|
||||
createStep: (
|
||||
blockId: string,
|
||||
step: BubbleStepType | InputStepType | LogicStepType | Step,
|
||||
step: DraggableStep | DraggableStepType,
|
||||
index?: number
|
||||
) => void
|
||||
updateStep: (
|
||||
@ -30,7 +29,7 @@ export type StepsActions = {
|
||||
export const stepsAction = (setTypebot: Updater<Typebot>): StepsActions => ({
|
||||
createStep: (
|
||||
blockId: string,
|
||||
step: BubbleStepType | InputStepType | LogicStepType | Step,
|
||||
step: DraggableStep | DraggableStepType,
|
||||
index?: number
|
||||
) => {
|
||||
setTypebot((typebot) => {
|
||||
@ -76,7 +75,7 @@ export const deleteStepDraft = (
|
||||
|
||||
export const createStepDraft = (
|
||||
typebot: WritableDraft<Typebot>,
|
||||
step: BubbleStepType | InputStepType | LogicStepType | Step,
|
||||
step: DraggableStep | DraggableStepType,
|
||||
blockId: string,
|
||||
index?: number
|
||||
) => {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { User } from 'db'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import {
|
||||
@ -13,6 +12,8 @@ import { isDefined } from 'utils'
|
||||
import { updateUser as updateUserInDb } from 'services/user'
|
||||
import { useToast } from '@chakra-ui/react'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { useCredentials } from 'services/credentials'
|
||||
import { Credentials, User } from 'db'
|
||||
|
||||
const userContext = createContext<{
|
||||
user?: User
|
||||
@ -20,6 +21,7 @@ const userContext = createContext<{
|
||||
isSaving: boolean
|
||||
hasUnsavedChanges: boolean
|
||||
isOAuthProvider: boolean
|
||||
credentials: Credentials[]
|
||||
updateUser: (newUser: Partial<User>) => void
|
||||
saveUser: () => void
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
@ -29,8 +31,12 @@ const userContext = createContext<{
|
||||
export const UserContext = ({ children }: { children: ReactNode }) => {
|
||||
const router = useRouter()
|
||||
const { data: session, status } = useSession()
|
||||
|
||||
const [user, setUser] = useState<User>()
|
||||
const [user, setUser] = useState<User | undefined>()
|
||||
const { credentials } = useCredentials({
|
||||
userId: user?.id,
|
||||
onError: (error) =>
|
||||
toast({ title: error.name, description: error.message }),
|
||||
})
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const isOAuthProvider = useMemo(
|
||||
() => (session?.providerType as boolean | undefined) ?? false,
|
||||
@ -69,9 +75,13 @@ export const UserContext = ({ children }: { children: ReactNode }) => {
|
||||
setIsSaving(true)
|
||||
const { error } = await updateUserInDb(user.id, user)
|
||||
if (error) toast({ title: error.name, description: error.message })
|
||||
await refreshUser()
|
||||
setIsSaving(false)
|
||||
}
|
||||
|
||||
const refreshUser = async () => {
|
||||
await fetch('/api/auth/session?update')
|
||||
reloadSession()
|
||||
setIsSaving(false)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -84,6 +94,7 @@ export const UserContext = ({ children }: { children: ReactNode }) => {
|
||||
isLoading: status === 'loading',
|
||||
hasUnsavedChanges,
|
||||
isOAuthProvider,
|
||||
credentials: credentials ?? [],
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { InputStepType, PublicTypebot, Typebot } from 'models'
|
||||
import { Plan, PrismaClient } from 'db'
|
||||
import { CredentialsType, Plan, PrismaClient } from 'db'
|
||||
import { parseTestTypebot } from './utils'
|
||||
import { userIds } from './data'
|
||||
|
||||
@ -7,9 +7,10 @@ const prisma = new PrismaClient()
|
||||
|
||||
const teardownTestData = async () => prisma.user.deleteMany()
|
||||
|
||||
export const seedDb = async () => {
|
||||
export const seedDb = async (googleRefreshToken: string) => {
|
||||
await teardownTestData()
|
||||
await createUsers()
|
||||
await createCredentials(googleRefreshToken)
|
||||
await createFolders()
|
||||
await createTypebots()
|
||||
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 = () =>
|
||||
prisma.dashboardFolder.createMany({
|
||||
data: [{ ownerId: userIds[1], name: 'Folder #1', id: 'folder1' }],
|
||||
|
@ -280,7 +280,9 @@ const createTypebotWithStep = (step: Omit<InputStep, 'id' | 'blockId'>) => {
|
||||
//@ts-ignore
|
||||
options:
|
||||
step.type === InputStepType.CHOICE
|
||||
? { itemIds: ['item1'] }
|
||||
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
{ itemIds: ['item1'] }
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
|
139
apps/builder/cypress/tests/integrations.ts
Normal file
139
apps/builder/cypress/tests/integrations.ts
Normal 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()
|
||||
}
|
28
apps/builder/libs/google-sheets.ts
Normal file
28
apps/builder/libs/google-sheets.ts
Normal 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 },
|
||||
})
|
@ -1,12 +1,5 @@
|
||||
import { Link } from '@chakra-ui/react'
|
||||
import {
|
||||
AutoformatRule,
|
||||
createAutoformatPlugin,
|
||||
} from '@udecode/plate-autoformat'
|
||||
import {
|
||||
MARK_BOLD,
|
||||
MARK_UNDERLINE,
|
||||
MARK_ITALIC,
|
||||
createBoldPlugin,
|
||||
createItalicPlugin,
|
||||
createUnderlinePlugin,
|
||||
@ -21,40 +14,12 @@ export const editorStyle: React.CSSProperties = {
|
||||
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(
|
||||
[
|
||||
createBoldPlugin(),
|
||||
createItalicPlugin(),
|
||||
createUnderlinePlugin(),
|
||||
createLinkPlugin(),
|
||||
createAutoformatPlugin({
|
||||
options: {
|
||||
rules: autoFormatRules,
|
||||
},
|
||||
}),
|
||||
],
|
||||
{ components: { [ELEMENT_LINK]: Link } }
|
||||
)
|
||||
|
@ -16,8 +16,8 @@
|
||||
"@dnd-kit/sortable": "^5.1.0",
|
||||
"@emotion/react": "^11.7.1",
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@googleapis/drive": "^2.1.0",
|
||||
"@next-auth/prisma-adapter": "next",
|
||||
"@udecode/plate-autoformat": "^9.0.0",
|
||||
"@udecode/plate-basic-marks": "^9.0.0",
|
||||
"@udecode/plate-common": "^7.0.2",
|
||||
"@udecode/plate-core": "^9.0.0",
|
||||
@ -30,6 +30,8 @@
|
||||
"fast-equals": "^2.0.4",
|
||||
"focus-visible": "^5.2.0",
|
||||
"framer-motion": "^4",
|
||||
"google-auth-library": "^7.11.0",
|
||||
"google-spreadsheet": "^3.2.0",
|
||||
"htmlparser2": "^7.2.0",
|
||||
"immer": "^9.0.7",
|
||||
"kbar": "^0.1.0-beta.24",
|
||||
@ -61,6 +63,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/cypress": "^8.0.2",
|
||||
"@types/google-spreadsheet": "^3.1.5",
|
||||
"@types/micro-cors": "^0.1.2",
|
||||
"@types/node": "^16.11.9",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
|
62
apps/builder/pages/api/credentials/google-sheets/callback.ts
Normal file
62
apps/builder/pages/api/credentials/google-sheets/callback.ts
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
25
apps/builder/pages/api/users/[id]/credentials.ts
Normal file
25
apps/builder/pages/api/users/[id]/credentials.ts
Normal 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
|
22
apps/builder/services/credentials.ts
Normal file
22
apps/builder/services/credentials.ts
Normal 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,
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
import { DashboardFolder } from '.prisma/client'
|
||||
import useSWR from 'swr'
|
||||
import { fetcher, sendRequest } from './utils'
|
||||
import { fetcher } from './utils'
|
||||
import { stringify } from 'qs'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const useFolders = ({
|
||||
parentId,
|
||||
|
66
apps/builder/services/integrations.ts
Normal file
66
apps/builder/services/integrations.ts
Normal 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,
|
||||
}
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
import { PublicTypebot, Typebot } from 'models'
|
||||
import { sendRequest } from './utils'
|
||||
import shortId from 'short-uuid'
|
||||
import { HStack, Text } from '@chakra-ui/react'
|
||||
import { CalendarIcon } from 'assets/icons'
|
||||
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
|
||||
import { isInputStep } from 'utils'
|
||||
import { isInputStep, sendRequest } from 'utils'
|
||||
|
||||
export const parseTypebotToPublicTypebot = (
|
||||
typebot: Typebot
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Result } from 'models'
|
||||
import useSWRInfinite from 'swr/infinite'
|
||||
import { fetcher, sendRequest } from './utils'
|
||||
import { fetcher } from './utils'
|
||||
import { stringify } from 'qs'
|
||||
import { Answer } from 'db'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
const getKey = (
|
||||
typebotId: string,
|
||||
|
@ -6,25 +6,24 @@ import {
|
||||
Settings,
|
||||
StartStep,
|
||||
Theme,
|
||||
BubbleStep,
|
||||
InputStep,
|
||||
BubbleStepType,
|
||||
InputStepType,
|
||||
ChoiceInputStep,
|
||||
LogicStepType,
|
||||
LogicStep,
|
||||
Step,
|
||||
ConditionStep,
|
||||
ComparisonOperators,
|
||||
LogicalOperator,
|
||||
DraggableStepType,
|
||||
DraggableStep,
|
||||
} from 'models'
|
||||
import shortId, { generate } from 'short-uuid'
|
||||
import { Typebot } from 'models'
|
||||
import useSWR from 'swr'
|
||||
import { fetcher, sendRequest, toKebabCase } from './utils'
|
||||
import { fetcher, toKebabCase } from './utils'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { stringify } from 'qs'
|
||||
import { isChoiceInput, isConditionStep } from 'utils'
|
||||
import { isChoiceInput, isConditionStep, sendRequest } from 'utils'
|
||||
|
||||
export const useTypebots = ({
|
||||
folderId,
|
||||
@ -114,9 +113,9 @@ export const parseNewBlock = ({
|
||||
}
|
||||
|
||||
export const parseNewStep = (
|
||||
type: BubbleStepType | InputStepType | LogicStepType,
|
||||
type: DraggableStepType,
|
||||
blockId: string
|
||||
): BubbleStep | InputStep | LogicStep => {
|
||||
): DraggableStep => {
|
||||
const id = `s${shortId.generate()}`
|
||||
switch (type) {
|
||||
case BubbleStepType.TEXT: {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { User } from 'db'
|
||||
import { sendRequest } from './utils'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const updateUser = async (id: string, user: User) =>
|
||||
sendRequest({
|
||||
|
@ -10,36 +10,6 @@ export const isMobile =
|
||||
typeof window !== 'undefined' &&
|
||||
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) => {
|
||||
e.preventDefault()
|
||||
e.returnValue = ''
|
||||
|
27
apps/viewer/libs/google-sheets.ts
Normal file
27
apps/viewer/libs/google-sheets.ts
Normal 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 },
|
||||
})
|
@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"bot-engine": "*",
|
||||
"db": "*",
|
||||
"google-spreadsheet": "^3.2.0",
|
||||
"models": "*",
|
||||
"next": "^12.0.7",
|
||||
"react": "^17.0.2",
|
||||
@ -18,6 +19,7 @@
|
||||
"utils": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/google-spreadsheet": "^3.1.5",
|
||||
"@types/node": "^17.0.4",
|
||||
"@types/react": "^17.0.38",
|
||||
"@typescript-eslint/eslint-plugin": "^5.9.0",
|
||||
|
@ -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
|
@ -16,7 +16,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv-cli": "^4.1.1",
|
||||
"turbo": "^1.0.24"
|
||||
"turbo": "^1.0.28"
|
||||
},
|
||||
"turbo": {
|
||||
"baseBranch": "origin/main",
|
||||
|
@ -9,6 +9,7 @@
|
||||
"db": "*",
|
||||
"fast-equals": "^2.0.4",
|
||||
"models": "*",
|
||||
"qs": "^6.10.3",
|
||||
"react-frame-component": "5.2.2-alpha.0",
|
||||
"react-phone-number-input": "^3.1.44",
|
||||
"react-scroll": "^1.8.4",
|
||||
|
@ -4,29 +4,18 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||
import { ChatStep } from './ChatStep'
|
||||
import { AvatarSideContainer } from './AvatarSideContainer'
|
||||
import { HostAvatarsContext } from '../../contexts/HostAvatarsContext'
|
||||
import {
|
||||
ChoiceInputStep,
|
||||
ComparisonOperators,
|
||||
ConditionStep,
|
||||
LogicalOperator,
|
||||
LogicStep,
|
||||
LogicStepType,
|
||||
Step,
|
||||
Target,
|
||||
} from 'models'
|
||||
import { Step, Target } from 'models'
|
||||
import { useTypebot } from '../../contexts/TypebotContext'
|
||||
import {
|
||||
isChoiceInput,
|
||||
isDefined,
|
||||
isInputStep,
|
||||
isIntegrationStep,
|
||||
isLogicStep,
|
||||
isTextBubbleStep,
|
||||
} from 'utils'
|
||||
import {
|
||||
evaluateExpression,
|
||||
isMathFormula,
|
||||
parseVariables,
|
||||
} from 'services/variable'
|
||||
import { executeLogic } from 'services/logic'
|
||||
import { getSingleChoiceTargetId } from 'services/inputs'
|
||||
import { executeIntegration } from 'services/integration'
|
||||
|
||||
type ChatBlockProps = {
|
||||
stepIds: string[]
|
||||
@ -50,12 +39,29 @@ export const ChatBlock = ({
|
||||
|
||||
useEffect(() => {
|
||||
autoScrollToBottom()
|
||||
onNewStepDisplayed()
|
||||
}, [displayedSteps])
|
||||
|
||||
const onNewStepDisplayed = async () => {
|
||||
const currentStep = [...displayedSteps].pop()
|
||||
if (currentStep && isLogicStep(currentStep)) {
|
||||
const target = executeLogic(currentStep)
|
||||
if (!currentStep) return
|
||||
if (isLogicStep(currentStep)) {
|
||||
const target = executeLogic(
|
||||
currentStep,
|
||||
typebot.variables,
|
||||
updateVariableValue
|
||||
)
|
||||
target ? onBlockEnd(target) : displayNextStep()
|
||||
}
|
||||
}, [displayedSteps])
|
||||
if (isIntegrationStep(currentStep)) {
|
||||
const target = await executeIntegration(
|
||||
currentStep,
|
||||
typebot.variables,
|
||||
updateVariableValue
|
||||
)
|
||||
target ? onBlockEnd(target) : displayNextStep()
|
||||
}
|
||||
}
|
||||
|
||||
const autoScrollToBottom = () => {
|
||||
scroll.scrollToBottom({
|
||||
@ -77,7 +83,13 @@ export const ChatBlock = ({
|
||||
const isSingleChoiceStep =
|
||||
isChoiceInput(currentStep) && !currentStep.options.isMultipleChoice
|
||||
if (isSingleChoiceStep)
|
||||
return onBlockEnd(getSingleChoiceTargetId(currentStep, answerContent))
|
||||
return onBlockEnd(
|
||||
getSingleChoiceTargetId(
|
||||
currentStep,
|
||||
typebot.choiceItems,
|
||||
answerContent
|
||||
)
|
||||
)
|
||||
if (
|
||||
currentStep?.target?.blockId ||
|
||||
displayedSteps.length === stepIds.length
|
||||
@ -88,67 +100,6 @@ export const ChatBlock = ({
|
||||
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 (
|
||||
<div className="flex">
|
||||
<HostAvatarsContext>
|
||||
|
13
packages/bot-engine/src/services/inputs.ts
Normal file
13
packages/bot-engine/src/services/inputs.ts
Normal 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
|
||||
}
|
121
packages/bot-engine/src/services/integration.ts
Normal file
121
packages/bot-engine/src/services/integration.ts
Normal 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) }
|
||||
}, {})
|
72
packages/bot-engine/src/services/logic.ts
Normal file
72
packages/bot-engine/src/services/logic.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -49,6 +49,22 @@ model User {
|
||||
folders DashboardFolder[]
|
||||
plan Plan @default(FREE)
|
||||
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 {
|
||||
|
12
packages/models/src/typebot/steps/bubble.ts
Normal file
12
packages/models/src/typebot/steps/bubble.ts
Normal 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 }
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
export * from './steps'
|
||||
export * from './bubble'
|
||||
export * from './inputs'
|
||||
export * from './logic'
|
||||
export * from './integration'
|
||||
|
@ -20,6 +20,13 @@ export enum InputStepType {
|
||||
CHOICE = 'choice input',
|
||||
}
|
||||
|
||||
export type InputStepOptions =
|
||||
| TextInputOptions
|
||||
| NumberInputOptions
|
||||
| EmailInputOptions
|
||||
| DateInputOptions
|
||||
| UrlInputOptions
|
||||
|
||||
export type TextInputStep = StepBase & {
|
||||
type: InputStepType.TEXT
|
||||
options?: TextInputOptions
|
||||
|
52
packages/models/src/typebot/steps/integration.ts
Normal file
52
packages/models/src/typebot/steps/integration.ts
Normal 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>
|
||||
}
|
@ -8,6 +8,8 @@ export enum LogicStepType {
|
||||
CONDITION = 'Condition',
|
||||
}
|
||||
|
||||
export type LogicStepOptions = SetVariableOptions | ConditionOptions
|
||||
|
||||
export type SetVariableStep = StepBase & {
|
||||
type: LogicStepType.SET_VARIABLE
|
||||
options?: SetVariableOptions
|
||||
|
@ -1,15 +1,40 @@
|
||||
import {
|
||||
InputStepOptions,
|
||||
IntegrationStepOptions,
|
||||
IntegrationStepType,
|
||||
LogicStepOptions,
|
||||
} from '.'
|
||||
import { BubbleStep, BubbleStepType } from './bubble'
|
||||
import { InputStep, InputStepType } from './inputs'
|
||||
import { IntegrationStep } from './integration'
|
||||
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 {
|
||||
TEXT = 'text',
|
||||
}
|
||||
export type DraggableStepType =
|
||||
| BubbleStepType
|
||||
| InputStepType
|
||||
| LogicStepType
|
||||
| IntegrationStepType
|
||||
|
||||
export type StepOptions =
|
||||
| InputStepOptions
|
||||
| LogicStepOptions
|
||||
| IntegrationStepOptions
|
||||
|
||||
export type StepBase = { id: string; blockId: string; target?: Target }
|
||||
|
||||
@ -18,9 +43,4 @@ export type StartStep = StepBase & {
|
||||
label: string
|
||||
}
|
||||
|
||||
export type TextStep = StepBase & {
|
||||
type: BubbleStepType.TEXT
|
||||
content: { html: string; richText: unknown[]; plainText: string }
|
||||
}
|
||||
|
||||
export type Target = { blockId: string; stepId?: string }
|
||||
|
@ -5,6 +5,8 @@ import {
|
||||
ConditionStep,
|
||||
InputStep,
|
||||
InputStepType,
|
||||
IntegrationStep,
|
||||
IntegrationStepType,
|
||||
LogicStep,
|
||||
LogicStepType,
|
||||
Step,
|
||||
@ -69,3 +71,6 @@ export const isSingleChoiceInput = (step: Step): step is ChoiceInputStep =>
|
||||
|
||||
export const isConditionStep = (step: Step): step is ConditionStep =>
|
||||
step.type === LogicStepType.CONDITION
|
||||
|
||||
export const isIntegrationStep = (step: Step): step is IntegrationStep =>
|
||||
step.type === IntegrationStepType.GOOGLE_SHEETS
|
||||
|
371
yarn.lock
371
yarn.lock
@ -939,6 +939,13 @@
|
||||
minimatch "^3.0.4"
|
||||
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":
|
||||
version "5.0.2"
|
||||
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"
|
||||
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":
|
||||
version "0.1.7"
|
||||
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"
|
||||
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":
|
||||
version "9.0.0"
|
||||
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"
|
||||
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:
|
||||
version "5.3.2"
|
||||
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"
|
||||
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:
|
||||
version "3.1.0"
|
||||
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"
|
||||
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:
|
||||
version "5.4.1"
|
||||
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"
|
||||
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:
|
||||
version "2.2.0"
|
||||
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"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
base64-js@^1.0.2:
|
||||
base64-js@^1.0.2, base64-js@^1.3.0:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
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"
|
||||
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:
|
||||
version "2.2.0"
|
||||
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"
|
||||
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:
|
||||
version "1.1.2"
|
||||
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"
|
||||
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:
|
||||
version "1.4.36"
|
||||
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"
|
||||
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:
|
||||
version "6.4.5"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.5.tgz#97380f758ae24ac15df8353e0cc27f8b95644655"
|
||||
@ -3665,7 +3718,7 @@ executable@^4.1.1:
|
||||
dependencies:
|
||||
pify "^2.2.0"
|
||||
|
||||
extend@~3.0.2:
|
||||
extend@^3.0.2, extend@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
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"
|
||||
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:
|
||||
version "2.0.2"
|
||||
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"
|
||||
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:
|
||||
version "2.0.5"
|
||||
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"
|
||||
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:
|
||||
version "4.0.0"
|
||||
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"
|
||||
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:
|
||||
version "4.2.9"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96"
|
||||
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:
|
||||
version "1.0.1"
|
||||
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"
|
||||
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:
|
||||
version "1.1.1"
|
||||
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"
|
||||
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:
|
||||
version "1.0.2"
|
||||
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"
|
||||
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:
|
||||
version "0.1.0-beta.24"
|
||||
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"
|
||||
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:
|
||||
version "1.4.9"
|
||||
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:
|
||||
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:
|
||||
version "6.5.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||
@ -7313,6 +7513,11 @@ tr46@^1.0.1:
|
||||
dependencies:
|
||||
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:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec"
|
||||
@ -7375,83 +7580,83 @@ tunnel-agent@^0.6.0:
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
turbo-darwin-64@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.0.24.tgz#f135baff0e44f9160c9b027e8c4dd2d5c8bb10a7"
|
||||
integrity sha512-A65Wxp+jBMfI3QX2uObX6DKvk+TxNXTf7ufQTHvRSLeAreB8QiVzJdYE0nC6YdrRwfPgFY3L72dhYd2v8ouXDg==
|
||||
turbo-darwin-64@1.0.28:
|
||||
version "1.0.28"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.0.28.tgz#662392368698c4e698b31871f0953836872f7b0e"
|
||||
integrity sha512-uvARrncW6HNTFi7PFe4sq4JqSOKs1vPgWjJjOEyVhsCFwBgYkXxYsJSdDfO8OhvJa3wv+eYFAK5RaUCk80Z8eg==
|
||||
|
||||
turbo-darwin-arm64@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-1.0.24.tgz#c360d7cc6a7403855733e3aebb841b1227fbbb2e"
|
||||
integrity sha512-31zfexqUhvk/CIfAUk2mwjlpEjIURXu4QG8hoWlGxpcpAhlnkIX6CXle+LoQSnU3+4EbNe2SE92fYXsT/SnHAg==
|
||||
turbo-darwin-arm64@1.0.28:
|
||||
version "1.0.28"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-1.0.28.tgz#3ba1a6f9a960321391b8cf809ed8fab0276a499d"
|
||||
integrity sha512-d/ANU+RIq4Fx/MphkqFThvwOpb+NYDuR+07aV5w8cwI7ljw7hPAe3EW3CSlkPJhvjs6P/oh+F86jhh1Q581mVA==
|
||||
|
||||
turbo-freebsd-64@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-freebsd-64/-/turbo-freebsd-64-1.0.24.tgz#9ef8914e7d1aaa995a8001a0ad81f7cc4520d332"
|
||||
integrity sha512-vZYbDkOHH5eeQrxsAYldrh2nDY884irtmgJdGbpjryJgnJx+xzriZfoFalm/d1ZfG3ArENRJqGU+k6BriefZzw==
|
||||
turbo-freebsd-64@1.0.28:
|
||||
version "1.0.28"
|
||||
resolved "https://registry.yarnpkg.com/turbo-freebsd-64/-/turbo-freebsd-64-1.0.28.tgz#f94e39dc455573c0a42f96f1a84649b252bf0571"
|
||||
integrity sha512-JMJWftuWhJan+Momc39vbbwaLYEcMpYyBxIrumyIrIkQVaiSKs/6oEFzh1YA+KE16kAgzTPJPXFDkmsY3idAQg==
|
||||
|
||||
turbo-freebsd-arm64@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-freebsd-arm64/-/turbo-freebsd-arm64-1.0.24.tgz#12644e8f1b077f9d7afb367f2b8c2a2e0592ca72"
|
||||
integrity sha512-TDIu1PlyusY8AB69KGM4wGrCjtfbzmVF4Hlgf9mVeSWVKzqkRASorOEq1k8KvfZ+sBTS2GBMpqwpa1KVkYpVhw==
|
||||
turbo-freebsd-arm64@1.0.28:
|
||||
version "1.0.28"
|
||||
resolved "https://registry.yarnpkg.com/turbo-freebsd-arm64/-/turbo-freebsd-arm64-1.0.28.tgz#5551f5c67a82a16ce1ba3193410764bae0849d1a"
|
||||
integrity sha512-fGJNE8qJUhosaIK5sGBheeve9y074FLWv8KfYuXMyV/6+dxpNV60HoAFvw8tL3q8TNp47pU6x8e8h+u1/rn1wQ==
|
||||
|
||||
turbo-linux-32@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-32/-/turbo-linux-32-1.0.24.tgz#6129f7560f5c48214c1724ae7e8196dedc56de21"
|
||||
integrity sha512-lhhK7914sUtuWYcDO8LV7NQkvTIwpAZlYH0XEOC/OTiYRQJvtKbEySLvefvtwuGjx7cGNI6OYraUsY3WWoK3FA==
|
||||
turbo-linux-32@1.0.28:
|
||||
version "1.0.28"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-32/-/turbo-linux-32-1.0.28.tgz#742340e6ca7d77c4fb884159bbc8c7faa8d61026"
|
||||
integrity sha512-fE0qIExxYuVFo5WlVWY0DJ1YZ/w+EC9RheT9nc1tU2EK83XPE1CZFW4lFIsWsXnIy9337zUeNDFVoVxOxCBSUQ==
|
||||
|
||||
turbo-linux-64@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.0.24.tgz#221e3e14037e8fc3108e12a62de209d8a47f0348"
|
||||
integrity sha512-EbfdrkwVsHDG7AIVQ1enWHoD6riAApx4VRAuFcQHTvJU9e+BuOQBMjb7e9jO4mUrpumtN3n20tP+86odRwsk5g==
|
||||
turbo-linux-64@1.0.28:
|
||||
version "1.0.28"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.0.28.tgz#167acbd2899c0da9252f755ae74d619aaf99efe6"
|
||||
integrity sha512-e+f/O1MlcKCMhJf10q1x+1KSImHwuFUW2+A6DbLk+ekBUW5RELC2qF7hGypCzcpm8xIqtj5A0kP3blFy60AMxw==
|
||||
|
||||
turbo-linux-arm64@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.0.24.tgz#95891e7d4375ccbf2478677568557948be33717a"
|
||||
integrity sha512-H4rqlgP2L7G3iAB/un/7DclExzLUkQ1NoZ0p/1Oa7Wb8H1YUlc8GkwUmpIFd5AOFSPL75DjYvlS8T5Tm23i+1A==
|
||||
turbo-linux-arm64@1.0.28:
|
||||
version "1.0.28"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.0.28.tgz#785415e2a04125a69c7b1d45073150dbab3985f6"
|
||||
integrity sha512-zN0nQClxp4nP4edinbdTd/9CpPjgNPsULc8LgunuJD+B9A0NRcRP5NCDo8/6ctTWs456sE3UhUF3t2b+uEgDzw==
|
||||
|
||||
turbo-linux-arm@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-arm/-/turbo-linux-arm-1.0.24.tgz#f5acb74170a8b5a787915e799e7b52840c7c6982"
|
||||
integrity sha512-lCNDVEkwxcn0acyPFVJgV5N5vKAP4LfXb+8uW/JpGHVoPHSONKtzYQG05J1KbHXpIjUT+DNgFtshtsdZYOewZQ==
|
||||
turbo-linux-arm@1.0.28:
|
||||
version "1.0.28"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-arm/-/turbo-linux-arm-1.0.28.tgz#f6de485aa732ba14cc1c851ba30b0f0a901507f3"
|
||||
integrity sha512-PbB/RzN4W9M6sNZTvcjmc3PZ2S4CeFyQv/53tSs82pIlwM7NKVJzxVC0j3xCtoqoDDgXoJBhCpPV7MUEjCARQg==
|
||||
|
||||
turbo-linux-mips64le@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-mips64le/-/turbo-linux-mips64le-1.0.24.tgz#f2cc99570222ac42fdcc0d0638f13bc0176859f9"
|
||||
integrity sha512-AmrgQUDIe9AdNyh5YrI6pfMTUHD/gYfbylNmedLuN5Al3xINdZObcISzd/7VWd+V8wNW/1b9lUnt70Rv/KExfA==
|
||||
turbo-linux-mips64le@1.0.28:
|
||||
version "1.0.28"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-mips64le/-/turbo-linux-mips64le-1.0.28.tgz#4f993f387d90fa99037ad9592e98648fdb9ca608"
|
||||
integrity sha512-7LKmFS9M+AKW5slTHLz00Y4ovZh2CpjgMUkNNC6qtJB8YyWwXwoU0U7Yz28q3+rNVkcEiqWWx4l1Tj1AotTlaA==
|
||||
|
||||
turbo-linux-ppc64le@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-ppc64le/-/turbo-linux-ppc64le-1.0.24.tgz#4d9508290d24cfdbaca24e57d8bcd0127281e2ed"
|
||||
integrity sha512-+6ESjsfrvRUr1AsurNcRTrqYr+XHG8g763+hXLog1MP9mn1cufZqWlAyE4G8/MLXDHsEKgK+tXqPLIyLBRjLEw==
|
||||
turbo-linux-ppc64le@1.0.28:
|
||||
version "1.0.28"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-ppc64le/-/turbo-linux-ppc64le-1.0.28.tgz#053163e9042790102ceb1d299a3b79b1aa197be4"
|
||||
integrity sha512-R382Op75XxcIiY1pWPnVnefxOeVbrVAeABIHLL1hKetbu9UPNzKAnQKqJYGzKIdTRKtPh5CQuErEFzs/Ky2ZgA==
|
||||
|
||||
turbo-windows-32@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-32/-/turbo-windows-32-1.0.24.tgz#2bf906c0cc9d675afc4693221fc339ade29e6c13"
|
||||
integrity sha512-pqRys+FfHxuLVmW/AariITL5qpItp4WPAsYnWLx4u7VpCOO/qmTAI/SL7/jnTm4gxjBv3uf//lisu0AvEZd+TA==
|
||||
turbo-windows-32@1.0.28:
|
||||
version "1.0.28"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-32/-/turbo-windows-32-1.0.28.tgz#b2b735fa56182aaf4095c9e4555fcf39ba050561"
|
||||
integrity sha512-SjDgimlD5TMvkrFRtsJb4uVP7T44gwr0HqiIpAuWj1m5d8Pz/OisOoUkM/ISPKqVycIU5JF8wx0+CTnxC7YNhQ==
|
||||
|
||||
turbo-windows-64@1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.0.24.tgz#5dd30b10110f2bb69caa479ddd72b4c471fb0dea"
|
||||
integrity sha512-YHAWha5XkW0Ate1HtwhzFD32kZFXtC8KB4ReEvHc9GM2inQob1ZinvktS0xi5MC5Sxl9+bObOWmsxeZPOgNCFA==
|
||||
turbo-windows-64@1.0.28:
|
||||
version "1.0.28"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.0.28.tgz#7a2ad6f8416f04f20a1445c8b7123b80c6b16583"
|
||||
integrity sha512-nT7bgcdl/9QNGBiwCYwTQ2VszcsqJ4NqT4YkE954KFZYxgSwMjjVTdoXcsnXMHpWiMiYfFF7HZLecUNnDm1uUA==
|
||||
|
||||
turbo@^1.0.24:
|
||||
version "1.0.24"
|
||||
resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.0.24.tgz#5efdeb44aab2f5e97b24a3e0ed4a159bfcd0a877"
|
||||
integrity sha512-bfOr7iW48+chDl+yKiZ5FIWzXOF6xOIyrAGPaWI+I5CdD27IZCEGvqvTV/weaHvjLbV7otybHQ56XCybBlVjoA==
|
||||
turbo@^1.0.28:
|
||||
version "1.0.28"
|
||||
resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.0.28.tgz#fb33cecd2d025e3140ddbcc8c274f0ed7328f09d"
|
||||
integrity sha512-5xmyVabNYqA0sCAU4VLdUS2A6GwIyy8FTszB/Fx4eNHwHudQwo00F2qORcDFwBHE4MqtnRoBFhL3ZJzo8c9A2w==
|
||||
optionalDependencies:
|
||||
turbo-darwin-64 "1.0.24"
|
||||
turbo-darwin-arm64 "1.0.24"
|
||||
turbo-freebsd-64 "1.0.24"
|
||||
turbo-freebsd-arm64 "1.0.24"
|
||||
turbo-linux-32 "1.0.24"
|
||||
turbo-linux-64 "1.0.24"
|
||||
turbo-linux-arm "1.0.24"
|
||||
turbo-linux-arm64 "1.0.24"
|
||||
turbo-linux-mips64le "1.0.24"
|
||||
turbo-linux-ppc64le "1.0.24"
|
||||
turbo-windows-32 "1.0.24"
|
||||
turbo-windows-64 "1.0.24"
|
||||
turbo-darwin-64 "1.0.28"
|
||||
turbo-darwin-arm64 "1.0.28"
|
||||
turbo-freebsd-64 "1.0.28"
|
||||
turbo-freebsd-arm64 "1.0.28"
|
||||
turbo-linux-32 "1.0.28"
|
||||
turbo-linux-64 "1.0.28"
|
||||
turbo-linux-arm "1.0.28"
|
||||
turbo-linux-arm64 "1.0.28"
|
||||
turbo-linux-mips64le "1.0.28"
|
||||
turbo-linux-ppc64le "1.0.28"
|
||||
turbo-windows-32 "1.0.28"
|
||||
turbo-windows-64 "1.0.28"
|
||||
|
||||
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
||||
version "0.14.5"
|
||||
@ -7522,6 +7727,11 @@ uri-js@^4.2.2:
|
||||
dependencies:
|
||||
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:
|
||||
version "0.10.3"
|
||||
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"
|
||||
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
|
||||
|
||||
uuid@^8.3.2:
|
||||
uuid@^8.0.0, uuid@^8.3.2:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
@ -7644,11 +7854,24 @@ watchpack@2.3.0:
|
||||
glob-to-regexp "^0.4.1"
|
||||
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:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
|
||||
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:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
|
||||
|
Reference in New Issue
Block a user