♻️ 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

@@ -1,10 +1,11 @@
import { TypingBubble } from '@/components'
import { isMobile } from '@/utils/isMobileSignal'
import type { AudioBubbleContent } from '@typebot.io/schemas'
import { AudioBubbleBlock } from '@typebot.io/schemas'
import { createSignal, onCleanup, onMount } from 'solid-js'
import { defaultAudioBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/audio/constants'
type Props = {
content: AudioBubbleContent
content: AudioBubbleBlock['content']
onTransitionEnd: (offsetTop?: number) => void
}
@@ -50,8 +51,11 @@ export const AudioBubble = (props: Props) => {
</div>
<audio
ref={audioElement}
src={props.content.url}
autoplay={props.content.isAutoplayEnabled ?? true}
src={props.content?.url}
autoplay={
props.content?.isAutoplayEnabled ??
defaultAudioBubbleContent.isAutoplayEnabled
}
class={
'z-10 text-fade-in ' +
(isTyping() ? 'opacity-0' : 'opacity-100 m-2')

View File

@@ -1,11 +1,12 @@
import { TypingBubble } from '@/components'
import { isMobile } from '@/utils/isMobileSignal'
import type { EmbedBubbleContent } from '@typebot.io/schemas'
import { createSignal, onCleanup, onMount } from 'solid-js'
import { clsx } from 'clsx'
import { EmbedBubbleBlock } from '@typebot.io/schemas'
import { defaultEmbedBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/embed/constants'
type Props = {
content: EmbedBubbleContent
content: EmbedBubbleBlock['content']
onTransitionEnd: (offsetTop?: number) => void
}
@@ -53,12 +54,14 @@ export const EmbedBubble = (props: Props) => {
? isMobile()
? '32px'
: '36px'
: `${props.content.height}px`,
: `${
props.content?.height ?? defaultEmbedBubbleContent.height
}px`,
}}
>
<iframe
id="embed-bubble-content"
src={props.content.url}
src={props.content?.url}
class={'w-full h-full '}
/>
</div>

View File

@@ -1,11 +1,12 @@
import { TypingBubble } from '@/components'
import type { ImageBubbleContent } from '@typebot.io/schemas'
import { createSignal, onCleanup, onMount } from 'solid-js'
import { clsx } from 'clsx'
import { isMobile } from '@/utils/isMobileSignal'
import { ImageBubbleBlock } from '@typebot.io/schemas'
import { defaultImageBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/image/constants'
type Props = {
content: ImageBubbleContent
content: ImageBubbleBlock['content']
onTransitionEnd: (offsetTop?: number) => void
}
@@ -44,8 +45,10 @@ export const ImageBubble = (props: Props) => {
const Image = (
<img
ref={image}
src={props.content.url}
alt={props.content.clickLink?.alt ?? 'Bubble image'}
src={props.content?.url}
alt={
props.content?.clickLink?.alt ?? defaultImageBubbleContent.clickLink.alt
}
class={
'text-fade-in w-full ' + (isTyping() ? 'opacity-0' : 'opacity-100')
}
@@ -71,7 +74,7 @@ export const ImageBubble = (props: Props) => {
>
{isTyping() ? <TypingBubble /> : null}
</div>
{props.content.clickLink ? (
{props.content?.clickLink ? (
<a
href={props.content.clickLink.url}
target="_blank"

View File

@@ -1,5 +1,5 @@
import { TypingBubble } from '@/components'
import type { TextBubbleContent, TypingEmulation } from '@typebot.io/schemas'
import type { Settings, TextBubbleBlock } from '@typebot.io/schemas'
import { For, createSignal, onCleanup, onMount } from 'solid-js'
import { PlateElement } from './plate/PlateBlock'
import { computePlainText } from '../helpers/convertRichTextToPlainText'
@@ -8,8 +8,8 @@ import { isMobile } from '@/utils/isMobileSignal'
import { computeTypingDuration } from '@typebot.io/bot-engine/computeTypingDuration'
type Props = {
content: TextBubbleContent
typingEmulation: TypingEmulation
content: TextBubbleBlock['content']
typingEmulation: Settings['typingEmulation']
onTransitionEnd: (offsetTop?: number) => void
}
@@ -31,7 +31,9 @@ export const TextBubble = (props: Props) => {
onMount(() => {
if (!isTyping) return
const plainText = computePlainText(props.content.richText)
const plainText = props.content?.richText
? computePlainText(props.content.richText)
: ''
const typingDuration =
props.typingEmulation?.enabled === false
? 0
@@ -69,7 +71,7 @@ export const TextBubble = (props: Props) => {
height: isTyping() ? (isMobile() ? '16px' : '20px') : '100%',
}}
>
<For each={props.content.richText}>
<For each={props.content?.richText}>
{(element) => <PlateElement element={element} />}
</For>
</div>

View File

@@ -1,12 +1,15 @@
import { TypingBubble } from '@/components'
import { isMobile } from '@/utils/isMobileSignal'
import type { VideoBubbleContent } from '@typebot.io/schemas'
import { VideoBubbleContentType } from '@typebot.io/schemas/features/blocks/bubbles/video/enums'
import { createSignal, Match, onCleanup, onMount, Switch } from 'solid-js'
import { clsx } from 'clsx'
import {
defaultVideoBubbleContent,
VideoBubbleContentType,
} from '@typebot.io/schemas/features/blocks/bubbles/video/constants'
import { VideoBubbleBlock } from '@typebot.io/schemas'
type Props = {
content: VideoBubbleContent
content: VideoBubbleBlock['content']
onTransitionEnd: (offsetTop?: number) => void
}
@@ -60,7 +63,7 @@ export const VideoBubble = (props: Props) => {
>
<video
autoplay
src={props.content.url}
src={props.content?.url}
controls
class={
'p-4 focus:outline-none w-full z-10 text-fade-in rounded-md ' +
@@ -90,15 +93,18 @@ export const VideoBubble = (props: Props) => {
? isMobile()
? '32px'
: '36px'
: `${props.content.height ?? '400'}px`,
: `${
props.content?.height ??
defaultVideoBubbleContent.height
}px`,
}}
>
<iframe
src={`${
props.content.type === VideoBubbleContentType.VIMEO
props.content?.type === VideoBubbleContentType.VIMEO
? 'https://player.vimeo.com/video'
: 'https://www.youtube.com/embed'
}/${props.content.id}`}
}/${props.content?.id}`}
class={'w-full h-full'}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen

View File

@@ -3,8 +3,8 @@ import { SearchInput } from '@/components/inputs/SearchInput'
import { InputSubmitContent } from '@/types'
import { isMobile } from '@/utils/isMobileSignal'
import type { ChoiceInputBlock } from '@typebot.io/schemas'
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice'
import { For, Show, createSignal, onMount } from 'solid-js'
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice/constants'
type Props = {
inputIndex: number
@@ -34,13 +34,13 @@ export const Buttons = (props: Props) => {
return (
<div class="flex flex-col gap-2 w-full">
<Show when={props.options.isSearchable}>
<Show when={props.options?.isSearchable}>
<div class="flex items-end typebot-input w-full">
<SearchInput
ref={inputRef}
onInput={filterItems}
placeholder={
props.options.searchInputPlaceholder ??
props.options?.searchInputPlaceholder ??
defaultChoiceInputOptions.searchInputPlaceholder
}
onClear={() => setFilteredItems(props.defaultItems)}
@@ -51,7 +51,7 @@ export const Buttons = (props: Props) => {
<div
class={
'flex flex-wrap justify-end gap-2' +
(props.options.isSearchable
(props.options?.isSearchable
? ' overflow-y-scroll max-h-80 rounded-md hide-scrollbar'
: '')
}

View File

@@ -5,7 +5,7 @@ import { ChoiceInputBlock } from '@typebot.io/schemas'
import { createSignal, For, onMount, Show } from 'solid-js'
import { Checkbox } from './Checkbox'
import { SearchInput } from '@/components/inputs/SearchInput'
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice'
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice/constants'
type Props = {
inputIndex: number
@@ -59,13 +59,13 @@ export const MultipleChoicesForm = (props: Props) => {
return (
<form class="flex flex-col items-end gap-2 w-full" onSubmit={handleSubmit}>
<Show when={props.options.isSearchable}>
<Show when={props.options?.isSearchable}>
<div class="flex items-end typebot-input w-full">
<SearchInput
ref={inputRef}
onInput={filterItems}
placeholder={
props.options.searchInputPlaceholder ??
props.options?.searchInputPlaceholder ??
defaultChoiceInputOptions.searchInputPlaceholder
}
onClear={() => setFilteredItems(props.defaultItems)}
@@ -75,7 +75,7 @@ export const MultipleChoicesForm = (props: Props) => {
<div
class={
'flex flex-wrap justify-end gap-2' +
(props.options.isSearchable
(props.options?.isSearchable
? ' overflow-y-scroll max-h-80 rounded-md hide-scrollbar'
: '')
}
@@ -144,7 +144,7 @@ export const MultipleChoicesForm = (props: Props) => {
</div>
{selectedItemIds().length > 0 && (
<SendButton disableIcon>
{props.options?.buttonLabel ?? 'Send'}
{props.options?.buttonLabel ?? defaultChoiceInputOptions.buttonLabel}
</SendButton>
)}
</form>

View File

@@ -1,11 +1,12 @@
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import type { DateInputOptions } from '@typebot.io/schemas'
import { DateInputBlock } from '@typebot.io/schemas'
import { createSignal } from 'solid-js'
import { defaultDateInputOptions } from '@typebot.io/schemas/features/blocks/inputs/date/constants'
type Props = {
onSubmit: (inputValue: InputSubmitContent) => void
options?: DateInputOptions
options?: DateInputBlock['options']
defaultValue?: string
}
@@ -38,7 +39,8 @@ export const DateForm = (props: Props) => {
>
{props.options?.isRange && (
<p class="font-semibold">
{props.options.labels?.from ?? 'From:'}
{props.options.labels?.from ??
defaultDateInputOptions.labels.from}
</p>
)}
<input
@@ -65,7 +67,8 @@ export const DateForm = (props: Props) => {
<div class="flex items-center p-4">
{props.options.isRange && (
<p class="font-semibold">
{props.options.labels?.to ?? 'To:'}
{props.options.labels?.to ??
defaultDateInputOptions.labels.to}
</p>
)}
<input
@@ -95,7 +98,8 @@ export const DateForm = (props: Props) => {
isDisabled={inputValues().to === '' && inputValues().from === ''}
class="my-2 ml-2"
>
{props.options?.labels?.button ?? 'Send'}
{props.options?.labels?.button ??
defaultDateInputOptions.labels.button}
</SendButton>
</form>
</div>

View File

@@ -5,6 +5,7 @@ import { InputSubmitContent } from '@/types'
import { isMobile } from '@/utils/isMobileSignal'
import type { EmailInputBlock } from '@typebot.io/schemas'
import { createSignal, onCleanup, onMount } from 'solid-js'
import { defaultEmailInputOptions } from '@typebot.io/schemas/features/blocks/inputs/email/constants'
type Props = {
block: EmailInputBlock
@@ -57,7 +58,8 @@ export const EmailInput = (props: Props) => {
ref={inputRef}
value={inputValue()}
placeholder={
props.block.options?.labels?.placeholder ?? 'Type your email...'
props.block.options?.labels?.placeholder ??
defaultEmailInputOptions.labels.placeholder
}
onInput={handleInput}
type="email"
@@ -69,7 +71,8 @@ export const EmailInput = (props: Props) => {
class="my-2 ml-2"
on:click={submit}
>
{props.block.options?.labels?.button ?? 'Send'}
{props.block.options?.labels?.button ??
defaultEmailInputOptions.labels.button}
</SendButton>
</div>
)

View File

@@ -1,13 +1,13 @@
import { SendButton } from '@/components/SendButton'
import { BotContext, InputSubmitContent } from '@/types'
import { FileInputBlock } from '@typebot.io/schemas'
import { defaultFileInputOptions } from '@typebot.io/schemas/features/blocks/inputs/file'
import { createSignal, Match, Show, Switch } from 'solid-js'
import { Button } from '@/components/Button'
import { Spinner } from '@/components/Spinner'
import { uploadFiles } from '../helpers/uploadFiles'
import { guessApiHost } from '@/utils/guessApiHost'
import { getRuntimeVariable } from '@typebot.io/env/getRuntimeVariable'
import { defaultFileInputOptions } from '@typebot.io/schemas/features/blocks/inputs/file/constants'
type Props = {
context: BotContext
@@ -27,14 +27,16 @@ export const FileUploadForm = (props: Props) => {
setErrorMessage(undefined)
const newFiles = Array.from(files)
const sizeLimit =
props.block.options.sizeLimit ??
getRuntimeVariable('NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE')
props.block.options && 'sizeLimit' in props.block.options
? props.block.options?.sizeLimit ??
getRuntimeVariable('NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE')
: undefined
if (
sizeLimit &&
newFiles.some((file) => file.size > sizeLimit * 1024 * 1024)
)
return setErrorMessage(`A file is larger than ${sizeLimit}MB`)
if (!props.block.options.isMultipleAllowed && files)
if (!props.block.options?.isMultipleAllowed && files)
return startSingleFileUpload(newFiles[0])
setSelectedFiles([...selectedFiles(), ...newFiles])
}
@@ -118,7 +120,7 @@ export const FileUploadForm = (props: Props) => {
const skip = () =>
props.onSkip(
props.block.options.labels.skip ?? defaultFileInputOptions.labels.skip
props.block.options?.labels?.skip ?? defaultFileInputOptions.labels.skip
)
return (
@@ -165,14 +167,20 @@ export const FileUploadForm = (props: Props) => {
</Show>
<p
class="text-sm text-gray-500 text-center"
innerHTML={props.block.options.labels.placeholder}
innerHTML={
props.block.options?.labels?.placeholder ??
defaultFileInputOptions.labels.placeholder
}
/>
</div>
<input
id="dropzone-file"
type="file"
class="hidden"
multiple={props.block.options.isMultipleAllowed}
multiple={
props.block.options?.isMultipleAllowed ??
defaultFileInputOptions.isMultipleAllowed
}
onChange={(e) => {
if (!e.currentTarget.files) return
onNewFiles(e.currentTarget.files)
@@ -185,19 +193,19 @@ export const FileUploadForm = (props: Props) => {
<Show
when={
selectedFiles().length === 0 &&
props.block.options.isRequired === false
props.block.options?.isRequired === false
}
>
<div class="flex justify-end">
<Button on:click={skip}>
{props.block.options.labels.skip ??
{props.block.options?.labels?.skip ??
defaultFileInputOptions.labels.skip}
</Button>
</div>
</Show>
<Show
when={
props.block.options.isMultipleAllowed &&
props.block.options?.isMultipleAllowed &&
selectedFiles().length > 0 &&
!isUploading()
}
@@ -206,17 +214,17 @@ export const FileUploadForm = (props: Props) => {
<div class="flex gap-2">
<Show when={selectedFiles().length}>
<Button variant="secondary" on:click={clearFiles}>
{props.block.options.labels.clear ??
{props.block.options?.labels?.clear ??
defaultFileInputOptions.labels.clear}
</Button>
</Show>
<SendButton type="submit" disableIcon>
{props.block.options.labels.button ===
{props.block.options?.labels?.button ===
defaultFileInputOptions.labels.button
? `Upload ${selectedFiles().length} file${
selectedFiles().length > 1 ? 's' : ''
}`
: props.block.options.labels.button}
: props.block.options?.labels?.button}
</SendButton>
</div>
</div>

View File

@@ -5,6 +5,7 @@ import { isMobile } from '@/utils/isMobileSignal'
import type { NumberInputBlock } from '@typebot.io/schemas'
import { createSignal, onCleanup, onMount } from 'solid-js'
import { numberInputHelper } from '../numberInputHelper'
import { defaultNumberInputOptions } from '@typebot.io/schemas/features/blocks/inputs/number/constants'
type NumberInputProps = {
block: NumberInputBlock
@@ -67,7 +68,8 @@ export const NumberInput = (props: NumberInputProps) => {
// eslint-disable-next-line solid/jsx-no-undef
use:bindValue
placeholder={
props.block.options?.labels?.placeholder ?? 'Type your answer...'
props.block.options?.labels?.placeholder ??
defaultNumberInputOptions.labels.placeholder
}
onInput={(e) => {
setInputValue(targetValue(e.currentTarget))
@@ -83,7 +85,8 @@ export const NumberInput = (props: NumberInputProps) => {
class="my-2 ml-2"
on:click={submit}
>
{props.block.options?.labels?.button ?? 'Send'}
{props.block.options?.labels?.button ??
defaultNumberInputOptions.labels.button}
</SendButton>
</div>
)

View File

@@ -1,23 +1,17 @@
import { BotContext } from '@/types'
import type { PaymentInputOptions, RuntimeOptions } from '@typebot.io/schemas'
import { PaymentProvider } from '@typebot.io/schemas/features/blocks/inputs/payment/enums'
import { Match, Switch } from 'solid-js'
import { StripePaymentForm } from './StripePaymentForm'
import { PaymentInputBlock, RuntimeOptions } from '@typebot.io/schemas'
type Props = {
context: BotContext
options: PaymentInputOptions & RuntimeOptions
options: PaymentInputBlock['options'] & RuntimeOptions
onSuccess: () => void
}
export const PaymentForm = (props: Props) => (
<Switch>
<Match when={props.options.provider === PaymentProvider.STRIPE}>
<StripePaymentForm
onSuccess={props.onSuccess}
options={props.options}
context={props.context}
/>
</Match>
</Switch>
<StripePaymentForm
onSuccess={props.onSuccess}
options={props.options}
context={props.context}
/>
)

View File

@@ -2,16 +2,17 @@ import { SendButton } from '@/components/SendButton'
import { createSignal, onMount, Show } from 'solid-js'
import type { Stripe, StripeElements } from '@stripe/stripe-js'
import { BotContext } from '@/types'
import type { PaymentInputOptions, RuntimeOptions } from '@typebot.io/schemas'
import type { PaymentInputBlock, RuntimeOptions } from '@typebot.io/schemas'
import { loadStripe } from '@/lib/stripe'
import {
removePaymentInProgressFromStorage,
setPaymentInProgressInStorage,
} from '../helpers/paymentInProgressStorage'
import { defaultPaymentInputOptions } from '@typebot.io/schemas/features/blocks/inputs/payment/constants'
type Props = {
context: BotContext
options: PaymentInputOptions & RuntimeOptions
options: PaymentInputBlock['options'] & RuntimeOptions
onSuccess: () => void
}
@@ -28,7 +29,9 @@ export const StripePaymentForm = (props: Props) => {
onMount(async () => {
initShadowMountPoint(paymentElementSlot)
stripe = await loadStripe(props.options.publicKey)
if (!props.options?.publicKey)
return setMessage('Missing Stripe public key')
stripe = await loadStripe(props.options?.publicKey)
if (!stripe) return
elements = stripe.elements({
appearance: {
@@ -60,16 +63,16 @@ export const StripePaymentForm = (props: Props) => {
typebot: props.context.typebot,
})
const { postalCode, ...address } =
props.options.additionalInformation?.address ?? {}
props.options?.additionalInformation?.address ?? {}
const { error, paymentIntent } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: window.location.href,
payment_method_data: {
billing_details: {
name: props.options.additionalInformation?.name,
email: props.options.additionalInformation?.email,
phone: props.options.additionalInformation?.phoneNumber,
name: props.options?.additionalInformation?.name,
email: props.options?.additionalInformation?.email,
phone: props.options?.additionalInformation?.phoneNumber,
address: {
...address,
postal_code: postalCode,
@@ -100,7 +103,9 @@ export const StripePaymentForm = (props: Props) => {
class="mt-4 w-full max-w-lg animate-fade-in"
disableIcon
>
{props.options.labels.button} {props.options.amountLabel}
{props.options?.labels?.button ??
defaultPaymentInputOptions.labels.button}{' '}
{props.options?.amountLabel}
</SendButton>
</Show>

View File

@@ -3,14 +3,15 @@ import { ChevronDownIcon } from '@/components/icons/ChevronDownIcon'
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import { isMobile } from '@/utils/isMobileSignal'
import type { PhoneNumberInputOptions } from '@typebot.io/schemas'
import { createSignal, For, onCleanup, onMount } from 'solid-js'
import { isEmpty } from '@typebot.io/lib'
import { phoneCountries } from '@typebot.io/lib/phoneCountries'
import { CommandData } from '@/features/commands/types'
import { PhoneNumberInputBlock } from '@typebot.io/schemas'
import { defaultPhoneInputOptions } from '@typebot.io/schemas/features/blocks/inputs/phone/constants'
type PhoneInputProps = Pick<
PhoneNumberInputOptions,
NonNullable<PhoneNumberInputBlock['options']>,
'labels' | 'defaultCountryCode'
> & {
defaultValue?: string
@@ -146,7 +147,10 @@ export const PhoneInput = (props: PhoneInputProps) => {
ref={inputRef}
value={inputValue()}
onInput={handleInput}
placeholder={props.labels.placeholder ?? 'Your phone number...'}
placeholder={
props.labels?.placeholder ??
defaultPhoneInputOptions.labels.placeholder
}
autofocus={!isMobile()}
/>
</div>
@@ -157,7 +161,7 @@ export const PhoneInput = (props: PhoneInputProps) => {
class="my-2 ml-2"
on:click={submit}
>
{props.labels?.button ?? 'Send'}
{props.labels?.button ?? defaultPhoneInputOptions.labels.button}
</SendButton>
</div>
)

View File

@@ -1,14 +1,12 @@
import { InputSubmitContent } from '@/types'
import {
PictureChoiceBlock,
defaultPictureChoiceOptions,
} from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
import { PictureChoiceBlock } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
import { For, Show, createSignal, onMount } from 'solid-js'
import { Checkbox } from '../buttons/components/Checkbox'
import { SendButton } from '@/components'
import { isDefined, isEmpty, isSvgSrc } from '@typebot.io/lib'
import { SearchInput } from '@/components/inputs/SearchInput'
import { isMobile } from '@/utils/isMobileSignal'
import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice/constants'
type Props = {
defaultItems: PictureChoiceBlock['items']
@@ -68,13 +66,13 @@ export const MultiplePictureChoice = (props: Props) => {
return (
<form class="flex flex-col gap-2 w-full items-end" onSubmit={handleSubmit}>
<Show when={props.options.isSearchable}>
<Show when={props.options?.isSearchable}>
<div class="flex items-end typebot-input w-full">
<SearchInput
ref={inputRef}
onInput={filterItems}
placeholder={
props.options.searchInputPlaceholder ??
props.options?.searchInputPlaceholder ??
defaultPictureChoiceOptions.searchInputPlaceholder
}
onClear={() => setFilteredItems(props.defaultItems)}
@@ -84,7 +82,7 @@ export const MultiplePictureChoice = (props: Props) => {
<div
class={
'flex flex-wrap justify-end gap-2' +
(props.options.isSearchable
(props.options?.isSearchable
? ' overflow-y-scroll max-h-[464px] rounded-md hide-scrollbar'
: '')
}

View File

@@ -43,12 +43,12 @@ export const SinglePictureChoice = (props: Props) => {
return (
<div class="flex flex-col gap-2 w-full">
<Show when={props.options.isSearchable}>
<Show when={props.options?.isSearchable}>
<div class="flex items-end typebot-input w-full">
<SearchInput
ref={inputRef}
onInput={filterItems}
placeholder={props.options.searchInputPlaceholder ?? ''}
placeholder={props.options?.searchInputPlaceholder ?? ''}
onClear={() => setFilteredItems(props.defaultItems)}
/>
</div>
@@ -56,7 +56,7 @@ export const SinglePictureChoice = (props: Props) => {
<div
class={
'gap-2 flex flex-wrap justify-end' +
(props.options.isSearchable
(props.options?.isSearchable
? ' overflow-y-scroll max-h-[464px] rounded-md hide-scrollbar'
: '')
}

View File

@@ -1,9 +1,10 @@
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import type { RatingInputBlock, RatingInputOptions } from '@typebot.io/schemas'
import type { RatingInputBlock } from '@typebot.io/schemas'
import { createSignal, For, Match, Switch } from 'solid-js'
import { isDefined, isEmpty, isNotDefined } from '@typebot.io/lib'
import { Button } from '@/components/Button'
import { defaultRatingInputOptions } from '@typebot.io/schemas/features/blocks/inputs/rating/constants'
type Props = {
block: RatingInputBlock
@@ -24,14 +25,14 @@ export const RatingForm = (props: Props) => {
}
const handleClick = (rating: number) => {
if (props.block.options.isOneClickSubmitEnabled)
if (props.block.options?.isOneClickSubmitEnabled)
props.onSubmit({ value: rating.toString() })
setRating(rating)
}
return (
<form class="flex flex-col gap-2" onSubmit={handleSubmit}>
{props.block.options.labels.left && (
{props.block.options?.labels?.left && (
<span class="text-sm w-full rating-label">
{props.block.options.labels.left}
</span>
@@ -40,8 +41,12 @@ export const RatingForm = (props: Props) => {
<For
each={Array.from(
Array(
props.block.options.length +
(props.block.options.buttonType === 'Numbers' ? 1 : 0)
(props.block.options?.length ??
defaultRatingInputOptions.length) +
((props.block.options?.buttonType ??
defaultRatingInputOptions.buttonType) === 'Numbers'
? 1
: 0)
)
)}
>
@@ -50,14 +55,18 @@ export const RatingForm = (props: Props) => {
{...props.block.options}
rating={rating()}
idx={
idx() + (props.block.options.buttonType === 'Numbers' ? 0 : 1)
idx() +
((props.block.options?.buttonType ??
defaultRatingInputOptions.buttonType) === 'Numbers'
? 0
: 1)
}
onClick={handleClick}
/>
)}
</For>
</div>
{props.block.options.labels.right && (
{props.block.options?.labels?.right && (
<span class="text-sm w-full text-right pr-2 rating-label">
{props.block.options.labels.right}
</span>
@@ -66,7 +75,8 @@ export const RatingForm = (props: Props) => {
<div class="flex justify-end">
{isDefined(rating()) && (
<SendButton disableIcon>
{props.block.options?.labels?.button ?? 'Send'}
{props.block.options?.labels?.button ??
defaultRatingInputOptions.labels.button}
</SendButton>
)}
</div>
@@ -78,7 +88,7 @@ type RatingButtonProps = {
rating?: number
idx: number
onClick: (rating: number) => void
} & RatingInputOptions
} & RatingInputBlock['options']
const RatingButton = (props: RatingButtonProps) => {
const handleClick = (e: MouseEvent) => {
@@ -87,7 +97,12 @@ const RatingButton = (props: RatingButtonProps) => {
}
return (
<Switch>
<Match when={props.buttonType === 'Numbers'}>
<Match
when={
(props.buttonType ?? defaultRatingInputOptions.buttonType) ===
'Numbers'
}
>
<Button
on:click={handleClick}
class={
@@ -100,7 +115,12 @@ const RatingButton = (props: RatingButtonProps) => {
{props.idx}
</Button>
</Match>
<Match when={props.buttonType !== 'Numbers'}>
<Match
when={
(props.buttonType ?? defaultRatingInputOptions.buttonType) !==
'Numbers'
}
>
<div
class={
'flex justify-center items-center rating-icon-container cursor-pointer ' +
@@ -109,7 +129,7 @@ const RatingButton = (props: RatingButtonProps) => {
: '')
}
innerHTML={
props.customIcon.isEnabled && !isEmpty(props.customIcon.svg)
props.customIcon?.isEnabled && !isEmpty(props.customIcon.svg)
? props.customIcon.svg
: defaultIcon
}

View File

@@ -5,6 +5,7 @@ import { InputSubmitContent } from '@/types'
import { isMobile } from '@/utils/isMobileSignal'
import type { TextInputBlock } from '@typebot.io/schemas'
import { createSignal, onCleanup, onMount } from 'solid-js'
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
type Props = {
block: TextInputBlock
@@ -26,12 +27,12 @@ export const TextInput = (props: Props) => {
}
const submitWhenEnter = (e: KeyboardEvent) => {
if (props.block.options.isLong) return
if (props.block.options?.isLong) return
if (e.key === 'Enter') submit()
}
const submitIfCtrlEnter = (e: KeyboardEvent) => {
if (!props.block.options.isLong) return
if (!props.block.options?.isLong) return
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) submit()
}
@@ -55,18 +56,19 @@ export const TextInput = (props: Props) => {
class={'flex items-end justify-between pr-2 typebot-input w-full'}
data-testid="input"
style={{
'max-width': props.block.options.isLong ? undefined : '350px',
'max-width': props.block.options?.isLong ? undefined : '350px',
}}
onKeyDown={submitWhenEnter}
>
{props.block.options.isLong ? (
{props.block.options?.isLong ? (
<Textarea
ref={inputRef as HTMLTextAreaElement}
onInput={handleInput}
onKeyDown={submitIfCtrlEnter}
value={inputValue()}
placeholder={
props.block.options?.labels?.placeholder ?? 'Type your answer...'
props.block.options?.labels?.placeholder ??
defaultTextInputOptions.labels.placeholder
}
/>
) : (
@@ -75,7 +77,8 @@ export const TextInput = (props: Props) => {
onInput={handleInput}
value={inputValue()}
placeholder={
props.block.options?.labels?.placeholder ?? 'Type your answer...'
props.block.options?.labels?.placeholder ??
defaultTextInputOptions.labels.placeholder
}
/>
)}
@@ -85,7 +88,8 @@ export const TextInput = (props: Props) => {
class="my-2 ml-2"
on:click={submit}
>
{props.block.options?.labels?.button ?? 'Send'}
{props.block.options?.labels?.button ??
defaultTextInputOptions.labels.button}
</SendButton>
</div>
)

View File

@@ -5,6 +5,7 @@ import { InputSubmitContent } from '@/types'
import { isMobile } from '@/utils/isMobileSignal'
import type { UrlInputBlock } from '@typebot.io/schemas'
import { createSignal, onCleanup, onMount } from 'solid-js'
import { defaultUrlInputOptions } from '@typebot.io/schemas/features/blocks/inputs/url/constants'
type Props = {
block: UrlInputBlock
@@ -63,7 +64,8 @@ export const UrlInput = (props: Props) => {
ref={inputRef as HTMLInputElement}
value={inputValue()}
placeholder={
props.block.options?.labels?.placeholder ?? 'Type your URL...'
props.block.options?.labels?.placeholder ??
defaultUrlInputOptions.labels.placeholder
}
onInput={handleInput}
type="url"
@@ -75,7 +77,8 @@ export const UrlInput = (props: Props) => {
class="my-2 ml-2"
on:click={submit}
>
{props.block.options?.labels?.button ?? 'Send'}
{props.block.options?.labels?.button ??
defaultUrlInputOptions.labels.button}
</SendButton>
</div>
)

View File

@@ -1,8 +1,8 @@
import { sendGaEvent } from '@/lib/gtag'
import type { GoogleAnalyticsOptions } from '@typebot.io/schemas'
import { GoogleAnalyticsBlock } from '@typebot.io/schemas'
export const executeGoogleAnalyticsBlock = async (
options: GoogleAnalyticsOptions
options: GoogleAnalyticsBlock['options']
) => {
if (!options?.trackingId) return
sendGaEvent(options)

View File

@@ -1,9 +1,9 @@
import type { RedirectOptions } from '@typebot.io/schemas'
import { RedirectBlock } from '@typebot.io/schemas'
export const executeRedirect = ({
url,
isNewTab,
}: RedirectOptions): { blockedPopupUrl: string } | undefined => {
}: RedirectBlock['options'] = {}): { blockedPopupUrl: string } | undefined => {
if (!url) return
const updatedWindow = window.open(url, isNewTab ? '_blank' : '_self')
if (!updatedWindow)