2
0

refactor(♻️ Add defaults everywhere (+ settings page)):

This commit is contained in:
Baptiste Arnaud
2022-01-25 18:19:37 +01:00
parent 21448bcc8a
commit c5aaa323d1
115 changed files with 1436 additions and 720 deletions

View File

@ -8,10 +8,11 @@ import { ImageUploadContent } from 'components/shared/ImageUploadContent'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { import {
BubbleStep, BubbleStep,
BubbleStepContent,
BubbleStepType, BubbleStepType,
ImageBubbleStep, ImageBubbleStep,
TextBubbleStep, TextBubbleStep,
VideoBubbleContent,
VideoBubbleStep,
} from 'models' } from 'models'
import { useRef } from 'react' import { useRef } from 'react'
import { VideoUploadContent } from './VideoUploadContent' import { VideoUploadContent } from './VideoUploadContent'
@ -39,14 +40,17 @@ export const ContentPopover = ({ step }: Props) => {
export const StepContent = ({ step }: Props) => { export const StepContent = ({ step }: Props) => {
const { updateStep } = useTypebot() const { updateStep } = useTypebot()
const handleContentChange = (content: BubbleStepContent) => const handleContentChange = (url: string) =>
updateStep(step.id, { content } as Partial<ImageBubbleStep>) updateStep(step.id, { content: { url } } as Partial<ImageBubbleStep>)
const handleVideoContentChange = (content: VideoBubbleContent) =>
updateStep(step.id, { content } as Partial<VideoBubbleStep>)
switch (step.type) { switch (step.type) {
case BubbleStepType.IMAGE: { case BubbleStepType.IMAGE: {
return ( return (
<ImageUploadContent <ImageUploadContent
content={step.content} url={step.content?.url}
onSubmit={handleContentChange} onSubmit={handleContentChange}
/> />
) )
@ -55,7 +59,7 @@ export const StepContent = ({ step }: Props) => {
return ( return (
<VideoUploadContent <VideoUploadContent
content={step.content} content={step.content}
onSubmit={handleContentChange} onSubmit={handleVideoContentChange}
/> />
) )
} }

View File

@ -1,5 +1,5 @@
import { Stack, Text } from '@chakra-ui/react' import { Stack, Text } from '@chakra-ui/react'
import { InputWithVariableButton } from 'components/shared/InputWithVariableButton' import { InputWithVariableButton } from 'components/shared/TextboxWithVariableButton/InputWithVariableButton'
import { VideoBubbleContent, VideoBubbleContentType } from 'models' import { VideoBubbleContent, VideoBubbleContentType } from 'models'
import urlParser from 'js-video-url-parser/lib/base' import urlParser from 'js-video-url-parser/lib/base'
import 'js-video-url-parser/lib/provider/vimeo' import 'js-video-url-parser/lib/provider/vimeo'

View File

@ -1,6 +1,6 @@
import { Stack } from '@chakra-ui/react' import { Stack } from '@chakra-ui/react'
import { DropdownList } from 'components/shared/DropdownList' import { DropdownList } from 'components/shared/DropdownList'
import { InputWithVariableButton } from 'components/shared/InputWithVariableButton' import { InputWithVariableButton } from 'components/shared/TextboxWithVariableButton/InputWithVariableButton'
import { TableListItemProps } from 'components/shared/TableList' import { TableListItemProps } from 'components/shared/TableList'
import { VariableSearchInput } from 'components/shared/VariableSearchInput' import { VariableSearchInput } from 'components/shared/VariableSearchInput'
import { Comparison, Variable, ComparisonOperators } from 'models' import { Comparison, Variable, ComparisonOperators } from 'models'

View File

@ -6,7 +6,7 @@ import { DateInputOptions, Variable } from 'models'
import React from 'react' import React from 'react'
type DateInputSettingsBodyProps = { type DateInputSettingsBodyProps = {
options?: DateInputOptions options: DateInputOptions
onOptionsChange: (options: DateInputOptions) => void onOptionsChange: (options: DateInputOptions) => void
} }
@ -32,23 +32,23 @@ export const DateInputSettingsBody = ({
<SwitchWithLabel <SwitchWithLabel
id="is-range" id="is-range"
label={'Is range?'} label={'Is range?'}
initialValue={options?.isRange ?? false} initialValue={options.isRange}
onCheckChange={handleIsRangeChange} onCheckChange={handleIsRangeChange}
/> />
<SwitchWithLabel <SwitchWithLabel
id="with-time" id="with-time"
label={'With time?'} label={'With time?'}
initialValue={options?.isRange ?? false} initialValue={options.isRange}
onCheckChange={handleHasTimeChange} onCheckChange={handleHasTimeChange}
/> />
{options?.isRange && ( {options.isRange && (
<Stack> <Stack>
<FormLabel mb="0" htmlFor="from"> <FormLabel mb="0" htmlFor="from">
From label: From label:
</FormLabel> </FormLabel>
<DebouncedInput <DebouncedInput
id="from" id="from"
initialValue={options?.labels?.from ?? 'From:'} initialValue={options.labels.from}
delay={100} delay={100}
onChange={handleFromChange} onChange={handleFromChange}
/> />
@ -61,7 +61,7 @@ export const DateInputSettingsBody = ({
</FormLabel> </FormLabel>
<DebouncedInput <DebouncedInput
id="to" id="to"
initialValue={options?.labels?.to ?? 'To:'} initialValue={options.labels.to}
delay={100} delay={100}
onChange={handleToChange} onChange={handleToChange}
/> />
@ -73,7 +73,7 @@ export const DateInputSettingsBody = ({
</FormLabel> </FormLabel>
<DebouncedInput <DebouncedInput
id="button" id="button"
initialValue={options?.labels?.button ?? 'Send'} initialValue={options.labels.button}
delay={100} delay={100}
onChange={handleButtonLabelChange} onChange={handleButtonLabelChange}
/> />
@ -83,7 +83,7 @@ export const DateInputSettingsBody = ({
Save answer in a variable: Save answer in a variable:
</FormLabel> </FormLabel>
<VariableSearchInput <VariableSearchInput
initialVariableId={options?.variableId} initialVariableId={options.variableId}
onSelectVariable={handleVariableChange} onSelectVariable={handleVariableChange}
/> />
</Stack> </Stack>

View File

@ -5,7 +5,7 @@ import { EmailInputOptions, Variable } from 'models'
import React from 'react' import React from 'react'
type EmailInputSettingsBodyProps = { type EmailInputSettingsBodyProps = {
options?: EmailInputOptions options: EmailInputOptions
onOptionsChange: (options: EmailInputOptions) => void onOptionsChange: (options: EmailInputOptions) => void
} }
@ -14,9 +14,9 @@ export const EmailInputSettingsBody = ({
onOptionsChange, onOptionsChange,
}: EmailInputSettingsBodyProps) => { }: EmailInputSettingsBodyProps) => {
const handlePlaceholderChange = (placeholder: string) => const handlePlaceholderChange = (placeholder: string) =>
onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } }) onOptionsChange({ ...options, labels: { ...options.labels, placeholder } })
const handleButtonLabelChange = (button: string) => const handleButtonLabelChange = (button: string) =>
onOptionsChange({ ...options, labels: { ...options?.labels, button } }) onOptionsChange({ ...options, labels: { ...options.labels, button } })
const handleVariableChange = (variable?: Variable) => const handleVariableChange = (variable?: Variable) =>
onOptionsChange({ ...options, variableId: variable?.id }) onOptionsChange({ ...options, variableId: variable?.id })
@ -28,7 +28,7 @@ export const EmailInputSettingsBody = ({
</FormLabel> </FormLabel>
<DebouncedInput <DebouncedInput
id="placeholder" id="placeholder"
initialValue={options?.labels?.placeholder ?? 'Type your email...'} initialValue={options.labels.placeholder}
delay={100} delay={100}
onChange={handlePlaceholderChange} onChange={handlePlaceholderChange}
/> />
@ -39,7 +39,7 @@ export const EmailInputSettingsBody = ({
</FormLabel> </FormLabel>
<DebouncedInput <DebouncedInput
id="button" id="button"
initialValue={options?.labels?.button ?? 'Send'} initialValue={options.labels.button}
delay={100} delay={100}
onChange={handleButtonLabelChange} onChange={handleButtonLabelChange}
/> />
@ -49,7 +49,7 @@ export const EmailInputSettingsBody = ({
Save answer in a variable: Save answer in a variable:
</FormLabel> </FormLabel>
<VariableSearchInput <VariableSearchInput
initialVariableId={options?.variableId} initialVariableId={options.variableId}
onSelectVariable={handleVariableChange} onSelectVariable={handleVariableChange}
/> />
</Stack> </Stack>

View File

@ -10,7 +10,7 @@ import {
Tag, Tag,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { DebouncedInput } from 'components/shared/DebouncedInput' import { DebouncedInput } from 'components/shared/DebouncedInput'
import { InputWithVariableButton } from 'components/shared/InputWithVariableButton' import { InputWithVariableButton } from 'components/shared/TextboxWithVariableButton/InputWithVariableButton'
import { GoogleAnalyticsOptions } from 'models' import { GoogleAnalyticsOptions } from 'models'
import React from 'react' import React from 'react'

View File

@ -1,6 +1,6 @@
import { Stack } from '@chakra-ui/react' import { Stack } from '@chakra-ui/react'
import { DropdownList } from 'components/shared/DropdownList' import { DropdownList } from 'components/shared/DropdownList'
import { InputWithVariableButton } from 'components/shared/InputWithVariableButton' import { InputWithVariableButton } from 'components/shared/TextboxWithVariableButton/InputWithVariableButton'
import { TableListItemProps } from 'components/shared/TableList' import { TableListItemProps } from 'components/shared/TableList'
import { Cell } from 'models' import { Cell } from 'models'

View File

@ -1,5 +1,5 @@
import { FormLabel, HStack, Stack } from '@chakra-ui/react' import { FormLabel, HStack, Stack } from '@chakra-ui/react'
import { SmartNumberInput } from 'components/settings/SmartNumberInput' import { SmartNumberInput } from 'components/shared/SmartNumberInput'
import { DebouncedInput } from 'components/shared/DebouncedInput' import { DebouncedInput } from 'components/shared/DebouncedInput'
import { VariableSearchInput } from 'components/shared/VariableSearchInput' import { VariableSearchInput } from 'components/shared/VariableSearchInput'
import { NumberInputOptions, Variable } from 'models' import { NumberInputOptions, Variable } from 'models'
@ -7,7 +7,7 @@ import React from 'react'
import { removeUndefinedFields } from 'services/utils' import { removeUndefinedFields } from 'services/utils'
type NumberInputSettingsBodyProps = { type NumberInputSettingsBodyProps = {
options?: NumberInputOptions options: NumberInputOptions
onOptionsChange: (options: NumberInputOptions) => void onOptionsChange: (options: NumberInputOptions) => void
} }
@ -16,9 +16,9 @@ export const NumberInputSettingsBody = ({
onOptionsChange, onOptionsChange,
}: NumberInputSettingsBodyProps) => { }: NumberInputSettingsBodyProps) => {
const handlePlaceholderChange = (placeholder: string) => const handlePlaceholderChange = (placeholder: string) =>
onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } }) onOptionsChange({ ...options, labels: { ...options.labels, placeholder } })
const handleButtonLabelChange = (button: string) => const handleButtonLabelChange = (button: string) =>
onOptionsChange({ ...options, labels: { ...options?.labels, button } }) onOptionsChange({ ...options, labels: { ...options.labels, button } })
const handleMinChange = (min?: number) => const handleMinChange = (min?: number) =>
onOptionsChange(removeUndefinedFields({ ...options, min })) onOptionsChange(removeUndefinedFields({ ...options, min }))
const handleMaxChange = (max?: number) => const handleMaxChange = (max?: number) =>
@ -36,7 +36,7 @@ export const NumberInputSettingsBody = ({
</FormLabel> </FormLabel>
<DebouncedInput <DebouncedInput
id="placeholder" id="placeholder"
initialValue={options?.labels?.placeholder ?? 'Type your answer...'} initialValue={options.labels.placeholder}
delay={100} delay={100}
onChange={handlePlaceholderChange} onChange={handlePlaceholderChange}
/> />
@ -58,7 +58,7 @@ export const NumberInputSettingsBody = ({
</FormLabel> </FormLabel>
<SmartNumberInput <SmartNumberInput
id="min" id="min"
initialValue={options?.min} value={options.min}
onValueChange={handleMinChange} onValueChange={handleMinChange}
/> />
</HStack> </HStack>
@ -68,7 +68,7 @@ export const NumberInputSettingsBody = ({
</FormLabel> </FormLabel>
<SmartNumberInput <SmartNumberInput
id="max" id="max"
initialValue={options?.max} value={options.max}
onValueChange={handleMaxChange} onValueChange={handleMaxChange}
/> />
</HStack> </HStack>
@ -78,7 +78,7 @@ export const NumberInputSettingsBody = ({
</FormLabel> </FormLabel>
<SmartNumberInput <SmartNumberInput
id="step" id="step"
initialValue={options?.step} value={options.step}
onValueChange={handleStepChange} onValueChange={handleStepChange}
/> />
</HStack> </HStack>
@ -87,7 +87,7 @@ export const NumberInputSettingsBody = ({
Save answer in a variable: Save answer in a variable:
</FormLabel> </FormLabel>
<VariableSearchInput <VariableSearchInput
initialVariableId={options?.variableId} initialVariableId={options.variableId}
onSelectVariable={handleVariableChange} onSelectVariable={handleVariableChange}
/> />
</Stack> </Stack>

View File

@ -5,7 +5,7 @@ import { EmailInputOptions, Variable } from 'models'
import React from 'react' import React from 'react'
type PhoneNumberSettingsBodyProps = { type PhoneNumberSettingsBodyProps = {
options?: EmailInputOptions options: EmailInputOptions
onOptionsChange: (options: EmailInputOptions) => void onOptionsChange: (options: EmailInputOptions) => void
} }
@ -14,9 +14,9 @@ export const PhoneNumberSettingsBody = ({
onOptionsChange, onOptionsChange,
}: PhoneNumberSettingsBodyProps) => { }: PhoneNumberSettingsBodyProps) => {
const handlePlaceholderChange = (placeholder: string) => const handlePlaceholderChange = (placeholder: string) =>
onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } }) onOptionsChange({ ...options, labels: { ...options.labels, placeholder } })
const handleButtonLabelChange = (button: string) => const handleButtonLabelChange = (button: string) =>
onOptionsChange({ ...options, labels: { ...options?.labels, button } }) onOptionsChange({ ...options, labels: { ...options.labels, button } })
const handleVariableChange = (variable?: Variable) => const handleVariableChange = (variable?: Variable) =>
onOptionsChange({ ...options, variableId: variable?.id }) onOptionsChange({ ...options, variableId: variable?.id })
@ -28,7 +28,7 @@ export const PhoneNumberSettingsBody = ({
</FormLabel> </FormLabel>
<DebouncedInput <DebouncedInput
id="placeholder" id="placeholder"
initialValue={options?.labels?.placeholder ?? 'Your phone number...'} initialValue={options.labels.placeholder}
delay={100} delay={100}
onChange={handlePlaceholderChange} onChange={handlePlaceholderChange}
/> />
@ -39,7 +39,7 @@ export const PhoneNumberSettingsBody = ({
</FormLabel> </FormLabel>
<DebouncedInput <DebouncedInput
id="button" id="button"
initialValue={options?.labels?.button ?? 'Send'} initialValue={options.labels.button}
delay={100} delay={100}
onChange={handleButtonLabelChange} onChange={handleButtonLabelChange}
/> />
@ -49,7 +49,7 @@ export const PhoneNumberSettingsBody = ({
Save answer in a variable: Save answer in a variable:
</FormLabel> </FormLabel>
<VariableSearchInput <VariableSearchInput
initialVariableId={options?.variableId} initialVariableId={options.variableId}
onSelectVariable={handleVariableChange} onSelectVariable={handleVariableChange}
/> />
</Stack> </Stack>

View File

@ -5,14 +5,14 @@ import { RedirectOptions } from 'models'
import React from 'react' import React from 'react'
type Props = { type Props = {
options?: RedirectOptions options: RedirectOptions
onOptionsChange: (options: RedirectOptions) => void onOptionsChange: (options: RedirectOptions) => void
} }
export const RedirectSettings = ({ options, onOptionsChange }: Props) => { export const RedirectSettings = ({ options, onOptionsChange }: Props) => {
const handleUrlChange = (url?: string) => onOptionsChange({ ...options, url }) const handleUrlChange = (url?: string) => onOptionsChange({ ...options, url })
const handleIsNewTabChange = (isNewTab?: boolean) => const handleIsNewTabChange = (isNewTab: boolean) =>
onOptionsChange({ ...options, isNewTab }) onOptionsChange({ ...options, isNewTab })
return ( return (
@ -23,7 +23,7 @@ export const RedirectSettings = ({ options, onOptionsChange }: Props) => {
</FormLabel> </FormLabel>
<DebouncedInput <DebouncedInput
id="tracking-id" id="tracking-id"
initialValue={options?.url ?? ''} initialValue={options.url ?? ''}
placeholder="Type a URL..." placeholder="Type a URL..."
delay={100} delay={100}
onChange={handleUrlChange} onChange={handleUrlChange}
@ -32,7 +32,7 @@ export const RedirectSettings = ({ options, onOptionsChange }: Props) => {
<SwitchWithLabel <SwitchWithLabel
id="new-tab" id="new-tab"
label="Open in new tab?" label="Open in new tab?"
initialValue={options?.isNewTab ?? false} initialValue={options.isNewTab}
onCheckChange={handleIsNewTabChange} onCheckChange={handleIsNewTabChange}
/> />
</Stack> </Stack>

View File

@ -5,7 +5,7 @@ import { SetVariableOptions, Variable } from 'models'
import React from 'react' import React from 'react'
type Props = { type Props = {
options?: SetVariableOptions options: SetVariableOptions
onOptionsChange: (options: SetVariableOptions) => void onOptionsChange: (options: SetVariableOptions) => void
} }
@ -26,7 +26,7 @@ export const SetVariableSettingsBody = ({
</FormLabel> </FormLabel>
<VariableSearchInput <VariableSearchInput
onSelectVariable={handleVariableChange} onSelectVariable={handleVariableChange}
initialVariableId={options?.variableId} initialVariableId={options.variableId}
id="variable-search" id="variable-search"
/> />
</Stack> </Stack>
@ -36,7 +36,7 @@ export const SetVariableSettingsBody = ({
</FormLabel> </FormLabel>
<DebouncedTextarea <DebouncedTextarea
id="expression" id="expression"
initialValue={options?.expressionToEvaluate ?? ''} initialValue={options.expressionToEvaluate ?? ''}
delay={100} delay={100}
onChange={handleExpressionChange} onChange={handleExpressionChange}
/> />

View File

@ -6,7 +6,7 @@ import { TextInputOptions, Variable } from 'models'
import React from 'react' import React from 'react'
type TextInputSettingsBodyProps = { type TextInputSettingsBodyProps = {
options?: TextInputOptions options: TextInputOptions
onOptionsChange: (options: TextInputOptions) => void onOptionsChange: (options: TextInputOptions) => void
} }
@ -15,9 +15,9 @@ export const TextInputSettingsBody = ({
onOptionsChange, onOptionsChange,
}: TextInputSettingsBodyProps) => { }: TextInputSettingsBodyProps) => {
const handlePlaceholderChange = (placeholder: string) => const handlePlaceholderChange = (placeholder: string) =>
onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } }) onOptionsChange({ ...options, labels: { ...options.labels, placeholder } })
const handleButtonLabelChange = (button: string) => const handleButtonLabelChange = (button: string) =>
onOptionsChange({ ...options, labels: { ...options?.labels, button } }) onOptionsChange({ ...options, labels: { ...options.labels, button } })
const handleLongChange = (isLong: boolean) => const handleLongChange = (isLong: boolean) =>
onOptionsChange({ ...options, isLong }) onOptionsChange({ ...options, isLong })
const handleVariableChange = (variable?: Variable) => const handleVariableChange = (variable?: Variable) =>
@ -37,7 +37,7 @@ export const TextInputSettingsBody = ({
</FormLabel> </FormLabel>
<DebouncedInput <DebouncedInput
id="placeholder" id="placeholder"
initialValue={options?.labels?.placeholder ?? 'Type your answer...'} initialValue={options.labels.placeholder}
delay={100} delay={100}
onChange={handlePlaceholderChange} onChange={handlePlaceholderChange}
/> />
@ -48,7 +48,7 @@ export const TextInputSettingsBody = ({
</FormLabel> </FormLabel>
<DebouncedInput <DebouncedInput
id="button" id="button"
initialValue={options?.labels?.button ?? 'Send'} initialValue={options.labels.button}
delay={100} delay={100}
onChange={handleButtonLabelChange} onChange={handleButtonLabelChange}
/> />
@ -58,7 +58,7 @@ export const TextInputSettingsBody = ({
Save answer in a variable: Save answer in a variable:
</FormLabel> </FormLabel>
<VariableSearchInput <VariableSearchInput
initialVariableId={options?.variableId} initialVariableId={options.variableId}
onSelectVariable={handleVariableChange} onSelectVariable={handleVariableChange}
/> />
</Stack> </Stack>

View File

@ -5,7 +5,7 @@ import { UrlInputOptions, Variable } from 'models'
import React from 'react' import React from 'react'
type UrlInputSettingsBodyProps = { type UrlInputSettingsBodyProps = {
options?: UrlInputOptions options: UrlInputOptions
onOptionsChange: (options: UrlInputOptions) => void onOptionsChange: (options: UrlInputOptions) => void
} }
@ -14,9 +14,9 @@ export const UrlInputSettingsBody = ({
onOptionsChange, onOptionsChange,
}: UrlInputSettingsBodyProps) => { }: UrlInputSettingsBodyProps) => {
const handlePlaceholderChange = (placeholder: string) => const handlePlaceholderChange = (placeholder: string) =>
onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } }) onOptionsChange({ ...options, labels: { ...options.labels, placeholder } })
const handleButtonLabelChange = (button: string) => const handleButtonLabelChange = (button: string) =>
onOptionsChange({ ...options, labels: { ...options?.labels, button } }) onOptionsChange({ ...options, labels: { ...options.labels, button } })
const handleVariableChange = (variable?: Variable) => const handleVariableChange = (variable?: Variable) =>
onOptionsChange({ ...options, variableId: variable?.id }) onOptionsChange({ ...options, variableId: variable?.id })
@ -28,7 +28,7 @@ export const UrlInputSettingsBody = ({
</FormLabel> </FormLabel>
<DebouncedInput <DebouncedInput
id="placeholder" id="placeholder"
initialValue={options?.labels?.placeholder ?? 'Type your URL...'} initialValue={options.labels.placeholder}
delay={100} delay={100}
onChange={handlePlaceholderChange} onChange={handlePlaceholderChange}
/> />
@ -39,7 +39,7 @@ export const UrlInputSettingsBody = ({
</FormLabel> </FormLabel>
<DebouncedInput <DebouncedInput
id="button" id="button"
initialValue={options?.labels?.button ?? 'Send'} initialValue={options.labels.button}
delay={100} delay={100}
onChange={handleButtonLabelChange} onChange={handleButtonLabelChange}
/> />
@ -49,7 +49,7 @@ export const UrlInputSettingsBody = ({
Save answer in a variable: Save answer in a variable:
</FormLabel> </FormLabel>
<VariableSearchInput <VariableSearchInput
initialVariableId={options?.variableId} initialVariableId={options.variableId}
onSelectVariable={handleVariableChange} onSelectVariable={handleVariableChange}
/> />
</Stack> </Stack>

View File

@ -1,5 +1,5 @@
import { Stack, FormControl, FormLabel } from '@chakra-ui/react' import { Stack, FormControl, FormLabel } from '@chakra-ui/react'
import { InputWithVariableButton } from 'components/shared/InputWithVariableButton' import { InputWithVariableButton } from 'components/shared/TextboxWithVariableButton/InputWithVariableButton'
import { TableListItemProps } from 'components/shared/TableList' import { TableListItemProps } from 'components/shared/TableList'
import { KeyValue } from 'models' import { KeyValue } from 'models'

View File

@ -10,7 +10,7 @@ import {
Stack, Stack,
useToast, useToast,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { InputWithVariableButton } from 'components/shared/InputWithVariableButton' import { InputWithVariableButton } from 'components/shared/TextboxWithVariableButton/InputWithVariableButton'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { import {
HttpMethod, HttpMethod,

View File

@ -103,7 +103,7 @@ export const StepNodeContent = ({ step }: Props) => {
return <ConditionNodeContent step={step} /> return <ConditionNodeContent step={step} />
} }
case LogicStepType.REDIRECT: { case LogicStepType.REDIRECT: {
if (!step.options) return <Text color={'gray.500'}>Configure...</Text> if (!step.options.url) return <Text color={'gray.500'}>Configure...</Text>
return <Text isTruncated>Redirect to {step.options?.url}</Text> return <Text isTruncated>Redirect to {step.options?.url}</Text>
} }
case IntegrationStepType.GOOGLE_SHEETS: { case IntegrationStepType.GOOGLE_SHEETS: {

View File

@ -0,0 +1,33 @@
import { Flex, FormLabel, Stack, Switch, Text } from '@chakra-ui/react'
import { GeneralSettings } from 'models'
import React from 'react'
type Props = {
generalSettings: GeneralSettings
onGeneralSettingsChange: (generalSettings: GeneralSettings) => void
}
export const GeneralSettingsForm = ({
generalSettings,
onGeneralSettingsChange,
}: Props) => {
const handleSwitchChange = () =>
onGeneralSettingsChange({
isBrandingEnabled: !generalSettings?.isBrandingEnabled,
})
return (
<Stack spacing={6}>
<Flex justifyContent="space-between" align="center">
<FormLabel htmlFor="branding" mb="0">
Typebot.io branding
</FormLabel>
<Switch
id="branding"
isChecked={generalSettings.isBrandingEnabled}
onChange={handleSwitchChange}
/>
</Flex>
</Stack>
)
}

View File

@ -0,0 +1,112 @@
import React from 'react'
import { Metadata } from 'models'
import {
FormLabel,
Popover,
PopoverTrigger,
Stack,
Image,
PopoverContent,
} from '@chakra-ui/react'
import { ImageUploadContent } from 'components/shared/ImageUploadContent'
import {
InputWithVariableButton,
TextareaWithVariableButton,
} from 'components/shared/TextboxWithVariableButton'
type Props = {
typebotName: string
metadata: Metadata
onMetadataChange: (metadata: Metadata) => void
}
export const MetadataForm = ({
typebotName,
metadata,
onMetadataChange,
}: Props) => {
const handleTitleChange = (title: string) =>
onMetadataChange({ ...metadata, title })
const handleDescriptionChange = (description: string) =>
onMetadataChange({ ...metadata, description })
const handleFavIconSubmit = (favIconUrl: string) =>
onMetadataChange({ ...metadata, favIconUrl })
const handleImageSubmit = (imageUrl: string) =>
onMetadataChange({ ...metadata, imageUrl })
return (
<Stack spacing="6">
<Stack>
<FormLabel mb="0" htmlFor="icon">
Icon:
</FormLabel>
<Popover isLazy>
<PopoverTrigger>
<Image
src={metadata.favIconUrl ?? '/favicon.png'}
w="20px"
alt="Fav icon"
cursor="pointer"
_hover={{ filter: 'brightness(.9)' }}
transition="filter 200ms"
rounded="md"
/>
</PopoverTrigger>
<PopoverContent p="4">
<ImageUploadContent
url={metadata.favIconUrl ?? ''}
onSubmit={handleFavIconSubmit}
isGiphyEnabled={false}
/>
</PopoverContent>
</Popover>
</Stack>
<Stack>
<FormLabel mb="0" htmlFor="image">
Image:
</FormLabel>
<Popover isLazy>
<PopoverTrigger>
<Image
src={metadata.imageUrl ?? '/viewer-preview.png'}
alt="Website image"
cursor="pointer"
_hover={{ filter: 'brightness(.9)' }}
transition="filter 200ms"
rounded="md"
/>
</PopoverTrigger>
<PopoverContent p="4">
<ImageUploadContent
url={metadata.imageUrl}
onSubmit={handleImageSubmit}
isGiphyEnabled={false}
/>
</PopoverContent>
</Popover>
</Stack>
<Stack>
<FormLabel mb="0" htmlFor="title">
Title:
</FormLabel>
<InputWithVariableButton
id="title"
initialValue={metadata.title ?? typebotName}
delay={100}
onChange={handleTitleChange}
/>
</Stack>
<Stack>
<FormLabel mb="0" htmlFor="description">
Description:
</FormLabel>
<TextareaWithVariableButton
id="description"
initialValue={metadata.description}
delay={100}
onChange={handleDescriptionChange}
/>
</Stack>
</Stack>
)
}

View File

@ -1,26 +0,0 @@
import { Flex, Stack } from '@chakra-ui/react'
import { TypingEmulationSettings } from 'models'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import React from 'react'
import { TypingEmulation } from './TypingEmulation'
export const SettingsContent = () => {
const { typebot, updateTypebot } = useTypebot()
const handleTypingEmulationUpdate = (
typingEmulation: TypingEmulationSettings
) => {
if (!typebot) return
updateTypebot({ settings: { ...typebot.settings, typingEmulation } })
}
return (
<Flex h="full" w="full" justifyContent="center" align="flex-start">
<Stack p="6" rounded="md" borderWidth={1} w="600px" minH="500px" mt={10}>
<TypingEmulation
typingEmulation={typebot?.settings?.typingEmulation}
onUpdate={handleTypingEmulationUpdate}
/>
</Stack>
</Flex>
)
}

View File

@ -0,0 +1,103 @@
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Heading,
HStack,
Stack,
} from '@chakra-ui/react'
import { ChatIcon, CodeIcon, MoreVerticalIcon } from 'assets/icons'
import { headerHeight } from 'components/shared/TypebotHeader'
import { useTypebot } from 'contexts/TypebotContext'
import { GeneralSettings, Metadata, TypingEmulation } from 'models'
import React from 'react'
import { GeneralSettingsForm } from './GeneralSettingsForm'
import { MetadataForm } from './MetadataForm'
import { TypingEmulationForm } from './TypingEmulationForm'
export const SettingsSideMenu = () => {
const { typebot, updateTypebot } = useTypebot()
const handleTypingEmulationChange = (typingEmulation: TypingEmulation) =>
typebot &&
updateTypebot({ settings: { ...typebot.settings, typingEmulation } })
const handleGeneralSettingsChange = (general: GeneralSettings) =>
typebot && updateTypebot({ settings: { ...typebot.settings, general } })
const handleMetadataChange = (metadata: Metadata) =>
typebot && updateTypebot({ settings: { ...typebot.settings, metadata } })
return (
<Stack
flex="1"
maxW="400px"
height={`calc(100vh - ${headerHeight}px)`}
borderRightWidth={1}
pt={10}
spacing={10}
overflowY="scroll"
pb="20"
>
<Heading fontSize="xl" textAlign="center">
Settings
</Heading>
<Accordion allowMultiple allowToggle>
<AccordionItem>
<AccordionButton py={6}>
<HStack flex="1" pl={2}>
<MoreVerticalIcon transform={'rotate(90deg)'} />
<Heading fontSize="lg">General</Heading>
</HStack>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} px="6">
{typebot && (
<GeneralSettingsForm
generalSettings={typebot.settings.general}
onGeneralSettingsChange={handleGeneralSettingsChange}
/>
)}
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton py={6}>
<HStack flex="1" pl={2}>
<ChatIcon />
<Heading fontSize="lg">Typing emulation</Heading>
</HStack>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} px="6">
{typebot && (
<TypingEmulationForm
typingEmulation={typebot.settings.typingEmulation}
onUpdate={handleTypingEmulationChange}
/>
)}
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton py={6}>
<HStack flex="1" pl={2}>
<CodeIcon />
<Heading fontSize="lg">Metadata</Heading>
</HStack>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} px="6">
{typebot && (
<MetadataForm
typebotName={typebot.name}
metadata={typebot.settings.metadata}
onMetadataChange={handleMetadataChange}
/>
)}
</AccordionPanel>
</AccordionItem>
</Accordion>
</Stack>
)
}

View File

@ -1,63 +0,0 @@
import { Flex, Stack, Switch, Text } from '@chakra-ui/react'
import { TypingEmulationSettings } from 'models'
import React from 'react'
import { SmartNumberInput } from './SmartNumberInput'
type TypingEmulationProps = {
typingEmulation?: TypingEmulationSettings
onUpdate: (typingEmulation: TypingEmulationSettings) => void
}
export const TypingEmulation = ({
typingEmulation,
onUpdate,
}: TypingEmulationProps) => {
const handleSwitchChange = () => {
if (!typingEmulation) return
onUpdate({ ...typingEmulation, enabled: !typingEmulation.enabled })
}
const handleSpeedChange = (speed?: number) => {
if (!typingEmulation) return
onUpdate({ ...typingEmulation, speed: speed ?? 0 })
}
const handleMaxDelayChange = (maxDelay?: number) => {
if (!typingEmulation) return
onUpdate({ ...typingEmulation, maxDelay: maxDelay ?? 0 })
}
return (
<Stack spacing={4}>
<Flex justifyContent="space-between" align="center">
<Text>Typing emulation</Text>
<Switch
isChecked={typingEmulation?.enabled}
onChange={handleSwitchChange}
/>
</Flex>
{typingEmulation?.enabled && (
<Stack pl={10}>
<Flex justify="space-between" align="center">
<Text>Words per minutes:</Text>
<SmartNumberInput
initialValue={typingEmulation.speed}
onValueChange={handleSpeedChange}
maxW="100px"
step={30}
/>
</Flex>
<Flex justify="space-between" align="center">
<Text>Max delay (in seconds):</Text>
<SmartNumberInput
initialValue={typingEmulation.maxDelay}
onValueChange={handleMaxDelayChange}
maxW="100px"
step={0.1}
/>
</Flex>
</Stack>
)}
</Stack>
)
}

View File

@ -0,0 +1,69 @@
import { Flex, FormLabel, Stack, Switch, Text } from '@chakra-ui/react'
import { TypingEmulation } from 'models'
import React from 'react'
import { isDefined } from 'utils'
import { SmartNumberInput } from '../shared/SmartNumberInput'
type Props = {
typingEmulation: TypingEmulation
onUpdate: (typingEmulation: TypingEmulation) => void
}
export const TypingEmulationForm = ({ typingEmulation, onUpdate }: Props) => {
const handleSwitchChange = () =>
onUpdate({
...typingEmulation,
enabled: !typingEmulation.enabled,
})
const handleSpeedChange = (speed?: number) =>
isDefined(speed) && onUpdate({ ...typingEmulation, speed })
const handleMaxDelayChange = (maxDelay?: number) =>
isDefined(maxDelay) && onUpdate({ ...typingEmulation, maxDelay })
return (
<Stack spacing={6}>
<Flex justifyContent="space-between" align="center">
<FormLabel htmlFor="typing-emulation" mb="0">
Typing emulation
</FormLabel>
<Switch
id="typing-emulation"
isChecked={typingEmulation.enabled}
onChange={handleSwitchChange}
/>
</Flex>
{typingEmulation.enabled && (
<Stack pl={10}>
<Flex justify="space-between" align="center">
<FormLabel htmlFor="speed" mb="0">
Words per minutes:
</FormLabel>
<SmartNumberInput
id="speed"
data-testid="speed"
value={typingEmulation.speed}
onValueChange={handleSpeedChange}
maxW="100px"
step={30}
/>
</Flex>
<Flex justify="space-between" align="center">
<FormLabel htmlFor="max-delay" mb="0">
Max delay (in seconds):
</FormLabel>
<SmartNumberInput
id="max-delay"
data-testid="max-delay"
value={typingEmulation.maxDelay}
onValueChange={handleMaxDelayChange}
maxW="100px"
step={0.1}
/>
</Flex>
</Stack>
)}
</Stack>
)
}

View File

@ -1,22 +1,27 @@
import { ChangeEvent, FormEvent, useState } from 'react' import { ChangeEvent, FormEvent, useEffect, useState } from 'react'
import { Button, HStack, Input, Stack } from '@chakra-ui/react' import { Button, HStack, Input, Stack } from '@chakra-ui/react'
import { SearchContextManager } from '@giphy/react-components' import { SearchContextManager } from '@giphy/react-components'
import { UploadButton } from '../buttons/UploadButton' import { UploadButton } from '../buttons/UploadButton'
import { GiphySearch } from './GiphySearch' import { GiphySearch } from './GiphySearch'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { ImageBubbleContent } from 'models' import { useDebounce } from 'use-debounce'
type Props = { type Props = {
content?: ImageBubbleContent url?: string
onSubmit: (content: ImageBubbleContent) => void onSubmit: (url: string) => void
isGiphyEnabled?: boolean
} }
export const ImageUploadContent = ({ content, onSubmit }: Props) => { export const ImageUploadContent = ({
url,
onSubmit,
isGiphyEnabled = true,
}: Props) => {
const [currentTab, setCurrentTab] = useState<'link' | 'upload' | 'giphy'>( const [currentTab, setCurrentTab] = useState<'link' | 'upload' | 'giphy'>(
'upload' 'upload'
) )
const handleSubmit = (url: string) => onSubmit({ url }) const handleSubmit = (url: string) => onSubmit(url)
return ( return (
<Stack> <Stack>
<HStack> <HStack>
@ -34,7 +39,7 @@ export const ImageUploadContent = ({ content, onSubmit }: Props) => {
> >
Embed link Embed link
</Button> </Button>
{process.env.NEXT_PUBLIC_GIPHY_API_KEY && ( {process.env.NEXT_PUBLIC_GIPHY_API_KEY && isGiphyEnabled && (
<Button <Button
variant={currentTab === 'giphy' ? 'solid' : 'ghost'} variant={currentTab === 'giphy' ? 'solid' : 'ghost'}
onClick={() => setCurrentTab('giphy')} onClick={() => setCurrentTab('giphy')}
@ -45,11 +50,7 @@ export const ImageUploadContent = ({ content, onSubmit }: Props) => {
)} )}
</HStack> </HStack>
<BodyContent <BodyContent tab={currentTab} onSubmit={handleSubmit} url={url} />
tab={currentTab}
onSubmit={handleSubmit}
url={content?.url}
/>
</Stack> </Stack>
) )
} }
@ -93,26 +94,23 @@ const UploadFileContent = ({ onNewUrl }: ContentProps) => {
const EmbedLinkContent = ({ initialUrl, onNewUrl }: ContentProps) => { const EmbedLinkContent = ({ initialUrl, onNewUrl }: ContentProps) => {
const [imageUrl, setImageUrl] = useState(initialUrl ?? '') const [imageUrl, setImageUrl] = useState(initialUrl ?? '')
const [debouncedImageUrl] = useDebounce(imageUrl, 100)
useEffect(() => {
if (initialUrl === debouncedImageUrl) return
onNewUrl(imageUrl)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedImageUrl])
const handleImageUrlChange = (e: ChangeEvent<HTMLInputElement>) => const handleImageUrlChange = (e: ChangeEvent<HTMLInputElement>) =>
setImageUrl(e.target.value) setImageUrl(e.target.value)
const handleUrlSubmit = (e: FormEvent) => {
e.preventDefault()
onNewUrl(imageUrl)
}
return ( return (
<Stack as="form" onSubmit={handleUrlSubmit}> <Input
<Input placeholder={'Paste the image link...'}
placeholder={'Paste the image link...'} onChange={handleImageUrlChange}
onChange={handleImageUrlChange} value={imageUrl}
value={imageUrl} />
/>
<Button type="submit" disabled={imageUrl === ''} colorScheme="blue">
Embed image
</Button>
</Stack>
) )
} }

View File

@ -0,0 +1,9 @@
import { Alert, AlertIcon, AlertProps } from '@chakra-ui/react'
import React from 'react'
export const Info = (props: AlertProps) => (
<Alert status="info" bgColor={'blue.50'} rounded="md" {...props}>
<AlertIcon />
{props.children}
</Alert>
)

View File

@ -6,29 +6,29 @@ import {
NumberIncrementStepper, NumberIncrementStepper,
NumberDecrementStepper, NumberDecrementStepper,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { useState, useEffect } from 'react' import { useState } from 'react'
export const SmartNumberInput = ({ export const SmartNumberInput = ({
initialValue, value,
onValueChange, onValueChange,
...props ...props
}: { }: {
initialValue?: number value?: number
onValueChange: (value?: number) => void onValueChange: (value?: number) => void
} & NumberInputProps) => { } & NumberInputProps) => {
const [value, setValue] = useState(initialValue?.toString() ?? '') const [currentValue, setCurrentValue] = useState(value?.toString() ?? '')
useEffect(() => { const handleValueChange = (value: string) => {
setCurrentValue(value)
if (value.endsWith('.') || value.endsWith(',')) return if (value.endsWith('.') || value.endsWith(',')) return
if (value === '') onValueChange(undefined) if (value === '') return onValueChange(undefined)
const newValue = parseFloat(value) const newValue = parseFloat(value)
if (isNaN(newValue)) return if (isNaN(newValue)) return
onValueChange(newValue) onValueChange(newValue)
// eslint-disable-next-line react-hooks/exhaustive-deps }
}, [value])
return ( return (
<NumberInput onChange={setValue} value={value} {...props}> <NumberInput onChange={handleValueChange} value={currentValue} {...props}>
<NumberInputField /> <NumberInputField />
<NumberInputStepper> <NumberInputStepper>
<NumberIncrementStepper /> <NumberIncrementStepper />

View File

@ -0,0 +1,10 @@
import { Input } from '@chakra-ui/react'
import React from 'react'
import {
TextBoxWithVariableButton,
TextBoxWithVariableButtonProps,
} from './TextboxWithVariableButton'
export const InputWithVariableButton = (
props: Omit<TextBoxWithVariableButtonProps, 'TextBox'>
) => <TextBoxWithVariableButton TextBox={Input} {...props} />

View File

@ -0,0 +1,10 @@
import { Textarea } from '@chakra-ui/react'
import React from 'react'
import {
TextBoxWithVariableButton,
TextBoxWithVariableButtonProps,
} from './TextboxWithVariableButton'
export const TextareaWithVariableButton = (
props: Omit<TextBoxWithVariableButtonProps, 'TextBox'>
) => <TextBoxWithVariableButton TextBox={Textarea} {...props} />

View File

@ -1,31 +1,40 @@
import { import {
ComponentWithAs,
Flex, Flex,
HStack, HStack,
IconButton, IconButton,
Input,
InputProps, InputProps,
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
TextareaProps,
Tooltip, Tooltip,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { UserIcon } from 'assets/icons' import { UserIcon } from 'assets/icons'
import { Variable } from 'models' import { Variable } from 'models'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
import { VariableSearchInput } from './VariableSearchInput' import { VariableSearchInput } from '../VariableSearchInput'
export const InputWithVariableButton = ({ export type TextBoxWithVariableButtonProps = {
initialValue,
onChange,
delay,
...props
}: {
initialValue: string initialValue: string
onChange: (value: string) => void onChange: (value: string) => void
delay?: number delay?: number
} & Omit<InputProps, 'onChange'>) => { TextBox:
const inputRef = useRef<HTMLInputElement | null>(null) | ComponentWithAs<'textarea', TextareaProps>
| ComponentWithAs<'input', InputProps>
} & Omit<InputProps & TextareaProps, 'onChange'>
export const TextBoxWithVariableButton = ({
initialValue,
onChange,
delay,
TextBox,
...props
}: TextBoxWithVariableButtonProps) => {
const textBoxRef = useRef<(HTMLInputElement & HTMLTextAreaElement) | null>(
null
)
const [value, setValue] = useState(initialValue) const [value, setValue] = useState(initialValue)
const [debouncedValue] = useDebounce(value, delay ?? 100) const [debouncedValue] = useDebounce(value, delay ?? 100)
const [carretPosition, setCarretPosition] = useState<number>(0) const [carretPosition, setCarretPosition] = useState<number>(0)
@ -36,48 +45,49 @@ export const InputWithVariableButton = ({
}, [debouncedValue]) }, [debouncedValue])
const handleVariableSelected = (variable?: Variable) => { const handleVariableSelected = (variable?: Variable) => {
if (!inputRef.current || !variable) return if (!textBoxRef.current || !variable) return
const cursorPosition = carretPosition const cursorPosition = carretPosition
const textBeforeCursorPosition = inputRef.current.value.substring( const textBeforeCursorPosition = textBoxRef.current.value.substring(
0, 0,
cursorPosition cursorPosition
) )
const textAfterCursorPosition = inputRef.current.value.substring( const textAfterCursorPosition = textBoxRef.current.value.substring(
cursorPosition, cursorPosition,
inputRef.current.value.length textBoxRef.current.value.length
) )
setValue( setValue(
textBeforeCursorPosition + textBeforeCursorPosition +
`{{${variable.name}}}` + `{{${variable.name}}}` +
textAfterCursorPosition textAfterCursorPosition
) )
inputRef.current.focus() textBoxRef.current.focus()
setTimeout(() => { setTimeout(() => {
if (!inputRef.current) return if (!textBoxRef.current) return
inputRef.current.selectionStart = inputRef.current.selectionEnd = textBoxRef.current.selectionStart = textBoxRef.current.selectionEnd =
carretPosition + `{{${variable.name}}}`.length carretPosition + `{{${variable.name}}}`.length
setCarretPosition(inputRef.current.selectionStart) setCarretPosition(textBoxRef.current.selectionStart)
}, 100) }, 100)
} }
const handleKeyUp = () => { const handleKeyUp = () => {
if (!inputRef.current?.selectionStart) return if (!textBoxRef.current?.selectionStart) return
setCarretPosition(inputRef.current.selectionStart) setCarretPosition(textBoxRef.current.selectionStart)
} }
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => const handleChange = (
setValue(e.target.value) e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => setValue(e.target.value)
return ( return (
<HStack spacing={0}> <HStack spacing={0} align={'flex-end'}>
<Input <TextBox
ref={inputRef} ref={textBoxRef}
onKeyUp={handleKeyUp} onKeyUp={handleKeyUp}
onClick={handleKeyUp} onClick={handleKeyUp}
value={value} value={value}
onChange={handleChange} onChange={handleChange}
{...props}
bgColor={'white'} bgColor={'white'}
{...props}
/> />
<Popover matchWidth isLazy> <Popover matchWidth isLazy>
<PopoverTrigger> <PopoverTrigger>

View File

@ -0,0 +1,2 @@
export { InputWithVariableButton } from './InputWithVariableButton'
export { TextareaWithVariableButton } from './TextareaWithVariableButton'

View File

@ -4,13 +4,10 @@ import React from 'react'
import { ColorPicker } from '../GeneralSettings/ColorPicker' import { ColorPicker } from '../GeneralSettings/ColorPicker'
type Props = { type Props = {
buttons?: ContainerColors buttons: ContainerColors
onButtonsChange: (buttons: ContainerColors) => void onButtonsChange: (buttons: ContainerColors) => void
} }
const defaultBackgroundColor = '#0042da'
const defaultTextColor = '#ffffff'
export const ButtonsTheme = ({ buttons, onButtonsChange }: Props) => { export const ButtonsTheme = ({ buttons, onButtonsChange }: Props) => {
const handleBackgroundChange = (backgroundColor: string) => const handleBackgroundChange = (backgroundColor: string) =>
onButtonsChange({ ...buttons, backgroundColor }) onButtonsChange({ ...buttons, backgroundColor })
@ -22,14 +19,14 @@ export const ButtonsTheme = ({ buttons, onButtonsChange }: Props) => {
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Text>Background:</Text> <Text>Background:</Text>
<ColorPicker <ColorPicker
initialColor={buttons?.backgroundColor ?? defaultBackgroundColor} initialColor={buttons.backgroundColor}
onColorChange={handleBackgroundChange} onColorChange={handleBackgroundChange}
/> />
</Flex> </Flex>
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Text>Text:</Text> <Text>Text:</Text>
<ColorPicker <ColorPicker
initialColor={buttons?.color ?? defaultTextColor} initialColor={buttons.color}
onColorChange={handleTextChange} onColorChange={handleTextChange}
/> />
</Flex> </Flex>

View File

@ -7,7 +7,7 @@ import { HostBubbles } from './HostBubbles'
import { InputsTheme } from './InputsTheme' import { InputsTheme } from './InputsTheme'
type Props = { type Props = {
chatTheme?: ChatTheme chatTheme: ChatTheme
onChatThemeChange: (chatTheme: ChatTheme) => void onChatThemeChange: (chatTheme: ChatTheme) => void
} }
@ -26,28 +26,28 @@ export const ChatThemeSettings = ({ chatTheme, onChatThemeChange }: Props) => {
<Stack borderWidth={1} rounded="md" p="4" spacing={4}> <Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">Bot bubbles</Heading> <Heading fontSize="lg">Bot bubbles</Heading>
<HostBubbles <HostBubbles
hostBubbles={chatTheme?.hostBubbles} hostBubbles={chatTheme.hostBubbles}
onHostBubblesChange={handleHostBubblesChange} onHostBubblesChange={handleHostBubblesChange}
/> />
</Stack> </Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}> <Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">User bubbles</Heading> <Heading fontSize="lg">User bubbles</Heading>
<GuestBubbles <GuestBubbles
guestBubbles={chatTheme?.guestBubbles} guestBubbles={chatTheme.guestBubbles}
onGuestBubblesChange={handleGuestBubblesChange} onGuestBubblesChange={handleGuestBubblesChange}
/> />
</Stack> </Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}> <Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">Buttons</Heading> <Heading fontSize="lg">Buttons</Heading>
<ButtonsTheme <ButtonsTheme
buttons={chatTheme?.buttons} buttons={chatTheme.buttons}
onButtonsChange={handleButtonsChange} onButtonsChange={handleButtonsChange}
/> />
</Stack> </Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}> <Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">Inputs</Heading> <Heading fontSize="lg">Inputs</Heading>
<InputsTheme <InputsTheme
inputs={chatTheme?.inputs} inputs={chatTheme.inputs}
onInputsChange={handleInputsChange} onInputsChange={handleInputsChange}
/> />
</Stack> </Stack>

View File

@ -4,12 +4,10 @@ import React from 'react'
import { ColorPicker } from '../GeneralSettings/ColorPicker' import { ColorPicker } from '../GeneralSettings/ColorPicker'
type Props = { type Props = {
guestBubbles?: ContainerColors guestBubbles: ContainerColors
onGuestBubblesChange: (hostBubbles: ContainerColors) => void onGuestBubblesChange: (hostBubbles: ContainerColors) => void
} }
const defaultBackgroundColor = '#ff8e21'
const defaultTextColor = '#ffffff'
export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => { export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
const handleBackgroundChange = (backgroundColor: string) => const handleBackgroundChange = (backgroundColor: string) =>
onGuestBubblesChange({ ...guestBubbles, backgroundColor }) onGuestBubblesChange({ ...guestBubbles, backgroundColor })
@ -21,14 +19,14 @@ export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Text>Background:</Text> <Text>Background:</Text>
<ColorPicker <ColorPicker
initialColor={guestBubbles?.backgroundColor ?? defaultBackgroundColor} initialColor={guestBubbles.backgroundColor}
onColorChange={handleBackgroundChange} onColorChange={handleBackgroundChange}
/> />
</Flex> </Flex>
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Text>Text:</Text> <Text>Text:</Text>
<ColorPicker <ColorPicker
initialColor={guestBubbles?.color ?? defaultTextColor} initialColor={guestBubbles.color}
onColorChange={handleTextChange} onColorChange={handleTextChange}
/> />
</Flex> </Flex>

View File

@ -4,13 +4,10 @@ import React from 'react'
import { ColorPicker } from '../GeneralSettings/ColorPicker' import { ColorPicker } from '../GeneralSettings/ColorPicker'
type Props = { type Props = {
hostBubbles?: ContainerColors hostBubbles: ContainerColors
onHostBubblesChange: (hostBubbles: ContainerColors) => void onHostBubblesChange: (hostBubbles: ContainerColors) => void
} }
const defaultBackgroundColor = '#f7f8ff'
const defaultTextColor = '#303235'
export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => { export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
const handleBackgroundChange = (backgroundColor: string) => const handleBackgroundChange = (backgroundColor: string) =>
onHostBubblesChange({ ...hostBubbles, backgroundColor }) onHostBubblesChange({ ...hostBubbles, backgroundColor })
@ -22,14 +19,14 @@ export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Text>Background:</Text> <Text>Background:</Text>
<ColorPicker <ColorPicker
initialColor={hostBubbles?.backgroundColor ?? defaultBackgroundColor} initialColor={hostBubbles.backgroundColor}
onColorChange={handleBackgroundChange} onColorChange={handleBackgroundChange}
/> />
</Flex> </Flex>
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Text>Text:</Text> <Text>Text:</Text>
<ColorPicker <ColorPicker
initialColor={hostBubbles?.color ?? defaultTextColor} initialColor={hostBubbles.color}
onColorChange={handleTextChange} onColorChange={handleTextChange}
/> />
</Flex> </Flex>

View File

@ -4,14 +4,10 @@ import React from 'react'
import { ColorPicker } from '../GeneralSettings/ColorPicker' import { ColorPicker } from '../GeneralSettings/ColorPicker'
type Props = { type Props = {
inputs?: InputColors inputs: InputColors
onInputsChange: (buttons: InputColors) => void onInputsChange: (buttons: InputColors) => void
} }
const defaultBackgroundColor = '#ffffff'
const defaultTextColor = '#303235'
const defaultPlaceholderColor = '#9095A0'
export const InputsTheme = ({ inputs, onInputsChange }: Props) => { export const InputsTheme = ({ inputs, onInputsChange }: Props) => {
const handleBackgroundChange = (backgroundColor: string) => const handleBackgroundChange = (backgroundColor: string) =>
onInputsChange({ ...inputs, backgroundColor }) onInputsChange({ ...inputs, backgroundColor })
@ -25,21 +21,21 @@ export const InputsTheme = ({ inputs, onInputsChange }: Props) => {
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Text>Background:</Text> <Text>Background:</Text>
<ColorPicker <ColorPicker
initialColor={inputs?.backgroundColor ?? defaultBackgroundColor} initialColor={inputs.backgroundColor}
onColorChange={handleBackgroundChange} onColorChange={handleBackgroundChange}
/> />
</Flex> </Flex>
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Text>Text:</Text> <Text>Text:</Text>
<ColorPicker <ColorPicker
initialColor={inputs?.color ?? defaultTextColor} initialColor={inputs.color}
onColorChange={handleTextChange} onColorChange={handleTextChange}
/> />
</Flex> </Flex>
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Text>Placeholder text:</Text> <Text>Placeholder text:</Text>
<ColorPicker <ColorPicker
initialColor={inputs?.placeholderColor ?? defaultPlaceholderColor} initialColor={inputs.placeholderColor}
onColorChange={handlePlaceholderChange} onColorChange={handlePlaceholderChange}
/> />
</Flex> </Flex>

View File

@ -11,7 +11,7 @@ import {
Input, Input,
Button, Button,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import React, { useEffect, useState } from 'react' import React, { ChangeEvent, useEffect, useState } from 'react'
const colorsSelection: `#${string}`[] = [ const colorsSelection: `#${string}`[] = [
'#264653', '#264653',
@ -38,6 +38,9 @@ export const ColorPicker = ({ initialColor, onColorChange }: Props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [color]) }, [color])
const handleColorChange = (e: ChangeEvent<HTMLInputElement>) =>
setColor(e.target.value)
return ( return (
<Popover variant="picker"> <Popover variant="picker">
<PopoverTrigger> <PopoverTrigger>
@ -89,9 +92,7 @@ export const ColorPicker = ({ initialColor, onColorChange }: Props) => {
aria-label="Color value" aria-label="Color value"
size="sm" size="sm"
value={color} value={color}
onChange={(e) => { onChange={handleColorChange}
setColor(e.target.value)
}}
/> />
</PopoverBody> </PopoverBody>
</PopoverContent> </PopoverContent>

View File

@ -1,16 +1,14 @@
import { Stack } from '@chakra-ui/react' import { Stack } from '@chakra-ui/react'
import { Background, BackgroundType, GeneralTheme } from 'models' import { Background, GeneralTheme } from 'models'
import React from 'react' import React from 'react'
import { BackgroundSelector } from './BackgroundSelector' import { BackgroundSelector } from './BackgroundSelector'
import { FontSelector } from './FontSelector' import { FontSelector } from './FontSelector'
type Props = { type Props = {
generalTheme?: GeneralTheme generalTheme: GeneralTheme
onGeneralThemeChange: (general: GeneralTheme) => void onGeneralThemeChange: (general: GeneralTheme) => void
} }
const defaultFont = 'Open Sans'
export const GeneralSettings = ({ export const GeneralSettings = ({
generalTheme, generalTheme,
onGeneralThemeChange, onGeneralThemeChange,
@ -24,11 +22,11 @@ export const GeneralSettings = ({
return ( return (
<Stack spacing={6}> <Stack spacing={6}>
<FontSelector <FontSelector
activeFont={generalTheme?.font ?? defaultFont} activeFont={generalTheme.font}
onSelectFont={handleSelectFont} onSelectFont={handleSelectFont}
/> />
<BackgroundSelector <BackgroundSelector
background={generalTheme?.background ?? { type: BackgroundType.NONE }} background={generalTheme.background}
onBackgroundChange={handleBackgroundChange} onBackgroundChange={handleBackgroundChange}
/> />
</Stack> </Stack>

View File

@ -17,17 +17,17 @@ import { ChatThemeSettings } from './ChatSettings'
import { CustomCssSettings } from './CustomCssSettings/CustomCssSettings' import { CustomCssSettings } from './CustomCssSettings/CustomCssSettings'
import { GeneralSettings } from './GeneralSettings' import { GeneralSettings } from './GeneralSettings'
export const SideMenu = () => { export const ThemeSideMenu = () => {
const { typebot, updateTypebot } = useTypebot() const { typebot, updateTypebot } = useTypebot()
const handleChatThemeChange = (chat: ChatTheme) => const handleChatThemeChange = (chat: ChatTheme) =>
updateTypebot({ theme: { ...typebot?.theme, chat } }) typebot && updateTypebot({ theme: { ...typebot.theme, chat } })
const handleGeneralThemeChange = (general: GeneralTheme) => const handleGeneralThemeChange = (general: GeneralTheme) =>
updateTypebot({ theme: { ...typebot?.theme, general } }) typebot && updateTypebot({ theme: { ...typebot.theme, general } })
const handleCustomCssChange = (customCss: string) => const handleCustomCssChange = (customCss: string) =>
updateTypebot({ theme: { ...typebot?.theme, customCss } }) typebot && updateTypebot({ theme: { ...typebot.theme, customCss } })
return ( return (
<Stack <Stack
@ -53,10 +53,12 @@ export const SideMenu = () => {
<AccordionIcon /> <AccordionIcon />
</AccordionButton> </AccordionButton>
<AccordionPanel pb={4}> <AccordionPanel pb={4}>
<GeneralSettings {typebot && (
generalTheme={typebot?.theme?.general} <GeneralSettings
onGeneralThemeChange={handleGeneralThemeChange} generalTheme={typebot.theme.general}
/> onGeneralThemeChange={handleGeneralThemeChange}
/>
)}
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
<AccordionItem> <AccordionItem>
@ -68,10 +70,12 @@ export const SideMenu = () => {
<AccordionIcon /> <AccordionIcon />
</AccordionButton> </AccordionButton>
<AccordionPanel pb={4}> <AccordionPanel pb={4}>
<ChatThemeSettings {typebot && (
chatTheme={typebot?.theme?.chat} <ChatThemeSettings
onChatThemeChange={handleChatThemeChange} chatTheme={typebot.theme.chat}
/> onChatThemeChange={handleChatThemeChange}
/>
)}
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
<AccordionItem> <AccordionItem>
@ -83,10 +87,12 @@ export const SideMenu = () => {
<AccordionIcon /> <AccordionIcon />
</AccordionButton> </AccordionButton>
<AccordionPanel pb={4}> <AccordionPanel pb={4}>
<CustomCssSettings {typebot && (
customCss={typebot?.theme?.customCss} <CustomCssSettings
onCustomCssChange={handleCustomCssChange} customCss={typebot.theme.customCss}
/> onCustomCssChange={handleCustomCssChange}
/>
)}
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>

View File

@ -62,9 +62,12 @@ export const UserContext = ({ children }: { children: ReactNode }) => {
useEffect(() => { useEffect(() => {
if (!router.isReady) return if (!router.isReady) return
if (status === 'loading') return if (status === 'loading') return
if (status === 'unauthenticated') router.replace('/signin') if (status === 'unauthenticated' && !isSigningIn())
router.replace('/signin')
}, [status, router]) }, [status, router])
const isSigningIn = () => ['/signin', '/register'].includes(router.pathname)
const updateUser = (newUser: Partial<User>) => { const updateUser = (newUser: Partial<User>) => {
if (!isDefined(user)) return if (!isDefined(user)) return
setUser({ ...user, ...newUser }) setUser({ ...user, ...newUser })

View File

@ -94,12 +94,23 @@
"allIds": ["benDCcLMUWNvi6Fg6CXE9H", "6Tax9rw7L8kmRn9JRD2Mrg"] "allIds": ["benDCcLMUWNvi6Fg6CXE9H", "6Tax9rw7L8kmRn9JRD2Mrg"]
}, },
"theme": { "theme": {
"general": { "chat": {
"font": "Open Sans", "inputs": {
"background": { "type": "None", "content": "#ffffff" } "color": "#303235",
} "backgroundColor": "#FFFFFF",
"placeholderColor": "#9095A0"
},
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
},
"general": { "font": "Open Sans", "background": { "type": "None" } }
}, },
"settings": { "settings": {
"general": { "isBrandingEnabled": true },
"metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
},
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 } "typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
}, },
"publicId": null "publicId": null

View File

@ -132,12 +132,23 @@
] ]
}, },
"theme": { "theme": {
"general": { "chat": {
"font": "Open Sans", "inputs": {
"background": { "type": "None", "content": "#ffffff" } "color": "#303235",
} "backgroundColor": "#FFFFFF",
"placeholderColor": "#9095A0"
},
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
},
"general": { "font": "Open Sans", "background": { "type": "None" } }
}, },
"settings": { "settings": {
"general": { "isBrandingEnabled": true },
"metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
},
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 } "typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
}, },
"publicId": null "publicId": null

View File

@ -138,12 +138,23 @@
"allIds": ["25yX9DnQgdafpdAjfAu5Fp", "oxEEtym3NfDf34NCipzjRQ"] "allIds": ["25yX9DnQgdafpdAjfAu5Fp", "oxEEtym3NfDf34NCipzjRQ"]
}, },
"theme": { "theme": {
"general": { "chat": {
"font": "Open Sans", "inputs": {
"background": { "type": "None", "content": "#ffffff" } "color": "#303235",
} "backgroundColor": "#FFFFFF",
"placeholderColor": "#9095A0"
},
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
},
"general": { "font": "Open Sans", "background": { "type": "None" } }
}, },
"settings": { "settings": {
"general": { "isBrandingEnabled": true },
"metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
},
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 } "typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
}, },
"publicId": null "publicId": null

View File

@ -252,12 +252,23 @@
] ]
}, },
"theme": { "theme": {
"general": { "chat": {
"font": "Open Sans", "inputs": {
"background": { "type": "None", "content": "#ffffff" } "color": "#303235",
} "backgroundColor": "#FFFFFF",
"placeholderColor": "#9095A0"
},
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
},
"general": { "font": "Open Sans", "background": { "type": "None" } }
}, },
"settings": { "settings": {
"general": { "isBrandingEnabled": true },
"metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
},
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 } "typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
}, },
"publicId": null "publicId": null

View File

@ -235,12 +235,23 @@
] ]
}, },
"theme": { "theme": {
"general": { "chat": {
"font": "Open Sans", "inputs": {
"background": { "type": "None", "content": "#ffffff" } "color": "#303235",
} "backgroundColor": "#FFFFFF",
"placeholderColor": "#9095A0"
},
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
},
"general": { "font": "Open Sans", "background": { "type": "None" } }
}, },
"settings": { "settings": {
"general": { "isBrandingEnabled": true },
"metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
},
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 } "typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
}, },
"publicId": null "publicId": null

View File

@ -52,7 +52,8 @@
"sqNGop2aYkXRvJqb9nGtFbD": { "sqNGop2aYkXRvJqb9nGtFbD": {
"id": "sqNGop2aYkXRvJqb9nGtFbD", "id": "sqNGop2aYkXRvJqb9nGtFbD",
"blockId": "bnsxmer7DD2R9DogoXTsvHJ", "blockId": "bnsxmer7DD2R9DogoXTsvHJ",
"type": "Redirect" "type": "Redirect",
"options": { "isNewTab": false }
} }
}, },
"allIds": [ "allIds": [
@ -96,12 +97,23 @@
"allIds": ["totLsWG6AQfcFT39CsZwDy", "7KgqWB88ufzhDwzvwHuEbN"] "allIds": ["totLsWG6AQfcFT39CsZwDy", "7KgqWB88ufzhDwzvwHuEbN"]
}, },
"theme": { "theme": {
"general": { "chat": {
"font": "Open Sans", "inputs": {
"background": { "type": "None", "content": "#ffffff" } "color": "#303235",
} "backgroundColor": "#FFFFFF",
"placeholderColor": "#9095A0"
},
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
},
"general": { "font": "Open Sans", "background": { "type": "None" } }
}, },
"settings": { "settings": {
"general": { "isBrandingEnabled": true },
"metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
},
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 } "typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
}, },
"publicId": null "publicId": null

View File

@ -54,7 +54,12 @@
"id": "s8n3nJajsBaYqrFeRYVvcf6", "id": "s8n3nJajsBaYqrFeRYVvcf6",
"blockId": "bwWRAaX5m6NZyZ9jjpXmWSb", "blockId": "bwWRAaX5m6NZyZ9jjpXmWSb",
"type": "number input", "type": "number input",
"edgeId": "dcJedLC7qsLtsmm1wbiFFc" "edgeId": "dcJedLC7qsLtsmm1wbiFFc",
"options": {
"labels": {
"placeholder": "Type a number..."
}
}
}, },
"sqMVMXeRYp4inLcRqej2Wac": { "sqMVMXeRYp4inLcRqej2Wac": {
"id": "sqMVMXeRYp4inLcRqej2Wac", "id": "sqMVMXeRYp4inLcRqej2Wac",
@ -71,13 +76,15 @@
"shfL5ueQDuj2RPcJPWZGArT": { "shfL5ueQDuj2RPcJPWZGArT": {
"id": "shfL5ueQDuj2RPcJPWZGArT", "id": "shfL5ueQDuj2RPcJPWZGArT",
"blockId": "baUyUnNBxZzPe1z5PqE4NkD", "blockId": "baUyUnNBxZzPe1z5PqE4NkD",
"type": "Set variable" "type": "Set variable",
"options": {}
}, },
"sugJ6xN3jFys1CjWfsxGhiJ": { "sugJ6xN3jFys1CjWfsxGhiJ": {
"id": "sugJ6xN3jFys1CjWfsxGhiJ", "id": "sugJ6xN3jFys1CjWfsxGhiJ",
"blockId": "baUyUnNBxZzPe1z5PqE4NkD", "blockId": "baUyUnNBxZzPe1z5PqE4NkD",
"type": "Set variable", "type": "Set variable",
"edgeId": "sA5gvCVVBVYdGsdeSGF5ei" "edgeId": "sA5gvCVVBVYdGsdeSGF5ei",
"options": {}
}, },
"shR7ae3iNEvB6arCSu7wVFF": { "shR7ae3iNEvB6arCSu7wVFF": {
"id": "shR7ae3iNEvB6arCSu7wVFF", "id": "shR7ae3iNEvB6arCSu7wVFF",
@ -141,12 +148,23 @@
] ]
}, },
"theme": { "theme": {
"general": { "chat": {
"font": "Open Sans", "inputs": {
"background": { "type": "None", "content": "#ffffff" } "color": "#303235",
} "backgroundColor": "#FFFFFF",
"placeholderColor": "#9095A0"
},
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
},
"general": { "font": "Open Sans", "background": { "type": "None" } }
}, },
"settings": { "settings": {
"general": { "isBrandingEnabled": true },
"metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
},
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 } "typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
}, },
"publicId": null "publicId": null

View File

@ -154,12 +154,23 @@
] ]
}, },
"theme": { "theme": {
"general": { "chat": {
"font": "Open Sans", "inputs": {
"background": { "type": "None", "content": "#ffffff" } "color": "#303235",
} "backgroundColor": "#FFFFFF",
"placeholderColor": "#9095A0"
},
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
},
"general": { "font": "Open Sans", "background": { "type": "None" } }
}, },
"settings": { "settings": {
"general": { "isBrandingEnabled": true },
"metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
},
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 } "typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
}, },
"publicId": null "publicId": null

View File

@ -136,12 +136,23 @@
"allIds": ["25yX9DnQgdafpdAjfAu5Fp", "6e4Sbp8pGTvBQYtCk2qXbN"] "allIds": ["25yX9DnQgdafpdAjfAu5Fp", "6e4Sbp8pGTvBQYtCk2qXbN"]
}, },
"theme": { "theme": {
"general": { "chat": {
"font": "Open Sans", "inputs": {
"background": { "type": "None", "content": "#ffffff" } "color": "#303235",
} "backgroundColor": "#FFFFFF",
"placeholderColor": "#9095A0"
},
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
},
"general": { "font": "Open Sans", "background": { "type": "None" } }
}, },
"settings": { "settings": {
"general": { "isBrandingEnabled": true },
"metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
},
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 } "typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
}, },
"publicId": null "publicId": null

View File

@ -1,7 +1,10 @@
import { Step, InputStepType } from 'models' import { Step } from 'models'
import { parseTestTypebot } from './utils' import { parseTestTypebot } from './utils'
export const userIds = ['user1', 'user2'] export const users = [
{ id: 'user1', email: 'test1@gmail.com' },
{ id: 'user2', email: 'test2@gmail.com' },
]
export const createTypebotWithStep = (step: Omit<Step, 'id' | 'blockId'>) => { export const createTypebotWithStep = (step: Omit<Step, 'id' | 'blockId'>) => {
cy.task( cy.task(
@ -9,21 +12,15 @@ export const createTypebotWithStep = (step: Omit<Step, 'id' | 'blockId'>) => {
parseTestTypebot({ parseTestTypebot({
id: 'typebot3', id: 'typebot3',
name: 'Typebot #3', name: 'Typebot #3',
ownerId: userIds[1], ownerId: users[1].id,
steps: { steps: {
byId: { byId: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
step1: { step1: {
...step, ...step,
id: 'step1', id: 'step1',
blockId: 'block1', blockId: 'block1',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
options:
step.type === InputStepType.CHOICE
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
{ itemIds: ['item1'] }
: undefined,
}, },
}, },
allIds: ['step1'], allIds: ['step1'],
@ -39,13 +36,10 @@ export const createTypebotWithStep = (step: Omit<Step, 'id' | 'blockId'>) => {
}, },
allIds: ['block1'], allIds: ['block1'],
}, },
choiceItems: choiceItems: {
step.type === InputStepType.CHOICE byId: { item1: { stepId: 'step1', id: 'item1' } },
? { allIds: ['item1'],
byId: { item1: { stepId: 'step1', id: 'item1' } }, },
allIds: ['item1'],
}
: undefined,
}) })
) )
} }

View File

@ -1,7 +1,13 @@
import { InputStepType, PublicTypebot, Typebot } from 'models' import {
defaultTextInputOptions,
InputStepType,
PublicTypebot,
TextInputStep,
Typebot,
} from 'models'
import { CredentialsType, Plan, PrismaClient } from 'db' import { CredentialsType, Plan, PrismaClient } from 'db'
import { parseTestTypebot } from './utils' import { parseTestTypebot } from './utils'
import { userIds } from './data' import { users } from './data'
const prisma = new PrismaClient() const prisma = new PrismaClient()
@ -23,10 +29,9 @@ export const createTypebot = (typebot: Typebot) =>
const createUsers = () => const createUsers = () =>
prisma.user.createMany({ prisma.user.createMany({
data: [ data: [
{ id: userIds[0], email: 'test1@gmail.com', emailVerified: new Date() }, { ...users[0], emailVerified: new Date() },
{ {
id: userIds[1], ...users[1],
email: 'test2@gmail.com',
emailVerified: new Date(), emailVerified: new Date(),
plan: Plan.PRO, plan: Plan.PRO,
stripeId: 'stripe-test2', stripeId: 'stripe-test2',
@ -39,7 +44,7 @@ const createCredentials = (refresh_token: string) =>
data: [ data: [
{ {
name: 'test2@gmail.com', name: 'test2@gmail.com',
ownerId: userIds[1], ownerId: users[1].id,
type: CredentialsType.GOOGLE_SHEETS, type: CredentialsType.GOOGLE_SHEETS,
data: { data: {
expiry_date: 1642441058842, expiry_date: 1642441058842,
@ -53,7 +58,7 @@ const createCredentials = (refresh_token: string) =>
const createFolders = () => const createFolders = () =>
prisma.dashboardFolder.createMany({ prisma.dashboardFolder.createMany({
data: [{ ownerId: userIds[1], name: 'Folder #1', id: 'folder1' }], data: [{ ownerId: users[1].id, name: 'Folder #1', id: 'folder1' }],
}) })
const createTypebots = async () => { const createTypebots = async () => {
@ -61,7 +66,7 @@ const createTypebots = async () => {
...parseTestTypebot({ ...parseTestTypebot({
id: 'typebot2', id: 'typebot2',
name: 'Typebot #2', name: 'Typebot #2',
ownerId: userIds[1], ownerId: users[1].id,
blocks: { blocks: {
byId: { byId: {
block1: { block1: {
@ -79,7 +84,8 @@ const createTypebots = async () => {
id: 'step1', id: 'step1',
type: InputStepType.TEXT, type: InputStepType.TEXT,
blockId: 'block1', blockId: 'block1',
}, options: defaultTextInputOptions,
} as TextInputStep,
}, },
allIds: ['step1'], allIds: ['step1'],
}, },
@ -91,7 +97,7 @@ const createTypebots = async () => {
...parseTestTypebot({ ...parseTestTypebot({
id: 'typebot1', id: 'typebot1',
name: 'Typebot #1', name: 'Typebot #1',
ownerId: userIds[1], ownerId: users[1].id,
blocks: { byId: {}, allIds: [] }, blocks: { byId: {}, allIds: [] },
steps: { byId: {}, allIds: [] }, steps: { byId: {}, allIds: [] },
}), }),
@ -115,6 +121,7 @@ const createResults = () => {
createdAt: new Date( createdAt: new Date(
today.setTime(today.getTime() + 1000 * 60 * 60 * 24 * idx) today.setTime(today.getTime() + 1000 * 60 * 60 * 24 * idx)
), ),
isCompleted: false,
} }
}), }),
], ],
@ -153,5 +160,5 @@ const parseTypebotToPublicTypebot = (
export const loadRawTypebotInDatabase = (typebot: Typebot) => export const loadRawTypebotInDatabase = (typebot: Typebot) =>
prisma.typebot.create({ prisma.typebot.create({
data: { ...typebot, id: 'typebot4', ownerId: userIds[1] } as any, data: { ...typebot, id: 'typebot4', ownerId: users[1].id } as any,
}) })

View File

@ -1,12 +1,11 @@
import { import {
Block, Block,
Theme,
BackgroundType,
Settings,
Typebot, Typebot,
Table, Table,
Step, Step,
ChoiceItem, ChoiceItem,
defaultTheme,
defaultSettings,
} from 'models' } from 'models'
export const parseTestTypebot = ({ export const parseTestTypebot = ({
@ -24,26 +23,13 @@ export const parseTestTypebot = ({
steps: Table<Step> steps: Table<Step>
choiceItems?: Table<ChoiceItem> choiceItems?: Table<ChoiceItem>
}): Typebot => { }): Typebot => {
const theme: Theme = {
general: {
font: 'Open Sans',
background: { type: BackgroundType.NONE, content: '#ffffff' },
},
}
const settings: Settings = {
typingEmulation: {
enabled: true,
speed: 300,
maxDelay: 1.5,
},
}
return { return {
id, id,
folderId: null, folderId: null,
name, name,
ownerId, ownerId,
theme, theme: defaultTheme,
settings, settings: defaultSettings,
createdAt: new Date(), createdAt: new Date(),
blocks: { blocks: {
byId: { byId: {

View File

@ -40,12 +40,25 @@ declare global {
} }
} }
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/
Cypress.on('uncaught:exception', (err) => { Cypress.on('uncaught:exception', (err) => {
if (err.message.includes('ResizeObserver loop limit exceeded')) { if (resizeObserverLoopErrRe.test(err.message)) {
return false return false
} }
}) })
export const prepareDbAndSignIn = () => {
cy.signOut()
cy.task('seed')
cy.signIn(users[1].email)
}
export const removePreventReload = () => {
cy.window().then((win) => {
win.removeEventListener('beforeunload', preventUserFromRefreshing)
})
}
export const getIframeBody = () => { export const getIframeBody = () => {
return cy return cy
.get('#typebot-iframe') .get('#typebot-iframe')
@ -57,6 +70,8 @@ export const getIframeBody = () => {
// Import commands.js using ES2015 syntax: // Import commands.js using ES2015 syntax:
import '@testing-library/cypress/add-commands' import '@testing-library/cypress/add-commands'
import 'cypress-file-upload' import 'cypress-file-upload'
import { users } from 'cypress/plugins/data'
import { preventUserFromRefreshing } from 'cypress/plugins/utils'
import './commands' import './commands'
// Alternatively you can use CommonJS syntax: // Alternatively you can use CommonJS syntax:

View File

@ -1,4 +1,5 @@
import { userIds } from 'cypress/plugins/data' import { users } from 'cypress/plugins/data'
import { prepareDbAndSignIn, removePreventReload } from 'cypress/support'
describe('Account page', () => { describe('Account page', () => {
before(() => { before(() => {
@ -11,13 +12,12 @@ describe('Account page', () => {
) )
}) })
beforeEach(() => { beforeEach(prepareDbAndSignIn)
cy.task('seed')
cy.signOut() afterEach(removePreventReload)
})
it('should edit user info properly', () => { it('should edit user info properly', () => {
cy.signIn('test1@gmail.com') cy.signIn(users[0].email)
cy.visit('/account') cy.visit('/account')
cy.findByRole('button', { name: 'Save' }).should('not.exist') cy.findByRole('button', { name: 'Save' }).should('not.exist')
cy.findByRole('textbox', { name: 'Email address' }).should( cy.findByRole('textbox', { name: 'Email address' }).should(
@ -35,23 +35,19 @@ describe('Account page', () => {
.should('have.attr', 'src') .should('have.attr', 'src')
.should( .should(
'include', 'include',
`https://s3.eu-west-3.amazonaws.com/typebot/users/${userIds[0]}/avatar` `https://s3.eu-west-3.amazonaws.com/typebot/users/${users[0].id}/avatar`
) )
cy.findByRole('button', { name: 'Save' }).should('exist').click() cy.findByRole('button', { name: 'Save' }).should('exist').click()
cy.wait('@getUpdatedSession') cy.wait('@getUpdatedSession')
cy.reload() .then((interception) => {
cy.findByRole('textbox', { name: 'Name' }).should('have.value', 'John Doe') return interception.response?.statusCode
cy.findByRole('img') })
.should('have.attr', 'src') .should('eq', 200)
.should(
'include',
`https://s3.eu-west-3.amazonaws.com/typebot/users/${userIds[0]}/avatar`
)
cy.findByRole('button', { name: 'Save' }).should('not.exist') cy.findByRole('button', { name: 'Save' }).should('not.exist')
}) })
it('should display valid plans', () => { it('should display valid plans', () => {
cy.signIn('test1@gmail.com') cy.signIn(users[0].email)
cy.visit('/account') cy.visit('/account')
cy.findByText('Free plan').should('exist') cy.findByText('Free plan').should('exist')
cy.findByRole('link', { name: 'Manage my subscription' }).should( cy.findByRole('link', { name: 'Manage my subscription' }).should(
@ -59,7 +55,7 @@ describe('Account page', () => {
) )
cy.findByRole('button', { name: 'Upgrade' }).should('exist') cy.findByRole('button', { name: 'Upgrade' }).should('exist')
cy.signOut() cy.signOut()
cy.signIn('test2@gmail.com') cy.signIn(users[1].email)
cy.visit('/account') cy.visit('/account')
cy.findByText('Pro plan').should('exist') cy.findByText('Pro plan').should('exist')
cy.findByRole('link', { name: 'Manage my subscription' }) cy.findByRole('link', { name: 'Manage my subscription' })

View File

@ -1,7 +1,10 @@
import { createTypebotWithStep } from 'cypress/plugins/data' import { createTypebotWithStep } from 'cypress/plugins/data'
import { preventUserFromRefreshing } from 'cypress/plugins/utils' import {
import { getIframeBody } from 'cypress/support' getIframeBody,
import { BubbleStepType, Step } from 'models' prepareDbAndSignIn,
removePreventReload,
} from 'cypress/support'
import { BubbleStepType, defaultImageBubbleContent, Step } from 'models'
const unsplashImageSrc = const unsplashImageSrc =
'https://images.unsplash.com/photo-1504297050568-910d24c426d3?ixlib=rb-1.2.1&ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80' 'https://images.unsplash.com/photo-1504297050568-910d24c426d3?ixlib=rb-1.2.1&ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80'
@ -13,19 +16,14 @@ describe('Image bubbles', () => {
method: 'POST', method: 'POST',
}).as('postImage') }).as('postImage')
}) })
afterEach(() => {
cy.window().then((win) => {
win.removeEventListener('beforeunload', preventUserFromRefreshing)
})
})
describe('Content settings', () => { describe('Content settings', () => {
beforeEach(() => { beforeEach(() => {
cy.task('seed') prepareDbAndSignIn()
createTypebotWithStep({ createTypebotWithStep({
type: BubbleStepType.IMAGE, type: BubbleStepType.IMAGE,
} as Omit<Step, 'id' | 'blockId'>) content: defaultImageBubbleContent,
cy.signOut() } as Step)
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit') cy.visit('/typebots/typebot3/edit')
cy.findByText('Click to edit...').click() cy.findByText('Click to edit...').click()
}) })
@ -46,7 +44,6 @@ describe('Image bubbles', () => {
cy.findByPlaceholderText('Paste the image link...') cy.findByPlaceholderText('Paste the image link...')
.clear() .clear()
.type(unsplashImageSrc) .type(unsplashImageSrc)
cy.findByRole('button', { name: 'Embed image' }).click()
cy.findByRole('img') cy.findByRole('img')
.should('have.attr', 'src') .should('have.attr', 'src')
.should('include', unsplashImageSrc) .should('include', unsplashImageSrc)
@ -62,20 +59,18 @@ describe('Image bubbles', () => {
}) })
}) })
describe('Preview', () => { describe('Preview', () => {
beforeEach(() => { before(() => {
cy.task('seed') prepareDbAndSignIn()
createTypebotWithStep({ createTypebotWithStep({
type: BubbleStepType.IMAGE, type: BubbleStepType.IMAGE,
content: { content: {
url: unsplashImageSrc, url: unsplashImageSrc,
}, },
} as Omit<Step, 'id' | 'blockId'>) } as Omit<Step, 'id' | 'blockId'>)
cy.signOut()
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit')
}) })
it('should display correctly', () => { it('should display correctly', () => {
cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click() cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody() getIframeBody()
.findByRole('img') .findByRole('img')

View File

@ -1,27 +1,23 @@
import { createTypebotWithStep } from 'cypress/plugins/data' import { createTypebotWithStep } from 'cypress/plugins/data'
import { preventUserFromRefreshing } from 'cypress/plugins/utils' import {
import { getIframeBody } from 'cypress/support' getIframeBody,
import { BubbleStepType, Step } from 'models' prepareDbAndSignIn,
removePreventReload,
} from 'cypress/support'
import { BubbleStepType, defaultTextBubbleContent, Step } from 'models'
describe('Text bubbles', () => { describe('Text bubbles', () => {
beforeEach(() => { beforeEach(() => {
cy.task('seed') prepareDbAndSignIn()
createTypebotWithStep({ createTypebotWithStep({
type: BubbleStepType.TEXT, type: BubbleStepType.TEXT,
content: { html: '', plainText: '', richText: [] }, content: defaultTextBubbleContent,
} as Omit<Step, 'id' | 'blockId'>) } as Omit<Step, 'id' | 'blockId'>)
cy.signOut() cy.visit('/typebots/typebot3/edit')
})
afterEach(() => {
cy.window().then((win) => {
win.removeEventListener('beforeunload', preventUserFromRefreshing)
})
}) })
afterEach(removePreventReload)
it('rich text features should work', () => { it('rich text features should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit')
cy.findByTestId('bold-button').click() cy.findByTestId('bold-button').click()
cy.findByRole('textbox', { name: 'Text editor' }).type('Bold text{enter}') cy.findByRole('textbox', { name: 'Text editor' }).type('Bold text{enter}')
cy.findByTestId('bold-button').click() cy.findByTestId('bold-button').click()

View File

@ -1,7 +1,15 @@
import { createTypebotWithStep } from 'cypress/plugins/data' import { createTypebotWithStep } from 'cypress/plugins/data'
import { preventUserFromRefreshing } from 'cypress/plugins/utils' import {
import { getIframeBody } from 'cypress/support' getIframeBody,
import { BubbleStepType, Step, VideoBubbleContentType } from 'models' prepareDbAndSignIn,
removePreventReload,
} from 'cypress/support'
import {
BubbleStepType,
defaultVideoBubbleContent,
Step,
VideoBubbleContentType,
} from 'models'
const videoSrc = const videoSrc =
'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4' 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4'
@ -9,22 +17,17 @@ const youtubeVideoSrc = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
const vimeoVideoSrc = 'https://vimeo.com/649301125' const vimeoVideoSrc = 'https://vimeo.com/649301125'
describe('Video bubbles', () => { describe('Video bubbles', () => {
afterEach(() => {
cy.window().then((win) => {
win.removeEventListener('beforeunload', preventUserFromRefreshing)
})
})
describe('Content settings', () => { describe('Content settings', () => {
beforeEach(() => { beforeEach(() => {
cy.task('seed') prepareDbAndSignIn()
createTypebotWithStep({ createTypebotWithStep({
type: BubbleStepType.VIDEO, type: BubbleStepType.VIDEO,
content: defaultVideoBubbleContent,
} as Omit<Step, 'id' | 'blockId'>) } as Omit<Step, 'id' | 'blockId'>)
cy.signOut()
}) })
afterEach(removePreventReload)
it('upload image file correctly', () => { it('upload image file correctly', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit') cy.visit('/typebots/typebot3/edit')
cy.findByText('Click to edit...').click() cy.findByText('Click to edit...').click()
cy.findByPlaceholderText('Paste the video link...').type(videoSrc, { cy.findByPlaceholderText('Paste the video link...').type(videoSrc, {
@ -53,10 +56,8 @@ describe('Video bubbles', () => {
}) })
describe('Preview', () => { describe('Preview', () => {
beforeEach(() => { beforeEach(prepareDbAndSignIn)
cy.task('seed') afterEach(removePreventReload)
cy.signOut()
})
it('should display video correctly', () => { it('should display video correctly', () => {
createTypebotWithStep({ createTypebotWithStep({
@ -66,7 +67,6 @@ describe('Video bubbles', () => {
url: videoSrc, url: videoSrc,
}, },
} as Omit<Step, 'id' | 'blockId'>) } as Omit<Step, 'id' | 'blockId'>)
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit') cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click() cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody() getIframeBody()
@ -84,7 +84,6 @@ describe('Video bubbles', () => {
id: 'dQw4w9WgXcQ', id: 'dQw4w9WgXcQ',
}, },
} as Omit<Step, 'id' | 'blockId'>) } as Omit<Step, 'id' | 'blockId'>)
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit') cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click() cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody() getIframeBody()
@ -103,7 +102,6 @@ describe('Video bubbles', () => {
id: '649301125', id: '649301125',
}, },
} as Omit<Step, 'id' | 'blockId'>) } as Omit<Step, 'id' | 'blockId'>)
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit') cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click() cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody() getIframeBody()

View File

@ -1,11 +1,13 @@
import { users } from 'cypress/plugins/data'
import { prepareDbAndSignIn, removePreventReload } from 'cypress/support'
describe('Dashboard page', () => { describe('Dashboard page', () => {
beforeEach(() => { beforeEach(prepareDbAndSignIn)
cy.task('seed')
cy.signOut() afterEach(removePreventReload)
})
it('folders navigation should work', () => { it('folders navigation should work', () => {
cy.signIn('test1@gmail.com') cy.signIn(users[0].email)
cy.visit('/typebots') cy.visit('/typebots')
createFolder('My folder #1') createFolder('My folder #1')
cy.findByTestId('folder-button').click() cy.findByTestId('folder-button').click()
@ -27,7 +29,6 @@ describe('Dashboard page', () => {
}) })
it('folders and typebots should be deletable', () => { it('folders and typebots should be deletable', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots') cy.visit('/typebots')
cy.findByText('Folder #1').should('exist') cy.findByText('Folder #1').should('exist')
cy.findAllByRole('button', { name: 'Show folder menu' }).first().click() cy.findAllByRole('button', { name: 'Show folder menu' }).first().click()
@ -42,7 +43,6 @@ describe('Dashboard page', () => {
}) })
it('folders should be draggable and droppable', () => { it('folders should be draggable and droppable', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots') cy.visit('/typebots')
cy.findByTestId('typebot-button-typebot1').mouseMoveBy(-100, 0, { cy.findByTestId('typebot-button-typebot1').mouseMoveBy(-100, 0, {
delay: 120, delay: 120,

View File

@ -1,16 +1,22 @@
import { createTypebotWithStep } from 'cypress/plugins/data' import { createTypebotWithStep } from 'cypress/plugins/data'
import { getIframeBody } from 'cypress/support' import {
import { InputStepType } from 'models' getIframeBody,
prepareDbAndSignIn,
removePreventReload,
} from 'cypress/support'
import { defaultChoiceInputOptions, InputStepType, Step } from 'models'
describe('Button input', () => { describe('Button input', () => {
beforeEach(() => { beforeEach(() => {
cy.task('seed') prepareDbAndSignIn()
createTypebotWithStep({ type: InputStepType.CHOICE }) createTypebotWithStep({
cy.signOut() type: InputStepType.CHOICE,
options: { ...defaultChoiceInputOptions, itemIds: ['item1'] },
} as Step)
}) })
afterEach(removePreventReload)
it('Can edit choice items', () => { it('Can edit choice items', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit') cy.visit('/typebots/typebot3/edit')
cy.findByDisplayValue('Click to edit').type('Item 1{enter}') cy.findByDisplayValue('Click to edit').type('Item 1{enter}')
cy.findByText('Item 1').trigger('mouseover') cy.findByText('Item 1').trigger('mouseover')
@ -46,7 +52,6 @@ describe('Button input', () => {
it('Single choice targets should work', () => { it('Single choice targets should work', () => {
cy.loadTypebotFixtureInDatabase('typebots/singleChoiceTarget.json') cy.loadTypebotFixtureInDatabase('typebots/singleChoiceTarget.json')
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/edit') cy.visit('/typebots/typebot4/edit')
cy.findByRole('button', { name: 'Preview' }).click() cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody().findByRole('button', { name: 'Burgers' }).click() getIframeBody().findByRole('button', { name: 'Burgers' }).click()

View File

@ -1,16 +1,22 @@
import { createTypebotWithStep } from 'cypress/plugins/data' import { createTypebotWithStep } from 'cypress/plugins/data'
import { getIframeBody } from 'cypress/support' import {
import { InputStepType } from 'models' getIframeBody,
prepareDbAndSignIn,
removePreventReload,
} from 'cypress/support'
import { defaultDateInputOptions, InputStepType, Step } from 'models'
describe('Date input', () => { describe('Date input', () => {
beforeEach(() => { beforeEach(() => {
cy.task('seed') prepareDbAndSignIn()
createTypebotWithStep({ type: InputStepType.DATE }) createTypebotWithStep({
cy.signOut() type: InputStepType.DATE,
options: defaultDateInputOptions,
} as Step)
}) })
afterEach(removePreventReload)
it('options should work', () => { it('options should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit') cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click() cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody() getIframeBody()

View File

@ -1,16 +1,22 @@
import { createTypebotWithStep } from 'cypress/plugins/data' import { createTypebotWithStep } from 'cypress/plugins/data'
import { getIframeBody } from 'cypress/support' import {
import { InputStepType } from 'models' getIframeBody,
prepareDbAndSignIn,
removePreventReload,
} from 'cypress/support'
import { defaultEmailInputOptions, InputStepType, Step } from 'models'
describe('Email input', () => { describe('Email input', () => {
beforeEach(() => { beforeEach(() => {
cy.task('seed') prepareDbAndSignIn()
createTypebotWithStep({ type: InputStepType.EMAIL }) createTypebotWithStep({
cy.signOut() type: InputStepType.EMAIL,
options: defaultEmailInputOptions,
} as Step)
}) })
afterEach(removePreventReload)
it('options should work', () => { it('options should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit') cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click() cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody() getIframeBody()
@ -18,7 +24,9 @@ describe('Email input', () => {
.should('have.attr', 'type') .should('have.attr', 'type')
.should('equal', 'email') .should('equal', 'email')
getIframeBody().findByRole('button', { name: 'Send' }) getIframeBody().findByRole('button', { name: 'Send' })
getIframeBody().findByPlaceholderText('Type your email...').should('exist') getIframeBody()
.findByPlaceholderText(defaultEmailInputOptions.labels.placeholder)
.should('exist')
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled') getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')
cy.findByTestId('step-step1').click({ force: true }) cy.findByTestId('step-step1').click({ force: true })
cy.findByRole('textbox', { name: 'Placeholder:' }) cy.findByRole('textbox', { name: 'Placeholder:' })

View File

@ -1,20 +1,26 @@
import { createTypebotWithStep } from 'cypress/plugins/data' import { createTypebotWithStep } from 'cypress/plugins/data'
import { getIframeBody } from 'cypress/support' import {
import { InputStepType } from 'models' getIframeBody,
prepareDbAndSignIn,
removePreventReload,
} from 'cypress/support'
import { defaultNumberInputOptions, InputStepType, Step } from 'models'
describe('Number input', () => { describe('Number input', () => {
beforeEach(() => { beforeEach(() => {
cy.task('seed') prepareDbAndSignIn()
createTypebotWithStep({ type: InputStepType.NUMBER }) createTypebotWithStep({
cy.signOut() type: InputStepType.NUMBER,
options: defaultNumberInputOptions,
} as Step)
}) })
afterEach(removePreventReload)
it('options should work', () => { it('options should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit') cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click() cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody() getIframeBody()
.findByPlaceholderText('Type your answer...') .findByPlaceholderText(defaultNumberInputOptions.labels.placeholder)
.should('have.attr', 'type') .should('have.attr', 'type')
.should('equal', 'number') .should('equal', 'number')
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled') getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')

View File

@ -1,20 +1,26 @@
import { createTypebotWithStep } from 'cypress/plugins/data' import { createTypebotWithStep } from 'cypress/plugins/data'
import { getIframeBody } from 'cypress/support' import {
import { InputStepType } from 'models' getIframeBody,
prepareDbAndSignIn,
removePreventReload,
} from 'cypress/support'
import { defaultPhoneInputOptions, InputStepType, Step } from 'models'
describe('Phone number input', () => { describe('Phone number input', () => {
beforeEach(() => { beforeEach(() => {
cy.task('seed') prepareDbAndSignIn()
createTypebotWithStep({ type: InputStepType.PHONE }) createTypebotWithStep({
cy.signOut() type: InputStepType.PHONE,
options: defaultPhoneInputOptions,
} as Step)
}) })
afterEach(removePreventReload)
it('options should work', () => { it('options should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit') cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click() cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody() getIframeBody()
.findByPlaceholderText('Your phone number...') .findByPlaceholderText(defaultPhoneInputOptions.labels.placeholder)
.should('have.attr', 'type') .should('have.attr', 'type')
.should('eq', 'tel') .should('eq', 'tel')
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled') getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')

View File

@ -1,27 +1,26 @@
import { createTypebotWithStep } from 'cypress/plugins/data' import { createTypebotWithStep } from 'cypress/plugins/data'
import { preventUserFromRefreshing } from 'cypress/plugins/utils' import {
import { getIframeBody } from 'cypress/support' getIframeBody,
import { InputStepType } from 'models' prepareDbAndSignIn,
removePreventReload,
} from 'cypress/support'
import { defaultTextInputOptions, InputStepType, Step } from 'models'
describe('Text input', () => { describe('Text input', () => {
beforeEach(() => { beforeEach(() => {
cy.task('seed') prepareDbAndSignIn()
createTypebotWithStep({ type: InputStepType.TEXT }) createTypebotWithStep({
cy.signOut() type: InputStepType.TEXT,
}) options: defaultTextInputOptions,
} as Step)
afterEach(() => {
cy.window().then((win) => {
win.removeEventListener('beforeunload', preventUserFromRefreshing)
})
}) })
afterEach(removePreventReload)
it('options should work', () => { it('options should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit') cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click() cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody() getIframeBody()
.findByPlaceholderText('Type your answer...') .findByPlaceholderText(defaultTextInputOptions.labels.placeholder)
.should('have.attr', 'type') .should('have.attr', 'type')
.should('equal', 'text') .should('equal', 'text')
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled') getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')

View File

@ -1,20 +1,27 @@
import { createTypebotWithStep } from 'cypress/plugins/data' import { createTypebotWithStep } from 'cypress/plugins/data'
import { getIframeBody } from 'cypress/support' import {
import { InputStepType } from 'models' getIframeBody,
prepareDbAndSignIn,
removePreventReload,
} from 'cypress/support'
import { defaultUrlInputOptions, InputStepType, Step } from 'models'
describe('URL input', () => { describe('URL input', () => {
afterEach(removePreventReload)
beforeEach(() => { beforeEach(() => {
cy.task('seed') prepareDbAndSignIn()
createTypebotWithStep({ type: InputStepType.URL }) createTypebotWithStep({
cy.signOut() type: InputStepType.URL,
options: defaultUrlInputOptions,
} as Step)
}) })
it('options should work', () => { it('options should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit') cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click() cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody() getIframeBody()
.findByPlaceholderText('Type your URL...') .findByPlaceholderText(defaultUrlInputOptions.labels.placeholder)
.should('have.attr', 'type') .should('have.attr', 'type')
.should('eq', 'url') .should('eq', 'url')
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled') getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')

View File

@ -1,22 +1,23 @@
import { createTypebotWithStep } from 'cypress/plugins/data' import { createTypebotWithStep } from 'cypress/plugins/data'
import { preventUserFromRefreshing } from 'cypress/plugins/utils' import { prepareDbAndSignIn, removePreventReload } from 'cypress/support'
import { IntegrationStepType } from 'models' import {
defaultGoogleAnalyticsOptions,
IntegrationStepType,
Step,
} from 'models'
describe('Google Analytics', () => { describe('Google Analytics', () => {
beforeEach(() => { afterEach(removePreventReload)
cy.task('seed')
createTypebotWithStep({ type: IntegrationStepType.GOOGLE_ANALYTICS })
cy.signOut()
})
afterEach(() => { beforeEach(() => {
cy.window().then((win) => { prepareDbAndSignIn()
win.removeEventListener('beforeunload', preventUserFromRefreshing) createTypebotWithStep({
}) type: IntegrationStepType.GOOGLE_ANALYTICS,
options: defaultGoogleAnalyticsOptions,
} as Step)
}) })
it('can be filled correctly', () => { it('can be filled correctly', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit') cy.visit('/typebots/typebot3/edit')
cy.intercept({ cy.intercept({
url: '/g/collect', url: '/g/collect',

View File

@ -1,16 +1,13 @@
import { preventUserFromRefreshing } from 'cypress/plugins/utils' import { users } from 'cypress/plugins/data'
import { getIframeBody } from 'cypress/support' import { getIframeBody, removePreventReload } from 'cypress/support'
describe('Google sheets', () => { describe('Google sheets', () => {
beforeEach(() => { afterEach(removePreventReload)
cy.task('seed', Cypress.env('GOOGLE_SHEETS_REFRESH_TOKEN'))
cy.signOut()
})
afterEach(() => { beforeEach(() => {
cy.window().then((win) => { cy.signOut()
win.removeEventListener('beforeunload', preventUserFromRefreshing) cy.task('seed', Cypress.env('GOOGLE_SHEETS_REFRESH_TOKEN'))
}) cy.signIn(users[1].email)
}) })
it('Insert row should work', () => { it('Insert row should work', () => {
@ -19,7 +16,6 @@ describe('Google sheets', () => {
method: 'POST', method: 'POST',
}).as('insertRowInGoogleSheets') }).as('insertRowInGoogleSheets')
cy.loadTypebotFixtureInDatabase('typebots/integrations/googleSheets.json') cy.loadTypebotFixtureInDatabase('typebots/integrations/googleSheets.json')
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/edit') cy.visit('/typebots/typebot4/edit')
fillInSpreadsheetInfo() fillInSpreadsheetInfo()
@ -55,7 +51,6 @@ describe('Google sheets', () => {
method: 'PATCH', method: 'PATCH',
}).as('updateRowInGoogleSheets') }).as('updateRowInGoogleSheets')
cy.loadTypebotFixtureInDatabase('typebots/integrations/googleSheets.json') cy.loadTypebotFixtureInDatabase('typebots/integrations/googleSheets.json')
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/edit') cy.visit('/typebots/typebot4/edit')
fillInSpreadsheetInfo() fillInSpreadsheetInfo()
@ -87,7 +82,6 @@ describe('Google sheets', () => {
cy.loadTypebotFixtureInDatabase( cy.loadTypebotFixtureInDatabase(
'typebots/integrations/googleSheetsGet.json' 'typebots/integrations/googleSheetsGet.json'
) )
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/edit') cy.visit('/typebots/typebot4/edit')
fillInSpreadsheetInfo() fillInSpreadsheetInfo()

View File

@ -1,22 +1,17 @@
import { preventUserFromRefreshing } from 'cypress/plugins/utils' import {
import { getIframeBody } from 'cypress/support' getIframeBody,
prepareDbAndSignIn,
removePreventReload,
} from 'cypress/support'
describe('Webhook step', () => { describe('Webhook step', () => {
beforeEach(() => { beforeEach(prepareDbAndSignIn)
cy.task('seed')
cy.signOut()
})
afterEach(() => { afterEach(removePreventReload)
cy.window().then((win) => {
win.removeEventListener('beforeunload', preventUserFromRefreshing)
})
})
describe('Configuration', () => { describe('Configuration', () => {
it('configuration is working', () => { it('configuration is working', () => {
cy.loadTypebotFixtureInDatabase('typebots/integrations/webhook.json') cy.loadTypebotFixtureInDatabase('typebots/integrations/webhook.json')
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/edit') cy.visit('/typebots/typebot4/edit')
cy.findByText('Configure...').click() cy.findByText('Configure...').click()
cy.findByRole('button', { name: 'GET' }).click() cy.findByRole('button', { name: 'GET' }).click()
@ -80,7 +75,6 @@ describe('Webhook step', () => {
cy.loadTypebotFixtureInDatabase( cy.loadTypebotFixtureInDatabase(
'typebots/integrations/webhookPreview.json' 'typebots/integrations/webhookPreview.json'
) )
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/edit') cy.visit('/typebots/typebot4/edit')
cy.findByRole('button', { name: 'Preview' }).click() cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody().findByRole('button', { name: 'Go' }).click() getIframeBody().findByRole('button', { name: 'Go' }).click()

View File

@ -1,21 +1,16 @@
import { preventUserFromRefreshing } from 'cypress/plugins/utils' import {
import { getIframeBody } from 'cypress/support' getIframeBody,
prepareDbAndSignIn,
removePreventReload,
} from 'cypress/support'
describe('Condition step', () => { describe('Condition step', () => {
beforeEach(() => { beforeEach(prepareDbAndSignIn)
cy.task('seed')
cy.signOut()
})
afterEach(() => { afterEach(removePreventReload)
cy.window().then((win) => {
win.removeEventListener('beforeunload', preventUserFromRefreshing)
})
})
it('options should work', () => { it('options should work', () => {
cy.loadTypebotFixtureInDatabase('typebots/logic/condition.json') cy.loadTypebotFixtureInDatabase('typebots/logic/condition.json')
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/edit') cy.visit('/typebots/typebot4/edit')
cy.findAllByText('Equal to').first().click() cy.findAllByText('Equal to').first().click()

View File

@ -1,21 +1,16 @@
import { preventUserFromRefreshing } from 'cypress/plugins/utils' import {
import { getIframeBody } from 'cypress/support' getIframeBody,
prepareDbAndSignIn,
removePreventReload,
} from 'cypress/support'
describe('Redirect', () => { describe('Redirect', () => {
beforeEach(() => { beforeEach(prepareDbAndSignIn)
cy.task('seed')
cy.signOut()
})
afterEach(() => { afterEach(removePreventReload)
cy.window().then((win) => {
win.removeEventListener('beforeunload', preventUserFromRefreshing)
})
})
it('should redirect to URL correctly', () => { it('should redirect to URL correctly', () => {
cy.loadTypebotFixtureInDatabase('typebots/logic/redirect.json') cy.loadTypebotFixtureInDatabase('typebots/logic/redirect.json')
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/edit') cy.visit('/typebots/typebot4/edit')
cy.findByText('Configure...').click() cy.findByText('Configure...').click()
cy.findByPlaceholderText('Type a URL...').type('google.com') cy.findByPlaceholderText('Type a URL...').type('google.com')

View File

@ -1,23 +1,18 @@
import { preventUserFromRefreshing } from 'cypress/plugins/utils' import {
import { getIframeBody } from 'cypress/support' getIframeBody,
prepareDbAndSignIn,
removePreventReload,
} from 'cypress/support'
describe('Set variables', () => { describe('Set variables', () => {
beforeEach(() => { beforeEach(prepareDbAndSignIn)
cy.task('seed')
cy.signOut()
})
afterEach(() => { afterEach(removePreventReload)
cy.window().then((win) => {
win.removeEventListener('beforeunload', preventUserFromRefreshing)
})
})
it('options should work', () => { it.only('options should work', () => {
cy.loadTypebotFixtureInDatabase('typebots/logic/setVariable.json') cy.loadTypebotFixtureInDatabase('typebots/logic/setVariable.json')
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/edit') cy.visit('/typebots/typebot4/edit')
cy.findByText('Type your answer...').click() cy.findByText('Type a number...').click()
cy.createVariable('Num') cy.createVariable('Num')
cy.findAllByText('Click to edit...').first().click() cy.findAllByText('Click to edit...').first().click()
cy.createVariable('Total') cy.createVariable('Total')
@ -31,9 +26,7 @@ describe('Set variables', () => {
'Custom value' 'Custom value'
) )
cy.findByRole('button', { name: 'Preview' }).click() cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody() getIframeBody().findByPlaceholderText('Type a number...').type('365{enter}')
.findByPlaceholderText('Type your answer...')
.type('365{enter}')
getIframeBody().findByText('Total: 365000').should('exist') getIframeBody().findByText('Total: 365000').should('exist')
getIframeBody().findByText('Custom var: Custom value') getIframeBody().findByText('Custom var: Custom value')
}) })

View File

@ -1,19 +1,21 @@
import path from 'path' import path from 'path'
import { parse } from 'papaparse' import { parse } from 'papaparse'
import { prepareDbAndSignIn, removePreventReload } from 'cypress/support'
const downloadsFolder = Cypress.config('downloadsFolder')
describe('Results page', () => { describe('Results page', () => {
beforeEach(() => { beforeEach(() => {
prepareDbAndSignIn()
cy.intercept({ url: '/api/typebots/typebot2/results*', method: 'GET' }).as( cy.intercept({ url: '/api/typebots/typebot2/results*', method: 'GET' }).as(
'getResults' 'getResults'
) )
cy.task('seed')
cy.signOut()
}) })
afterEach(removePreventReload)
it('results should be deletable', () => { it('results should be deletable', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot2/results') cy.visit('/typebots/typebot2/results')
cy.wait('@getResults')
cy.findByText('content198').should('exist') cy.findByText('content198').should('exist')
cy.findByText('content197').should('exist') cy.findByText('content197').should('exist')
cy.findAllByRole('checkbox').eq(2).check({ force: true }) cy.findAllByRole('checkbox').eq(2).check({ force: true })
@ -30,7 +32,6 @@ describe('Results page', () => {
}) })
it('submissions table should have infinite scroll', () => { it('submissions table should have infinite scroll', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot2/results') cy.visit('/typebots/typebot2/results')
cy.findByText('content50').should('not.exist') cy.findByText('content50').should('not.exist')
cy.findByText('content199').should('exist') cy.findByText('content199').should('exist')
@ -44,8 +45,6 @@ describe('Results page', () => {
}) })
it('should correctly export selection in CSV', () => { it('should correctly export selection in CSV', () => {
const downloadsFolder = Cypress.config('downloadsFolder')
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot2/results') cy.visit('/typebots/typebot2/results')
cy.wait('@getResults') cy.wait('@getResults')
cy.findByRole('button', { name: 'Export' }).should('not.exist') cy.findByRole('button', { name: 'Export' }).should('not.exist')

View File

@ -0,0 +1,27 @@
import {
getIframeBody,
prepareDbAndSignIn,
removePreventReload,
} from 'cypress/support'
describe('General settings', () => {
beforeEach(prepareDbAndSignIn)
afterEach(removePreventReload)
it('should reflect changes in real time', () => {
cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json')
cy.visit('/typebots/typebot4/settings')
getIframeBody()
.findByRole('link', { name: 'Made with Typebot.' })
.should('have.attr', 'href')
.should('eq', 'https://www.typebot.io/?utm_source=litebadge')
cy.findByRole('button', { name: 'General' }).click()
cy.findByRole('checkbox', { name: 'Typebot.io branding' }).uncheck({
force: true,
})
getIframeBody()
.findByRole('link', { name: 'Made with Typebot.' })
.should('not.exist')
})
})

View File

@ -0,0 +1,51 @@
import { prepareDbAndSignIn, removePreventReload } from 'cypress/support'
const favIconUrl = 'https://www.baptistearno.com/favicon.png'
const imageUrl = 'https://www.baptistearno.com/images/site-preview.png'
describe('Typing emulation', () => {
beforeEach(prepareDbAndSignIn)
afterEach(removePreventReload)
it('should reflect changes in real time', () => {
cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json')
cy.visit('/typebots/typebot4/settings')
cy.findByRole('button', { name: 'Metadata' }).click()
// Fav icon
cy.findAllByRole('img', { name: 'Fav icon' })
.click()
.should('have.attr', 'src')
.should('eq', '/favicon.png')
cy.findByRole('button', { name: 'Giphy' }).should('not.exist')
cy.findByRole('button', { name: 'Embed link' }).click()
cy.findByPlaceholderText('Paste the image link...').type(favIconUrl)
cy.findAllByRole('img', { name: 'Fav icon' })
.should('have.attr', 'src')
.should('eq', favIconUrl)
// Image
cy.findAllByRole('img', { name: 'Website image' })
.click()
.should('have.attr', 'src')
.should('eq', '/viewer-preview.png')
cy.findByRole('button', { name: 'Giphy' }).should('not.exist')
cy.findByRole('button', { name: 'Embed link' }).click()
cy.findByPlaceholderText('Paste the image link...').type(imageUrl)
cy.findAllByRole('img', { name: 'Website image' })
.should('have.attr', 'src')
.should('eq', imageUrl)
// Title
cy.findByRole('textbox', { name: 'Title:' })
.click({ force: true })
.clear()
.type('Awesome typebot')
// Description
cy.findByRole('textbox', { name: 'Description:' })
.clear()
.type('Lorem ipsum')
})
})

View File

@ -0,0 +1,20 @@
import { prepareDbAndSignIn, removePreventReload } from 'cypress/support'
describe('Typing emulation', () => {
beforeEach(prepareDbAndSignIn)
afterEach(removePreventReload)
it('should reflect changes in real time', () => {
cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json')
cy.visit('/typebots/typebot4/settings')
cy.findByRole('button', { name: 'Typing emulation' }).click()
cy.findByTestId('speed').clear().type('350')
cy.findByTestId('max-delay').clear().type('1.5')
cy.findByRole('checkbox', { name: 'Typing emulation' }).uncheck({
force: true,
})
cy.findByTestId('speed').should('not.exist')
cy.findByTestId('max-delay').should('not.exist')
})
})

View File

@ -1,14 +1,16 @@
import { getIframeBody } from 'cypress/support' import {
getIframeBody,
prepareDbAndSignIn,
removePreventReload,
} from 'cypress/support'
describe('General theme settings', () => { describe('General theme settings', () => {
beforeEach(() => { beforeEach(prepareDbAndSignIn)
cy.task('seed')
cy.signOut() afterEach(removePreventReload)
})
it('should reflect changes in real time', () => { it('should reflect changes in real time', () => {
cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json') cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json')
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/theme') cy.visit('/typebots/typebot4/theme')
getIframeBody().findByText('Ready?').should('exist') getIframeBody().findByText('Ready?').should('exist')
cy.findByRole('button', { name: 'Chat' }).click() cy.findByRole('button', { name: 'Chat' }).click()
@ -56,9 +58,6 @@ describe('General theme settings', () => {
.eq(3) .eq(3)
.click({ force: true }) .click({ force: true })
cy.findByRole('textbox', { name: 'Color value' }).clear().type('#264653') cy.findByRole('textbox', { name: 'Color value' }).clear().type('#264653')
cy.findAllByRole('button', { name: 'Pick a color' })
.eq(6)
.click({ force: true })
getIframeBody().findByRole('button', { name: 'Go' }).click() getIframeBody().findByRole('button', { name: 'Go' }).click()
getIframeBody() getIframeBody()
.findByTestId('guest-bubble') .findByTestId('guest-bubble')
@ -68,8 +67,17 @@ describe('General theme settings', () => {
.findByTestId('guest-bubble') .findByTestId('guest-bubble')
.should('have.css', 'color') .should('have.css', 'color')
.should('eq', 'rgb(38, 70, 83)') .should('eq', 'rgb(38, 70, 83)')
cy.findAllByRole('button', { name: 'Pick a color' })
.eq(3)
.click({ force: true })
cy.findByRole('textbox', { name: 'Color value' }).clear().type('#ffe8d6') // Input
cy.findAllByRole('button', { name: 'Pick a color' })
.eq(6)
.click({ force: true })
cy.findByRole('textbox', { name: 'Color value' })
.clear({ force: true })
.type('#ffe8d6')
cy.findAllByRole('button', { name: 'Pick a color' }) cy.findAllByRole('button', { name: 'Pick a color' })
.eq(7) .eq(7)
.click({ force: true }) .click({ force: true })
@ -85,6 +93,6 @@ describe('General theme settings', () => {
getIframeBody() getIframeBody()
.findByTestId('input') .findByTestId('input')
.should('have.css', 'color') .should('have.css', 'color')
.should('eq', 'rgb(2, 61, 138)') .should('eq', 'rgb(2, 62, 138)')
}) })
}) })

View File

@ -1,14 +1,16 @@
import { getIframeBody } from 'cypress/support' import {
getIframeBody,
prepareDbAndSignIn,
removePreventReload,
} from 'cypress/support'
describe('Custom CSS settings', () => { describe('Custom CSS settings', () => {
beforeEach(() => { beforeEach(prepareDbAndSignIn)
cy.task('seed')
cy.signOut() afterEach(removePreventReload)
})
it('should reflect changes in real time', () => { it('should reflect changes in real time', () => {
cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json') cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json')
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/theme') cy.visit('/typebots/typebot4/theme')
cy.findByRole('button', { name: 'Custom CSS' }).click() cy.findByRole('button', { name: 'Custom CSS' }).click()

View File

@ -1,14 +1,16 @@
import { getIframeBody } from 'cypress/support' import {
getIframeBody,
prepareDbAndSignIn,
removePreventReload,
} from 'cypress/support'
describe('General theme settings', () => { describe('General theme settings', () => {
beforeEach(() => { beforeEach(prepareDbAndSignIn)
cy.task('seed')
cy.signOut() afterEach(removePreventReload)
})
it('should reflect changes in real time', () => { it('should reflect changes in real time', () => {
cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json') cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json')
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/theme') cy.visit('/typebots/typebot4/theme')
cy.findByRole('button', { name: 'General' }).click() cy.findByRole('button', { name: 'General' }).click()

View File

@ -0,0 +1,24 @@
import { Flex } from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import React, { useMemo } from 'react'
import { TypebotViewer } from 'bot-engine'
import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
import { SettingsSideMenu } from 'components/settings/SettingsSideMenu'
export const SettingsContent = () => {
const { typebot } = useTypebot()
const publicTypebot = useMemo(
() => (typebot ? parseTypebotToPublicTypebot(typebot) : undefined),
// eslint-disable-next-line react-hooks/exhaustive-deps
[typebot?.settings]
)
return (
<Flex h="full" w="full">
<SettingsSideMenu />
<Flex flex="1">
{publicTypebot && <TypebotViewer typebot={publicTypebot} />}
</Flex>
</Flex>
)
}

View File

@ -3,7 +3,7 @@ import { TypebotViewer } from 'bot-engine'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { parseTypebotToPublicTypebot } from 'services/publicTypebot' import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
import { SideMenu } from '../../components/theme/SideMenu' import { ThemeSideMenu } from '../../components/theme/ThemeSideMenu'
export const ThemeContent = () => { export const ThemeContent = () => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
@ -14,7 +14,7 @@ export const ThemeContent = () => {
) )
return ( return (
<Flex h="full" w="full"> <Flex h="full" w="full">
<SideMenu /> <ThemeSideMenu />
<Flex flex="1"> <Flex flex="1">
{publicTypebot && <TypebotViewer typebot={publicTypebot} />} {publicTypebot && <TypebotViewer typebot={publicTypebot} />}
</Flex> </Flex>

View File

@ -31,7 +31,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const data = JSON.parse(req.body) const data = JSON.parse(req.body)
const typebots = await prisma.typebot.update({ const typebots = await prisma.typebot.update({
where: { id: typebotId }, where: { id: typebotId },
data, data: {
...data,
theme: data.theme ?? undefined,
settings: data.settings ?? undefined,
},
}) })
return res.send({ typebots }) return res.send({ typebots })
} }

View File

@ -1,6 +1,6 @@
import { Flex } from '@chakra-ui/layout' import { Flex } from '@chakra-ui/layout'
import { Seo } from 'components/Seo' import { Seo } from 'components/Seo'
import { SettingsContent } from 'components/settings/SettingsContent' import { SettingsContent } from 'layouts/settings/SettingsContent'
import { TypebotHeader } from 'components/shared/TypebotHeader' import { TypebotHeader } from 'components/shared/TypebotHeader'
import React from 'react' import React from 'react'

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

View File

@ -1,23 +1,41 @@
import { import {
Block, Block,
TextBubbleStep,
PublicTypebot, PublicTypebot,
StartStep, StartStep,
BubbleStepType, BubbleStepType,
InputStepType, InputStepType,
ChoiceInputStep,
LogicStepType, LogicStepType,
Step, Step,
ConditionStep,
ComparisonOperators,
LogicalOperator,
DraggableStepType, DraggableStepType,
DraggableStep, DraggableStep,
defaultTheme,
defaultSettings,
StepOptions,
BubbleStepContent,
IntegrationStepType,
defaultTextBubbleContent,
defaultImageBubbleContent,
defaultVideoBubbleContent,
defaultTextInputOptions,
defaultNumberInputOptions,
defaultEmailInputOptions,
defaultDateInputOptions,
defaultPhoneInputOptions,
defaultUrlInputOptions,
defaultChoiceInputOptions,
defaultSetVariablesOptions,
defaultConditionOptions,
defaultRedirectOptions,
defaultGoogleSheetsOptions,
defaultGoogleAnalyticsOptions,
defaultWebhookOptions,
StepWithOptionsType,
} from 'models' } from 'models'
import shortId, { generate } from 'short-uuid' import shortId from 'short-uuid'
import { Typebot } from 'models' import { Typebot } from 'models'
import useSWR from 'swr' import useSWR from 'swr'
import { fetcher, toKebabCase } from './utils' import { fetcher, toKebabCase } from './utils'
import { isBubbleStepType, stepTypeHasOption } from 'utils'
import { deepEqual } from 'fast-equals' import { deepEqual } from 'fast-equals'
import { stringify } from 'qs' import { stringify } from 'qs'
import { isChoiceInput, isConditionStep, sendRequest } from 'utils' import { isChoiceInput, isConditionStep, sendRequest } from 'utils'
@ -114,56 +132,56 @@ export const parseNewStep = (
blockId: string blockId: string
): DraggableStep => { ): DraggableStep => {
const id = `s${shortId.generate()}` const id = `s${shortId.generate()}`
return {
id,
blockId,
type,
content: isBubbleStepType(type) ? parseDefaultContent(type) : undefined,
options: stepTypeHasOption(type)
? parseDefaultStepOptions(type)
: undefined,
} as DraggableStep
}
const parseDefaultContent = (type: BubbleStepType): BubbleStepContent => {
switch (type) { switch (type) {
case BubbleStepType.TEXT: { case BubbleStepType.TEXT:
const textStep: Pick<TextBubbleStep, 'type' | 'content'> = { return defaultTextBubbleContent
type, case BubbleStepType.IMAGE:
content: { html: '', richText: [], plainText: '' }, return defaultImageBubbleContent
} case BubbleStepType.VIDEO:
return { return defaultVideoBubbleContent
id, }
blockId, }
...textStep,
} const parseDefaultStepOptions = (type: StepWithOptionsType): StepOptions => {
} switch (type) {
case InputStepType.CHOICE: { case InputStepType.TEXT:
const choiceInput: Pick<ChoiceInputStep, 'type' | 'options'> = { return defaultTextInputOptions
type, case InputStepType.NUMBER:
options: { itemIds: [] }, return defaultNumberInputOptions
} case InputStepType.EMAIL:
return { return defaultEmailInputOptions
id, case InputStepType.DATE:
blockId, return defaultDateInputOptions
...choiceInput, case InputStepType.PHONE:
} return defaultPhoneInputOptions
} case InputStepType.URL:
case LogicStepType.CONDITION: { return defaultUrlInputOptions
const id = generate() case InputStepType.CHOICE:
const conditionStep: Pick<ConditionStep, 'type' | 'options'> = { return defaultChoiceInputOptions
type, case LogicStepType.SET_VARIABLE:
options: { return defaultSetVariablesOptions
comparisons: { case LogicStepType.CONDITION:
byId: { return defaultConditionOptions
[id]: { id, comparisonOperator: ComparisonOperators.EQUAL }, case LogicStepType.REDIRECT:
}, return defaultRedirectOptions
allIds: [id], case IntegrationStepType.GOOGLE_SHEETS:
}, return defaultGoogleSheetsOptions
logicalOperator: LogicalOperator.AND, case IntegrationStepType.GOOGLE_ANALYTICS:
}, return defaultGoogleAnalyticsOptions
} case IntegrationStepType.WEBHOOK:
return { return defaultWebhookOptions
id,
blockId,
...conditionStep,
}
}
default: {
return {
id,
blockId,
type,
}
}
} }
} }
@ -223,6 +241,8 @@ export const parseNewTypebot = ({
variables: { byId: {}, allIds: [] }, variables: { byId: {}, allIds: [] },
edges: { byId: {}, allIds: [] }, edges: { byId: {}, allIds: [] },
webhooks: { byId: {}, allIds: [] }, webhooks: { byId: {}, allIds: [] },
theme: defaultTheme,
settings: defaultSettings,
} }
} }

View File

@ -4,9 +4,9 @@ import { methodNotAllowed } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'POST') { if (req.method === 'POST') {
const { typebotId } = JSON.parse(req.body) const { typebotId } = JSON.parse(req.body) as { typebotId: string }
const result = await prisma.result.create({ const result = await prisma.result.create({
data: { typebotId }, data: { typebotId, isCompleted: false },
}) })
return res.send(result) return res.send(result)
} }

View File

@ -1,10 +1,11 @@
import prisma from 'libs/prisma' import prisma from 'libs/prisma'
import { Result } from 'models'
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed } from 'utils' import { methodNotAllowed } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'PATCH') { if (req.method === 'PATCH') {
const data = JSON.parse(req.body) const data = JSON.parse(req.body) as Result
const id = req.query.id.toString() const id = req.query.id.toString()
const result = await prisma.result.update({ const result = await prisma.result.update({
where: { id }, where: { id },

View File

@ -0,0 +1,34 @@
module.exports = {
ignorePatterns: ['node_modules'],
env: {
browser: true,
es6: true,
},
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'next/core-web-vitals',
'prettier',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
ecmaFeatures: {
jsx: true, // Allows for the parsing of JSX
},
},
settings: {
react: {
version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use
},
},
plugins: ['prettier', 'react', 'cypress', '@typescript-eslint'],
ignorePatterns: 'dist',
rules: {
'react/no-unescaped-entities': [0],
'prettier/prettier': 'error',
'react/display-name': [0],
'@next/next/no-img-element': [0],
},
}

View File

@ -1,5 +1,6 @@
import { GoogleAnalyticsOptions } from 'models' import { GoogleAnalyticsOptions } from 'models'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
declare const gtag: any declare const gtag: any
const initGoogleAnalytics = (id: string): Promise<void> => const initGoogleAnalytics = (id: string): Promise<void> =>

View File

@ -34,13 +34,20 @@
"rollup-plugin-postcss": "^4.0.2", "rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"tailwindcss": "^3.0.11", "tailwindcss": "^3.0.11",
"typescript": "^4.5.4" "typescript": "^4.5.4",
"@typescript-eslint/eslint-plugin": "^5.9.0",
"eslint": "<8.0.0",
"eslint-config-next": "12.0.7",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-prettier": "^4.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^17.0.2" "react": "^17.0.2"
}, },
"scripts": { "scripts": {
"build": "yarn rollup -c", "build": "yarn rollup -c",
"dev": "yarn rollup -c --watch" "dev": "yarn rollup -c --watch",
"lint": "eslint --fix -c ./.eslintrc.js \"./src/**/*.ts*\""
} }
} }

View File

@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useTypebot } from '../../contexts/TypebotContext' import { useTypebot } from '../../contexts/TypebotContext'
import { HostAvatar } from '../avatars/HostAvatar' import { HostAvatar } from '../avatars/HostAvatar'
import { useFrame } from 'react-frame-component' import { useFrame } from 'react-frame-component'
@ -22,6 +22,7 @@ export const AvatarSideContainer = () => {
return () => { return () => {
resizeObserver.disconnect() resizeObserver.disconnect()
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
return ( return (

View File

@ -35,11 +35,13 @@ export const ChatBlock = ({
const nextStep = const nextStep =
typebot.steps.byId[startStepId ?? stepIds[displayedSteps.length]] typebot.steps.byId[startStepId ?? stepIds[displayedSteps.length]]
if (nextStep) setDisplayedSteps([...displayedSteps, nextStep]) if (nextStep) setDisplayedSteps([...displayedSteps, nextStep])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
useEffect(() => { useEffect(() => {
autoScrollToBottom() autoScrollToBottom()
onNewStepDisplayed() onNewStepDisplayed()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [displayedSteps]) }, [displayedSteps])
const onNewStepDisplayed = async () => { const onNewStepDisplayed = async () => {

View File

@ -42,6 +42,7 @@ const InputChatStep = ({
useEffect(() => { useEffect(() => {
addNewAvatarOffset() addNewAvatarOffset()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
const handleSubmit = (value: string) => { const handleSubmit = (value: string) => {

View File

@ -24,11 +24,12 @@ export const ImageBubble = ({ step, onTransitionEnd }: Props) => {
const url = useMemo( const url = useMemo(
() => () =>
parseVariables({ text: step.content?.url, variables: typebot.variables }), parseVariables({ text: step.content?.url, variables: typebot.variables }),
[typebot.variables] [step.content?.url, typebot.variables]
) )
useEffect(() => { useEffect(() => {
showContentAfterMediaLoad() showContentAfterMediaLoad()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
const showContentAfterMediaLoad = () => { const showContentAfterMediaLoad = () => {
@ -83,6 +84,7 @@ export const ImageBubble = ({ step, onTransitionEnd }: Props) => {
height: isTyping ? '2rem' : 'auto', height: isTyping ? '2rem' : 'auto',
maxWidth: '100%', maxWidth: '100%',
}} }}
alt="Bubble image"
/> />
</div> </div>
</div> </div>

View File

@ -28,6 +28,7 @@ export const TextBubble = ({ step, onTransitionEnd }: Props) => {
const content = useMemo( const content = useMemo(
() => () =>
parseVariables({ text: step.content.html, variables: typebot.variables }), parseVariables({ text: step.content.html, variables: typebot.variables }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[typebot.variables] [typebot.variables]
) )
@ -40,6 +41,7 @@ export const TextBubble = ({ step, onTransitionEnd }: Props) => {
setTimeout(() => { setTimeout(() => {
onTypingEnd() onTypingEnd()
}, typingTimeout) }, typingTimeout)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
const onTypingEnd = () => { const onTypingEnd = () => {

View File

@ -28,6 +28,7 @@ export const VideoBubble = ({ step, onTransitionEnd }: Props) => {
useEffect(() => { useEffect(() => {
showContentAfterMediaLoad() showContentAfterMediaLoad()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
const showContentAfterMediaLoad = () => { const showContentAfterMediaLoad = () => {
@ -86,6 +87,7 @@ const VideoContent = ({
}) => { }) => {
const url = useMemo( const url = useMemo(
() => parseVariables({ text: content?.url, variables: variables }), () => parseVariables({ text: content?.url, variables: variables }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[variables] [variables]
) )
if (!content?.type) return <></> if (!content?.type) return <></>

View File

@ -13,6 +13,7 @@ export const ChoiceForm = ({ options, onSubmit }: ChoiceFormProps) => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
const items = useMemo( const items = useMemo(
() => filterTable(options?.itemIds ?? [], typebot.choiceItems), () => filterTable(options?.itemIds ?? [], typebot.choiceItems),
// eslint-disable-next-line react-hooks/exhaustive-deps
[] []
) )
const [selectedIds, setSelectedIds] = useState<string[]>([]) const [selectedIds, setSelectedIds] = useState<string[]>([])
@ -41,6 +42,7 @@ export const ChoiceForm = ({ options, onSubmit }: ChoiceFormProps) => {
<div className="flex flex-wrap"> <div className="flex flex-wrap">
{options?.itemIds.map((itemId) => ( {options?.itemIds.map((itemId) => (
<button <button
key={itemId}
role={options?.isMultipleChoice ? 'checkbox' : 'button'} role={options?.isMultipleChoice ? 'checkbox' : 'button'}
onClick={handleClick(itemId)} onClick={handleClick(itemId)}
className={ className={

View File

@ -26,7 +26,7 @@ type TextInputProps = {
} }
export const TextInput = ({ step, onChange }: TextInputProps) => { export const TextInput = ({ step, onChange }: TextInputProps) => {
const inputRef = useRef<any>(null) const inputRef = useRef<HTMLInputElement & HTMLTextAreaElement>(null)
useEffect(() => { useEffect(() => {
if (!inputRef.current) return if (!inputRef.current) return
@ -102,7 +102,8 @@ export const TextInput = ({ step, onChange }: TextInputProps) => {
case InputStepType.PHONE: { case InputStepType.PHONE: {
return ( return (
<PhoneInput <PhoneInput
ref={inputRef} // eslint-disable-next-line @typescript-eslint/no-explicit-any
ref={inputRef as any}
onChange={handlePhoneNumberChange} onChange={handlePhoneNumberChange}
placeholder={ placeholder={
step.options?.labels?.placeholder ?? 'Your phone number...' step.options?.labels?.placeholder ?? 'Your phone number...'

View File

@ -5,7 +5,7 @@ import { useFrame } from 'react-frame-component'
import { setCssVariablesValue } from '../services/theme' import { setCssVariablesValue } from '../services/theme'
import { useAnswers } from '../contexts/AnswersContext' import { useAnswers } from '../contexts/AnswersContext'
import { deepEqual } from 'fast-equals' import { deepEqual } from 'fast-equals'
import { Answer, Block, Edge, PublicTypebot } from 'models' import { Answer, Block, PublicTypebot } from 'models'
type Props = { type Props = {
typebot: PublicTypebot typebot: PublicTypebot
@ -45,6 +45,7 @@ export const ConversationContainer = ({
typebot.steps.byId[blocks.byId[blocks.allIds[0]].stepIds[0]].edgeId typebot.steps.byId[blocks.byId[blocks.allIds[0]].stepIds[0]].edgeId
if (!firstEdgeId) return if (!firstEdgeId) return
displayNextBlock(firstEdgeId) displayNextBlock(firstEdgeId)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
useEffect(() => { useEffect(() => {
@ -56,6 +57,7 @@ export const ConversationContainer = ({
if (!answer || deepEqual(localAnswer, answer)) return if (!answer || deepEqual(localAnswer, answer)) return
setLocalAnswer(answer) setLocalAnswer(answer)
onNewAnswer(answer) onNewAnswer(answer)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers]) }, [answers])
return ( return (

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { TypebotContext } from '../contexts/TypebotContext' import { TypebotContext } from '../contexts/TypebotContext'
import Frame from 'react-frame-component' import Frame from 'react-frame-component'
@ -61,7 +62,7 @@ export const TypebotViewer = ({
}} }}
/> />
<TypebotContext typebot={typebot}> <TypebotContext typebot={typebot}>
<AnswersContext typebotId={typebot.id}> <AnswersContext>
<div <div
className="flex text-base overflow-hidden bg-cover h-screen w-screen flex-col items-center typebot-container" className="flex text-base overflow-hidden bg-cover h-screen w-screen flex-col items-center typebot-container"
style={{ style={{
@ -78,6 +79,17 @@ export const TypebotViewer = ({
onCompleted={handleCompleted} onCompleted={handleCompleted}
/> />
</div> </div>
{typebot.settings.general.isBrandingEnabled && (
<a
href={'https://www.typebot.io/?utm_source=litebadge'}
target="_blank"
rel="noopener noreferrer"
className="fixed py-1 px-2 bg-white z-50 rounded shadow-md"
style={{ bottom: '20px' }}
>
Made with <span className="text-blue-500">Typebot</span>.
</a>
)}
</div> </div>
</AnswersContext> </AnswersContext>
</TypebotContext> </TypebotContext>

View File

@ -4,16 +4,11 @@ import React, { createContext, ReactNode, useContext, useState } from 'react'
const answersContext = createContext<{ const answersContext = createContext<{
answers: Answer[] answers: Answer[]
addAnswer: (answer: Answer) => void addAnswer: (answer: Answer) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
}>({}) }>({})
export const AnswersContext = ({ export const AnswersContext = ({ children }: { children: ReactNode }) => {
children,
typebotId,
}: {
children: ReactNode
typebotId: string
}) => {
const [answers, setAnswers] = useState<Answer[]>([]) const [answers, setAnswers] = useState<Answer[]>([])
const addAnswer = (answer: Answer) => const addAnswer = (answer: Answer) =>

View File

@ -5,6 +5,7 @@ const hostAvatarsContext = createContext<{
lastBubblesTopOffset: number[] lastBubblesTopOffset: number[]
addNewAvatarOffset: () => void addNewAvatarOffset: () => void
updateLastAvatarOffset: (newOffset: number) => void updateLastAvatarOffset: (newOffset: number) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
}>({}) }>({})

View File

@ -4,6 +4,7 @@ import React, { createContext, ReactNode, useContext, useState } from 'react'
const typebotContext = createContext<{ const typebotContext = createContext<{
typebot: PublicTypebot typebot: PublicTypebot
updateVariableValue: (variableId: string, value: string) => void updateVariableValue: (variableId: string, value: string) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
}>({}) }>({})

Some files were not shown because too many files have changed in this diff Show More