2
0

♻️ Introduce typebot v6 with events (#1013)

Closes #885
This commit is contained in:
Baptiste Arnaud
2023-11-08 15:34:16 +01:00
committed by GitHub
parent 68e4fc71fb
commit 35300eaf34
634 changed files with 58971 additions and 31449 deletions

View File

@ -5,7 +5,6 @@
"version": "0.1.0",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"scripts": {
"build": "pnpm tsc --noEmit && tsup",
"dev": "tsup --watch"
@ -41,7 +40,7 @@
"@typebot.io/tsconfig": "workspace:*",
"tsup": "6.5.0",
"typebot-js": "workspace:*",
"typescript": "4.9.4",
"typescript": "5.2.2",
"@typebot.io/lib": "workspace:*"
},
"peerDependencies": {

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react'
import { useAnswers } from '../../../providers/AnswersProvider'
import { InputBlock, InputBlockType } from '@typebot.io/schemas'
import { InputBlock } from '@typebot.io/schemas'
import { GuestBubble } from './bubbles/GuestBubble'
import { byId } from '@typebot.io/lib'
import { InputSubmitContent } from '@/types'
@ -17,6 +17,9 @@ import { ChoiceForm } from '@/features/blocks/inputs/buttons'
import { PaymentForm } from '@/features/blocks/inputs/payment'
import { RatingForm } from '@/features/blocks/inputs/rating'
import { FileUploadForm } from '@/features/blocks/inputs/fileUpload'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { getBlockById } from '@typebot.io/lib/getBlockById'
export const InputChatBlock = ({
block,
@ -39,9 +42,11 @@ export const InputChatBlock = ({
const [answer, setAnswer] = useState<string>()
const [isEditting, setIsEditting] = useState(false)
const { variableId } = block.options
const { variableId } = block.options ?? {}
const defaultValue =
(typebot.settings.general.isInputPrefillEnabled ?? true) && variableId
(typebot.settings.general?.isInputPrefillEnabled ??
defaultSettings.general.isInputPrefillEnabled) &&
variableId
? typebot.variables.find(
(variable) =>
variable.name === typebot.variables.find(byId(variableId))?.name
@ -51,14 +56,17 @@ export const InputChatBlock = ({
const handleSubmit = async ({ label, value, itemId }: InputSubmitContent) => {
setAnswer(label ?? value)
const isRetry = !isInputValid(value, block.type)
if (!isRetry && addAnswer)
if (!isRetry && addAnswer) {
const { group } = getBlockById(block.id, typebot.groups)
await addAnswer(typebot.variables)({
blockId: block.id,
groupId: block.groupId,
groupId: group.id,
content: value,
variableId,
uploadedFiles: block.type === InputBlockType.FILE,
})
}
if (!isEditting) onTransitionEnd({ label, value, itemId }, isRetry)
setIsEditting(false)
}
@ -66,11 +74,11 @@ export const InputChatBlock = ({
if (isLoading) return null
if (answer) {
const avatarUrl = typebot.theme.chat.guestAvatar?.url
const avatarUrl = typebot.theme.chat?.guestAvatar?.url
return (
<GuestBubble
message={answer}
showAvatar={typebot.theme.chat.guestAvatar?.isEnabled ?? false}
showAvatar={typebot.theme.chat?.guestAvatar?.isEnabled ?? false}
avatarSrc={avatarUrl && parseVariables(typebot.variables)(avatarUrl)}
/>
)
@ -160,7 +168,7 @@ const Input = ({
<PaymentForm
options={block.options}
onSuccess={() =>
onSubmit({ value: block.options.labels.success ?? 'Success' })
onSubmit({ value: block.options?.labels?.success ?? 'Success' })
}
/>
)

View File

@ -3,7 +3,8 @@ import { EmbedBubble } from '@/features/blocks/bubbles/embed'
import { ImageBubble } from '@/features/blocks/bubbles/image'
import { TextBubble } from '@/features/blocks/bubbles/textBubble'
import { VideoBubble } from '@/features/blocks/bubbles/video'
import { BubbleBlock, BubbleBlockType } from '@typebot.io/schemas'
import { BubbleBlock } from '@typebot.io/schemas'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
type Props = {
block: BubbleBlock
@ -23,7 +24,7 @@ export const HostBubble = ({ block, onTransitionEnd }: Props) => {
case BubbleBlockType.AUDIO:
return (
<AudioBubble
url={block.content.url}
url={block.content?.url}
onTransitionEnd={onTransitionEnd}
/>
)

View File

@ -15,7 +15,6 @@ import {
import {
BubbleBlock,
InputBlock,
LogicBlockType,
PublicTypebot,
Block,
} from '@typebot.io/schemas'
@ -30,6 +29,8 @@ import { executeIntegration } from '@/utils/executeIntegration'
import { executeLogic } from '@/utils/executeLogic'
import { blockCanBeRetried, parseRetryBlock } from '@/utils/inputs'
import { PopupBlockedToast } from '../PopupBlockedToast'
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
import { getBlockById } from '@typebot.io/lib/getBlockById'
type ChatGroupProps = {
blocks: Block[]
@ -143,19 +144,20 @@ export const ChatGroup = ({
if (blockedPopupUrl) setBlockedPopupUrl(blockedPopupUrl)
const isRedirecting =
currentBlock.type === LogicBlockType.REDIRECT &&
currentBlock.options.isNewTab === false
currentBlock.options?.isNewTab === false
if (isRedirecting) return
nextEdgeId
? onGroupEnd({ edgeId: nextEdgeId, updatedTypebot: linkedTypebot })
: displayNextBlock()
}
if (isIntegrationBlock(currentBlock)) {
const { group } = getBlockById(currentBlock.id, typebot.groups)
const nextEdgeId = await executeIntegration({
block: currentBlock,
context: {
apiHost,
typebotId: currentTypebotId,
groupId: currentBlock.groupId,
groupId: group.id,
blockId: currentBlock.id,
variables: typebot.variables,
isPreview,
@ -181,10 +183,12 @@ export const ChatGroup = ({
scroll()
const currentBlock = [...processedBlocks].pop()
if (currentBlock) {
if (isRetry && blockCanBeRetried(currentBlock))
if (isRetry && blockCanBeRetried(currentBlock)) {
const { group } = getBlockById(currentBlock.id, typebot.groups)
return insertBlockInStack(
parseRetryBlock(currentBlock, typebot.variables, createEdge)
parseRetryBlock(currentBlock, group.id, typebot.variables, createEdge)
)
}
if (
isInputBlock(currentBlock) &&
currentBlock.options?.variableId &&
@ -196,7 +200,7 @@ export const ChatGroup = ({
)
}
const isSingleChoiceBlock =
isChoiceInput(currentBlock) && !currentBlock.options.isMultipleChoice
isChoiceInput(currentBlock) && !currentBlock.options?.isMultipleChoice
if (isSingleChoiceBlock) {
const nextEdgeId = currentBlock.items.find(
byId(answerContent?.itemId)
@ -214,7 +218,7 @@ export const ChatGroup = ({
nextBlock ? insertBlockInStack(nextBlock) : onGroupEnd({})
}
const avatarSrc = typebot.theme.chat.hostAvatar?.url
const avatarSrc = typebot.theme.chat?.hostAvatar?.url
return (
<div className="flex w-full" data-group-name={groupTitle}>
@ -224,10 +228,10 @@ export const ChatGroup = ({
key={idx}
displayChunk={chunk}
hostAvatar={{
isEnabled: typebot.theme.chat.hostAvatar?.isEnabled ?? true,
isEnabled: typebot.theme.chat?.hostAvatar?.isEnabled ?? true,
src: avatarSrc && parseVariables(typebot.variables)(avatarSrc),
}}
hasGuestAvatar={typebot.theme.chat.guestAvatar?.isEnabled ?? false}
hasGuestAvatar={typebot.theme.chat?.guestAvatar?.isEnabled ?? false}
onDisplayNextBlock={displayNextBlock}
keepShowingHostAvatar={keepShowingHostAvatar}
blockedPopupUrl={blockedPopupUrl}

View File

@ -57,7 +57,7 @@ export const ConversationContainer = ({
if (!nextGroup) return
onNewGroupVisible({
id: 'edgeId',
from: { groupId: 'block', blockId: 'block' },
from: { blockId: 'block' },
to: { groupId },
})
return setDisplayedGroups([

View File

@ -7,7 +7,6 @@ import { ConversationContainer } from './ConversationContainer'
import { AnswersProvider } from '../providers/AnswersProvider'
import {
AnswerInput,
BackgroundType,
Edge,
PublicTypebot,
VariableWithValue,
@ -16,6 +15,7 @@ import { Log } from '@typebot.io/prisma'
import { LiteBadge } from './LiteBadge'
import { isNotEmpty } from '@typebot.io/lib'
import { getViewerUrl } from '@typebot.io/lib/getViewerUrl'
import { BackgroundType } from '@typebot.io/schemas/features/typebot/theme/constants'
export type TypebotViewerProps = {
typebot: Omit<PublicTypebot, 'updatedAt' | 'createdAt'>
@ -78,7 +78,7 @@ export const TypebotViewer = ({
<style
dangerouslySetInnerHTML={{
__html: `@import url('https://fonts.googleapis.com/css2?family=${
typebot.theme.general.font ?? 'Open Sans'
typebot.theme.general?.font ?? 'Open Sans'
}:ital,wght@0,300;0,400;0,600;1,300;1,400;1,600&display=swap');`,
}}
/>
@ -112,7 +112,7 @@ export const TypebotViewer = ({
startGroupId={startGroupId}
/>
</div>
{typebot.settings.general.isBrandingEnabled && <LiteBadge />}
{typebot.settings.general?.isBrandingEnabled && <LiteBadge />}
</div>
</AnswersProvider>
</TypebotProvider>

View File

@ -1,11 +1,11 @@
import { useEffect, useRef, useState } from 'react'
import { useTypebot } from '@/providers/TypebotProvider'
import { AudioBubbleContent } from '@typebot.io/schemas'
import { TypingBubble } from '@/components/TypingBubble'
import { parseVariables } from '@/features/variables'
import { AudioBubbleBlock } from '@typebot.io/schemas'
type Props = {
url: AudioBubbleContent['url']
url: NonNullable<AudioBubbleBlock['content']>['url']
onTransitionEnd: () => void
}

View File

@ -36,7 +36,7 @@ export const EmbedBubble = ({ block, onTransitionEnd }: Props) => {
}
}, [isLoading, isTyping, onTypingEnd])
const height = block.content.height
const height = block.content?.height
? typeof block.content.height === 'string'
? parseVariables(typebot.variables)(block.content.height) + 'px'
: block.content.height

View File

@ -1,9 +1,10 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTypebot } from '@/providers/TypebotProvider'
import { BubbleBlockType, TextBubbleBlock } from '@typebot.io/schemas'
import { TextBubbleBlock } from '@typebot.io/schemas'
import { computeTypingDuration } from '../utils/computeTypingDuration'
import { parseVariables } from '@/features/variables'
import { TypingBubble } from '@/components/TypingBubble'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
type Props = {
block: TextBubbleBlock
@ -12,19 +13,13 @@ type Props = {
export const showAnimationDuration = 400
const defaultTypingEmulation = {
enabled: true,
speed: 300,
maxDelay: 1.5,
}
export const TextBubble = ({ block, onTransitionEnd }: Props) => {
const { typebot, isLoading } = useTypebot()
const messageContainer = useRef<HTMLDivElement | null>(null)
const [isTyping, setIsTyping] = useState(true)
const [content] = useState(
parseVariables(typebot.variables)(block.content.html)
parseVariables(typebot.variables)(block.content?.html)
)
const onTypingEnd = useCallback(() => {
@ -37,10 +32,10 @@ export const TextBubble = ({ block, onTransitionEnd }: Props) => {
useEffect(() => {
if (!isTyping || isLoading) return
const typingTimeout = computeTypingDuration(
block.content.plainText ?? '',
typebot.settings?.typingEmulation ?? defaultTypingEmulation
)
const typingTimeout = computeTypingDuration({
bubbleContent: block.content?.plainText ?? '',
typingSettings: typebot.settings?.typingEmulation,
})
const timeout = setTimeout(() => {
onTypingEnd()
}, typingTimeout)
@ -49,7 +44,7 @@ export const TextBubble = ({ block, onTransitionEnd }: Props) => {
clearTimeout(timeout)
}
}, [
block.content.plainText,
block.content?.plainText,
isLoading,
isTyping,
onTypingEnd,

View File

@ -1,16 +1,25 @@
import { TypingEmulation } from '@typebot.io/schemas'
import { Settings } from '@typebot.io/schemas'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
export const computeTypingDuration = (
bubbleContent: string,
typingSettings: TypingEmulation
) => {
type Props = {
bubbleContent: string
typingSettings?: Settings['typingEmulation']
}
export const computeTypingDuration = ({
bubbleContent,
typingSettings,
}: Props) => {
let wordCount = bubbleContent.match(/(\w+)/g)?.length ?? 0
if (wordCount === 0) wordCount = bubbleContent.length
const typedWordsPerMinute = typingSettings.speed
let typingTimeout = typingSettings.enabled
? (wordCount / typedWordsPerMinute) * 60000
: 0
if (typingTimeout > typingSettings.maxDelay * 1000)
typingTimeout = typingSettings.maxDelay * 1000
const { enabled, speed, maxDelay } = {
enabled: typingSettings?.enabled ?? defaultSettings.typingEmulation.enabled,
speed: typingSettings?.speed ?? defaultSettings.typingEmulation.speed,
maxDelay:
typingSettings?.maxDelay ?? defaultSettings.typingEmulation.maxDelay,
}
const typedWordsPerMinute = speed
let typingTimeout = enabled ? (wordCount / typedWordsPerMinute) * 60000 : 0
if (typingTimeout > maxDelay * 1000) typingTimeout = maxDelay * 1000
return typingTimeout
}

View File

@ -1,13 +1,9 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTypebot } from '@/providers/TypebotProvider'
import {
Variable,
VideoBubbleContent,
VideoBubbleContentType,
VideoBubbleBlock,
} from '@typebot.io/schemas'
import { Variable, VideoBubbleBlock } from '@typebot.io/schemas'
import { TypingBubble } from '@/components/TypingBubble'
import { parseVariables } from '@/features/variables'
import { VideoBubbleContentType } from '@typebot.io/schemas/features/blocks/bubbles/video/constants'
type Props = {
block: VideoBubbleBlock
@ -71,7 +67,7 @@ const VideoContent = ({
isTyping,
variables,
}: {
content?: VideoBubbleContent
content?: VideoBubbleBlock['content']
isTyping: boolean
variables: Variable[]
}) => {

View File

@ -1,12 +1,12 @@
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import { DateInputOptions } from '@typebot.io/schemas'
import { useState } from 'react'
import { parseReadableDate } from '../utils/parseReadableDate'
import { DateInputBlock } from '@typebot.io/schemas'
type DateInputProps = {
onSubmit: (inputValue: InputSubmitContent) => void
options?: DateInputOptions
options: DateInputBlock['options']
}
export const DateForm = ({

View File

@ -2,9 +2,10 @@ import { Spinner, SendButton } from '@/components/SendButton'
import { useAnswers } from '@/providers/AnswersProvider'
import { useTypebot } from '@/providers/TypebotProvider'
import { InputSubmitContent } from '@/types'
import { defaultFileInputOptions, FileInputBlock } from '@typebot.io/schemas'
import { FileInputBlock } from '@typebot.io/schemas'
import React, { ChangeEvent, FormEvent, useState, DragEvent } from 'react'
import { uploadFiles } from '../helpers/uploadFiles'
import { defaultFileInputOptions } from '@typebot.io/schemas/features/blocks/inputs/file/constants'
type Props = {
block: FileInputBlock
@ -13,13 +14,13 @@ type Props = {
}
export const FileUploadForm = ({
block: {
id,
options: { isMultipleAllowed, labels, sizeLimit, isRequired },
},
block: { id, options },
onSubmit,
onSkip,
}: Props) => {
const { isMultipleAllowed, labels, isRequired } = options ?? {}
const sizeLimit =
options && 'sizeLimit' in options ? options?.sizeLimit : undefined
const { isPreview, currentTypebotId } = useTypebot()
const { resultId } = useAnswers()
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
@ -160,7 +161,7 @@ export const FileUploadForm = ({
)}
<p
className="text-sm text-gray-500 text-center"
dangerouslySetInnerHTML={{ __html: labels.placeholder }}
dangerouslySetInnerHTML={{ __html: labels?.placeholder ?? '' }}
/>
</div>
<input
@ -181,7 +182,7 @@ export const FileUploadForm = ({
}
onClick={onSkip}
>
{labels.skip ?? defaultFileInputOptions.labels.skip}
{labels?.skip ?? defaultFileInputOptions.labels.skip}
</button>
</div>
)}
@ -195,17 +196,17 @@ export const FileUploadForm = ({
}
onClick={clearFiles}
>
{labels.clear ?? defaultFileInputOptions.labels.clear}
{labels?.clear ?? defaultFileInputOptions.labels.clear}
</button>
)}
<SendButton
type="submit"
label={
labels.button === defaultFileInputOptions.labels.button
labels?.button === defaultFileInputOptions.labels.button
? `${labels.button} ${selectedFiles.length} file${
selectedFiles.length > 1 ? 's' : ''
}`
: labels.button
: labels?.button ?? ''
}
disableIcon
/>

View File

@ -23,7 +23,6 @@ export const uploadFiles = async ({
i += 1
const { data } = await sendRequest<{
presignedUrl: string
formData: Record<string, string>
hasReachedStorageLimit: boolean
}>(
`${basePath}/storage/upload-url?filePath=${encodeURIComponent(
@ -36,14 +35,9 @@ export const uploadFiles = async ({
const url = data.presignedUrl
if (data.hasReachedStorageLimit) urls.push(null)
else {
const formData = new FormData()
Object.entries(data.formData).forEach(([key, value]) => {
formData.append(key, value)
})
formData.append('file', file)
const upload = await fetch(data.presignedUrl, {
method: 'POST',
body: formData,
const upload = await fetch(url, {
method: 'PUT',
body: file,
})
if (!upload.ok) continue

View File

@ -1,14 +1,15 @@
import { PaymentInputOptions, PaymentProvider } from '@typebot.io/schemas'
import React from 'react'
import { PaymentInputBlock } from '@typebot.io/schemas'
import { StripePaymentForm } from './StripePaymentForm'
import { PaymentProvider } from '@typebot.io/schemas/features/blocks/inputs/payment/constants'
type Props = {
onSuccess: () => void
options: PaymentInputOptions
options: PaymentInputBlock['options']
}
export const PaymentForm = ({ onSuccess, options }: Props): JSX.Element => {
switch (options.provider) {
switch (options?.provider) {
case undefined:
case PaymentProvider.STRIPE:
return <StripePaymentForm onSuccess={onSuccess} options={options} />
}

View File

@ -1,7 +1,7 @@
import React, { FormEvent, useEffect, useState } from 'react'
import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js'
import { Elements } from '@stripe/react-stripe-js'
import { PaymentInputOptions, Variable } from '@typebot.io/schemas'
import { PaymentInputBlock, Variable } from '@typebot.io/schemas'
import { SendButton, Spinner } from '@/components/SendButton'
import { initStripe } from '@/lib/stripe'
import { parseVariables } from '@/features/variables'
@ -11,7 +11,7 @@ import { createPaymentIntentQuery } from '../../queries/createPaymentIntentQuery
import { Stripe } from '@stripe/stripe-js'
type Props = {
options: PaymentInputOptions
options: PaymentInputBlock['options']
onSuccess: () => void
}
@ -76,7 +76,7 @@ const CheckoutForm = ({
onSuccess: () => void
clientSecret: string
amountLabel: string
options: PaymentInputOptions
options: PaymentInputBlock['options']
variables: Variable[]
viewerHost: string
}) => {
@ -131,13 +131,13 @@ const CheckoutForm = ({
return_url: viewerHost,
payment_method_data: {
billing_details: {
name: options.additionalInformation?.name
? parseVariables(variables)(options.additionalInformation?.name)
name: options?.additionalInformation?.name
? parseVariables(variables)(options.additionalInformation.name)
: undefined,
email: options.additionalInformation?.email
email: options?.additionalInformation?.email
? parseVariables(variables)(options.additionalInformation?.email)
: undefined,
phone: options.additionalInformation?.phoneNumber
phone: options?.additionalInformation?.phoneNumber
? parseVariables(variables)(
options.additionalInformation?.phoneNumber
)
@ -172,7 +172,7 @@ const CheckoutForm = ({
/>
{isPayButtonVisible && (
<SendButton
label={`${options.labels.button} ${amountLabel}`}
label={`${options?.labels?.button} ${amountLabel}`}
isDisabled={isLoading || !stripe || !elements}
isLoading={isLoading}
className="mt-4 w-full max-w-lg"

View File

@ -1,4 +1,4 @@
import { PaymentInputOptions, Variable } from '@typebot.io/schemas'
import { PaymentInputBlock, Variable } from '@typebot.io/schemas'
import { sendRequest } from '@typebot.io/lib'
export const createPaymentIntentQuery = ({
@ -7,7 +7,7 @@ export const createPaymentIntentQuery = ({
inputOptions,
variables,
}: {
inputOptions: PaymentInputOptions
inputOptions: PaymentInputBlock['options']
apiHost: string
variables: Variable[]
isPreview: boolean

View File

@ -52,8 +52,10 @@ export const PhoneInput = ({
ref={inputRef}
value={inputValue}
onChange={handleChange}
placeholder={block.options.labels.placeholder ?? 'Your phone number...'}
defaultCountry={block.options.defaultCountryCode as Country}
placeholder={
block.options?.labels?.placeholder ?? 'Your phone number...'
}
defaultCountry={block.options?.defaultCountryCode as Country}
autoFocus={!isMobile}
/>
<SendButton

View File

@ -1,8 +1,9 @@
import { InputSubmitContent } from '@/types'
import { RatingInputOptions, RatingInputBlock } from '@typebot.io/schemas'
import { RatingInputBlock } from '@typebot.io/schemas'
import React, { FormEvent, useState } from 'react'
import { isDefined, isEmpty, isNotDefined } from '@typebot.io/lib'
import { SendButton } from '../../../../../components/SendButton'
import { defaultRatingInputOptions } from '@typebot.io/schemas/features/blocks/inputs/rating/constants'
type Props = {
block: RatingInputBlock
@ -19,14 +20,14 @@ export const RatingForm = ({ block, onSubmit }: Props) => {
}
const handleClick = (rating: number) => {
if (block.options.isOneClickSubmitEnabled)
if (block.options?.isOneClickSubmitEnabled)
onSubmit({ value: rating.toString() })
setRating(rating)
}
return (
<form className="flex flex-col" onSubmit={handleSubmit}>
{block.options.labels.left && (
{block.options?.labels?.left && (
<span className="text-sm w-full mb-2 rating-label">
{block.options.labels.left}
</span>
@ -34,20 +35,20 @@ export const RatingForm = ({ block, onSubmit }: Props) => {
<div className="flex flex-wrap justify-center">
{Array.from(
Array(
block.options.length +
(block.options.buttonType === 'Numbers' ? 1 : 0)
(block.options?.length ?? defaultRatingInputOptions.length) +
(block.options?.buttonType === 'Numbers' ? 1 : 0)
)
).map((_, idx) => (
<RatingButton
{...block.options}
key={idx}
rating={rating}
idx={idx + (block.options.buttonType === 'Numbers' ? 0 : 1)}
idx={idx + (block.options?.buttonType === 'Numbers' ? 0 : 1)}
onClick={handleClick}
/>
))}
</div>
{block.options.labels.right && (
{block.options?.labels?.right && (
<span className="text-sm w-full text-right mb-2 pr-2 rating-label">
{block.options.labels.right}
</span>
@ -56,7 +57,7 @@ export const RatingForm = ({ block, onSubmit }: Props) => {
<div className="flex justify-end mr-2">
{isDefined(rating) && (
<SendButton
label={block.options?.labels.button ?? 'Send'}
label={block.options?.labels?.button ?? 'Send'}
disableIcon
/>
)}
@ -71,7 +72,10 @@ const RatingButton = ({
buttonType,
customIcon,
onClick,
}: Pick<RatingInputOptions, 'buttonType' | 'customIcon'> & {
}: Pick<
NonNullable<RatingInputBlock['options']>,
'buttonType' | 'customIcon'
> & {
rating: number | undefined
idx: number
onClick: (idx: number) => void
@ -100,7 +104,7 @@ const RatingButton = ({
onClick={() => onClick(idx)}
dangerouslySetInnerHTML={{
__html:
customIcon.isEnabled && !isEmpty(customIcon.svg)
customIcon?.isEnabled && !isEmpty(customIcon.svg)
? customIcon.svg
: defaultIcon,
}}

View File

@ -2,9 +2,11 @@ import { parseVariables } from '@/features/variables'
import { IntegrationState } from '@/types'
import { sendEventToParent } from '@/utils/chat'
import { isEmbedded } from '@/utils/helpers'
import { ChatwootBlock, ChatwootOptions } from '@typebot.io/schemas'
import { ChatwootBlock } from '@typebot.io/schemas'
const parseSetUserCode = (user: ChatwootOptions['user']) => `
const parseSetUserCode = (
user: NonNullable<ChatwootBlock['options']>['user']
) => `
window.$chatwoot.setUser("${user?.id ?? ''}", {
email: ${user?.email ? `"${user.email}"` : 'undefined'},
name: ${user?.name ? `"${user.name}"` : 'undefined'},
@ -17,7 +19,7 @@ const parseChatwootOpenCode = ({
baseUrl,
websiteToken,
user,
}: ChatwootOptions) => `
}: ChatwootBlock['options'] = {}) => `
if (window.$chatwoot) {
if(${Boolean(user)}) {
${parseSetUserCode(user)}

View File

@ -3,7 +3,6 @@ import { IntegrationState } from '@/types'
import { parseLog } from '@/utils/helpers'
import {
GoogleSheetsBlock,
GoogleSheetsAction,
GoogleSheetsInsertRowOptions,
GoogleSheetsUpdateRowOptions,
GoogleSheetsGetOptions,
@ -12,12 +11,14 @@ import {
Variable,
} from '@typebot.io/schemas'
import { sendRequest, byId } from '@typebot.io/lib'
import { GoogleSheetsAction } from '@typebot.io/schemas/features/blocks/integrations/googleSheets/constants'
export const executeGoogleSheetBlock = async (
block: GoogleSheetsBlock,
context: IntegrationState
) => {
if (!('action' in block.options)) return block.outgoingEdgeId
if (!block.options || !('action' in block.options))
return block.outgoingEdgeId
switch (block.options.action) {
case GoogleSheetsAction.INSERT_ROW:
insertRowInGoogleSheets(block.options, context)
@ -68,7 +69,7 @@ const updateRowInGoogleSheets = (
options: GoogleSheetsUpdateRowOptions,
{ variables, apiHost, onNewLog, resultId }: IntegrationState
) => {
if (!options.cellsToUpsert || !options.referenceCell) return
if (!options.cellsToUpsert || !('referenceCell' in options)) return
sendRequest({
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}`,
method: 'POST',
@ -78,8 +79,8 @@ const updateRowInGoogleSheets = (
values: parseCellValues(options.cellsToUpsert, variables),
resultId,
referenceCell: {
column: options.referenceCell.column,
value: parseVariables(variables)(options.referenceCell.value ?? ''),
column: options.referenceCell?.column,
value: parseVariables(variables)(options.referenceCell?.value ?? ''),
},
},
}).then(({ error }) => {
@ -113,15 +114,18 @@ const getRowFromGoogleSheets = async (
body: {
action: GoogleSheetsAction.GET,
credentialsId: options.credentialsId,
referenceCell: options.referenceCell
? {
column: options.referenceCell.column,
value: parseVariables(variables)(options.referenceCell.value ?? ''),
}
: undefined,
referenceCell:
'referenceCell' in options
? {
column: options.referenceCell?.column,
value: parseVariables(variables)(
options.referenceCell?.value ?? ''
),
}
: undefined,
filter: options.filter
? {
comparisons: options.filter.comparisons.map((comparison) => ({
comparisons: options.filter.comparisons?.map((comparison) => ({
...comparison,
value: parseVariables(variables)(comparison.value),
})),

View File

@ -29,18 +29,18 @@ export const executeSendEmailBlock = (
url: `${apiHost}/api/typebots/${typebotId}/integrations/email?resultId=${resultId}`,
method: 'POST',
body: {
credentialsId: options.credentialsId,
recipients: options.recipients.map(parseVariables(variables)),
subject: parseVariables(variables)(options.subject ?? ''),
body: parseVariables(variables)(options.body ?? ''),
cc: (options.cc ?? []).map(parseVariables(variables)),
bcc: (options.bcc ?? []).map(parseVariables(variables)),
replyTo: options.replyTo
credentialsId: options?.credentialsId,
recipients: options?.recipients?.map(parseVariables(variables)),
subject: parseVariables(variables)(options?.subject ?? ''),
body: parseVariables(variables)(options?.body ?? ''),
cc: (options?.cc ?? []).map(parseVariables(variables)),
bcc: (options?.bcc ?? []).map(parseVariables(variables)),
replyTo: options?.replyTo
? parseVariables(variables)(options.replyTo)
: undefined,
fileUrls: variables.find(byId(options.attachmentsVariableId))?.value,
isCustomBody: options.isCustomBody,
isBodyCode: options.isBodyCode,
fileUrls: variables.find(byId(options?.attachmentsVariableId))?.value,
isCustomBody: options?.isCustomBody,
isBodyCode: options?.isBodyCode,
resultValues,
},
}).then(({ error }) => {

View File

@ -48,7 +48,7 @@ export const executeWebhook = async (
: 'Webhook successfuly executed',
details: JSON.stringify(error ?? data, null, 2).substring(0, 1000),
})
const newVariables = block.options.responseVariableMapping.reduce<
const newVariables = block.options?.responseVariableMapping?.reduce<
VariableWithUnknowValue[]
>((newVariables, varMapping) => {
if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables
@ -66,6 +66,6 @@ export const executeWebhook = async (
return newVariables
}
}, [])
updateVariables(newVariables)
if (newVariables) updateVariables(newVariables)
return block.outgoingEdgeId
}

View File

@ -1,13 +1,11 @@
import { parseVariables } from '@/features/variables'
import { EdgeId, LogicState } from '@/types'
import {
Comparison,
ComparisonOperators,
ConditionBlock,
LogicalOperator,
Variable,
} from '@typebot.io/schemas'
import { Comparison, ConditionBlock, Variable } from '@typebot.io/schemas'
import { isNotDefined, isDefined } from '@typebot.io/lib'
import {
LogicalOperator,
ComparisonOperators,
} from '@typebot.io/schemas/features/blocks/logic/condition/constants'
export const executeCondition = (
block: ConditionBlock,
@ -16,9 +14,9 @@ export const executeCondition = (
const passedCondition = block.items.find((item) => {
const { content } = item
const isConditionPassed =
content.logicalOperator === LogicalOperator.AND
? content.comparisons.every(executeComparison(variables))
: content.comparisons.some(executeComparison(variables))
content?.logicalOperator === LogicalOperator.AND
? content.comparisons?.every(executeComparison(variables))
: content?.comparisons?.some(executeComparison(variables))
return isConditionPassed
})
return passedCondition ? passedCondition.outgoingEdgeId : block.outgoingEdgeId

View File

@ -8,7 +8,7 @@ export const executeScript = async (
block: ScriptBlock,
{ typebot: { variables } }: LogicState
) => {
if (!block.options.content) return
if (!block.options?.content) return
if (block.options.shouldExecuteInParentContext && isEmbedded) {
sendEventToParent({
codeToExecute: parseVariables(variables)(block.options.content),

View File

@ -8,6 +8,8 @@ export const executeSetVariable = (
{ typebot: { variables }, updateVariableValue, updateVariables }: LogicState
): EdgeId | undefined => {
if (!block.options?.variableId) return block.outgoingEdgeId
if (block.options.type !== undefined && block.options.type !== 'Custom')
return block.outgoingEdgeId
const evaluatedExpression = block.options.expressionToEvaluate
? evaluateSetVariableExpression(variables)(
block.options.expressionToEvaluate

View File

@ -9,10 +9,10 @@ export const fetchAndInjectTypebot = async (
): Promise<LinkedTypebot | undefined> => {
const { data, error } = isPreview
? await sendRequest<{ typebot: Typebot }>(
`/api/typebots/${block.options.typebotId}`
`/api/typebots/${block.options?.typebotId}`
)
: await sendRequest<{ typebot: PublicTypebot }>(
`${apiHost}/api/publicTypebots/${block.options.typebotId}`
`${apiHost}/api/publicTypebots/${block.options?.typebotId}`
)
if (!data || error) return
return injectLinkedTypebot(data.typebot)

View File

@ -21,12 +21,12 @@ export const executeTypebotLink = async (
currentTypebotId,
} = context
const linkedTypebot = (
block.options.typebotId === 'current'
block.options?.typebotId === 'current'
? typebot
: [typebot, ...linkedTypebots].find((typebot) =>
'typebotId' in typebot
? typebot.typebotId === block.options.typebotId
: typebot.id === block.options.typebotId
? typebot.typebotId === block.options?.typebotId
: typebot.id === block.options?.typebotId
) ?? (await fetchAndInjectTypebot(block, context))
) as PublicTypebot | LinkedTypebot | undefined
if (!linkedTypebot) {
@ -47,13 +47,13 @@ export const executeTypebotLink = async (
'typebotId' in linkedTypebot ? linkedTypebot.typebotId : linkedTypebot.id
)
const nextGroupId =
block.options.groupId ??
block.options?.groupId ??
linkedTypebot.groups.find((b) => b.blocks.some((s) => s.type === 'start'))
?.id
if (!nextGroupId) return { nextEdgeId: block.outgoingEdgeId }
const newEdge: Edge = {
id: (Math.random() * 1000).toString(),
from: { blockId: '', groupId: '' },
from: { blockId: '' },
to: {
groupId: nextGroupId,
},

View File

@ -6,7 +6,7 @@ export const executeWait = async (
block: WaitBlock,
{ typebot: { variables } }: LogicState
) => {
if (!block.options.secondsToWaitFor) return block.outgoingEdgeId
if (!block.options?.secondsToWaitFor) return block.outgoingEdgeId
const parsedSecondsToWaitFor = parseVariables(variables)(
block.options.secondsToWaitFor
)

View File

@ -1,12 +1,12 @@
import {
Background,
BackgroundType,
ChatTheme,
ContainerColors,
GeneralTheme,
InputColors,
Theme,
} from '@typebot.io/schemas'
import { BackgroundType } from '@typebot.io/schemas/features/typebot/theme/constants'
const cssVariableNames = {
general: {

View File

@ -1,4 +1,4 @@
import { GoogleAnalyticsOptions } from '@typebot.io/schemas'
import { GoogleAnalyticsBlock } from '@typebot.io/schemas'
declare const gtag: (
type: string,
@ -33,7 +33,7 @@ const initGoogleAnalytics = (id: string): Promise<void> =>
if (existingScript) resolve()
})
export const sendGaEvent = (options: GoogleAnalyticsOptions) => {
export const sendGaEvent = (options: GoogleAnalyticsBlock['options']) => {
if (!options) return
gtag('event', options.action, {
event_category: options.category,

View File

@ -133,7 +133,7 @@ export const TypebotProvider = ({
groups: [...localTypebot.groups, ...typebotToInject.groups],
variables: [...localTypebot.variables, ...typebotToInject.variables],
edges: [...localTypebot.edges, ...typebotToInject.edges],
}
} as TypebotViewerProps['typebot']
setLocalTypebot(updatedTypebot)
return typebotToInject
}

View File

@ -1,12 +1,8 @@
import {
BubbleBlock,
BubbleBlockType,
InputBlock,
InputBlockType,
Block,
} from '@typebot.io/schemas'
import { BubbleBlock, InputBlock, Block } from '@typebot.io/schemas'
import { isBubbleBlock, isInputBlock } from '@typebot.io/lib'
import type { TypebotPostMessageData } from 'typebot-js'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
export const getLastChatBlockType = (
blocks: Block[]

View File

@ -4,7 +4,8 @@ import { executeGoogleSheetBlock } from '@/features/blocks/integrations/googleSh
import { executeSendEmailBlock } from '@/features/blocks/integrations/sendEmail'
import { executeWebhook } from '@/features/blocks/integrations/webhook'
import { IntegrationState } from '@/types'
import { IntegrationBlock, IntegrationBlockType } from '@typebot.io/schemas'
import { IntegrationBlock } from '@typebot.io/schemas'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
export const executeIntegration = ({
block,

View File

@ -6,8 +6,9 @@ import { executeTypebotLink } from '@/features/blocks/logic/typebotLink'
import { executeWait } from '@/features/blocks/logic/wait'
import { LinkedTypebot } from '@/providers/TypebotProvider'
import { EdgeId, LogicState } from '@/types'
import { LogicBlock, LogicBlockType } from '@typebot.io/schemas'
import { LogicBlock } from '@typebot.io/schemas'
import { executeScript } from '@/features/blocks/logic/script/executeScript'
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
export const executeLogic = async (
block: LogicBlock,

View File

@ -4,16 +4,16 @@ import { validateUrl } from '@/features/blocks/inputs/url'
import { parseVariables } from '@/features/variables'
import {
BubbleBlock,
BubbleBlockType,
Edge,
EmailInputBlock,
InputBlockType,
PhoneNumberInputBlock,
Block,
UrlInputBlock,
Variable,
} from '@typebot.io/schemas'
import { isInputBlock } from '@typebot.io/lib'
import { isDefined, isInputBlock } from '@typebot.io/lib'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
export const isInputValid = (
inputValue: string,
@ -33,23 +33,25 @@ export const isInputValid = (
export const blockCanBeRetried = (
block: Block
): block is EmailInputBlock | UrlInputBlock | PhoneNumberInputBlock =>
isInputBlock(block) && 'retryMessageContent' in block.options
isInputBlock(block) &&
isDefined(block.options) &&
'retryMessageContent' in block.options
export const parseRetryBlock = (
block: EmailInputBlock | UrlInputBlock | PhoneNumberInputBlock,
groupId: string,
variables: Variable[],
createEdge: (edge: Edge) => void
): BubbleBlock => {
const content = parseVariables(variables)(block.options.retryMessageContent)
const content = parseVariables(variables)(block.options?.retryMessageContent)
const newBlockId = block.id + Math.random() * 1000
const newEdge: Edge = {
id: (Math.random() * 1000).toString(),
from: { blockId: newBlockId, groupId: block.groupId },
to: { groupId: block.groupId, blockId: block.id },
from: { blockId: newBlockId },
to: { groupId, blockId: block.id },
}
createEdge(newEdge)
return {
groupId: block.groupId,
id: newBlockId,
type: BubbleBlockType.TEXT,
content: {

View File

@ -4,7 +4,6 @@ export default defineConfig((options) => ({
entry: ['src/index.ts'],
sourcemap: true,
minify: !options.watch,
dts: true,
format: ['esm', 'cjs'],
loader: {
'.css': 'text',

View File

@ -18,7 +18,7 @@
"jest-environment-jsdom": "29.4.1",
"prettier": "2.8.3",
"ts-jest": "29.0.5",
"typescript": "4.9.4",
"typescript": "5.2.2",
"@typebot.io/tsconfig": "workspace:*"
}
}