refactor(♻️ Add defaults everywhere (+ settings page)):
This commit is contained in:
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
@ -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'
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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: {
|
||||||
|
33
apps/builder/components/settings/GeneralSettingsForm.tsx
Normal file
33
apps/builder/components/settings/GeneralSettingsForm.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
112
apps/builder/components/settings/MetadataForm.tsx
Normal file
112
apps/builder/components/settings/MetadataForm.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
103
apps/builder/components/settings/SettingsSideMenu.tsx
Normal file
103
apps/builder/components/settings/SettingsSideMenu.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
69
apps/builder/components/settings/TypingEmulationForm.tsx
Normal file
69
apps/builder/components/settings/TypingEmulationForm.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
9
apps/builder/components/shared/Info.tsx
Normal file
9
apps/builder/components/shared/Info.tsx
Normal 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>
|
||||||
|
)
|
@ -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 />
|
@ -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} />
|
@ -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} />
|
@ -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>
|
@ -0,0 +1,2 @@
|
|||||||
|
export { InputWithVariableButton } from './InputWithVariableButton'
|
||||||
|
export { TextareaWithVariableButton } from './TextareaWithVariableButton'
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
@ -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 })
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
|
@ -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: {
|
||||||
|
@ -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:
|
||||||
|
@ -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' })
|
||||||
|
@ -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')
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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,
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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:' })
|
||||||
|
@ -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')
|
||||||
|
@ -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')
|
||||||
|
@ -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')
|
||||||
|
@ -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')
|
||||||
|
@ -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',
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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')
|
||||||
|
@ -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')
|
||||||
})
|
})
|
||||||
|
@ -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')
|
||||||
|
27
apps/builder/cypress/tests/settings/general.ts
Normal file
27
apps/builder/cypress/tests/settings/general.ts
Normal 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')
|
||||||
|
})
|
||||||
|
})
|
51
apps/builder/cypress/tests/settings/metadata.ts
Normal file
51
apps/builder/cypress/tests/settings/metadata.ts
Normal 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')
|
||||||
|
})
|
||||||
|
})
|
20
apps/builder/cypress/tests/settings/typingEmulation.ts
Normal file
20
apps/builder/cypress/tests/settings/typingEmulation.ts
Normal 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')
|
||||||
|
})
|
||||||
|
})
|
@ -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)')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
24
apps/builder/layouts/settings/SettingsContent.tsx
Normal file
24
apps/builder/layouts/settings/SettingsContent.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
BIN
apps/builder/public/viewer-preview.png
Normal file
BIN
apps/builder/public/viewer-preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 174 KiB |
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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 },
|
||||||
|
34
packages/bot-engine/.eslintrc.js
Normal file
34
packages/bot-engine/.eslintrc.js
Normal 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],
|
||||||
|
},
|
||||||
|
}
|
@ -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> =>
|
||||||
|
@ -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*\""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 (
|
||||||
|
@ -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 () => {
|
||||||
|
@ -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) => {
|
||||||
|
@ -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>
|
||||||
|
@ -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 = () => {
|
||||||
|
@ -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 <></>
|
||||||
|
@ -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={
|
||||||
|
@ -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...'
|
||||||
|
@ -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 (
|
||||||
|
@ -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>
|
||||||
|
@ -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) =>
|
||||||
|
@ -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
|
||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
|
@ -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
Reference in New Issue
Block a user