diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/ContentPopover/ContentPopover.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/ContentPopover/ContentPopover.tsx index daf1d068c..687211805 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/ContentPopover/ContentPopover.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/ContentPopover/ContentPopover.tsx @@ -8,10 +8,11 @@ import { ImageUploadContent } from 'components/shared/ImageUploadContent' import { useTypebot } from 'contexts/TypebotContext' import { BubbleStep, - BubbleStepContent, BubbleStepType, ImageBubbleStep, TextBubbleStep, + VideoBubbleContent, + VideoBubbleStep, } from 'models' import { useRef } from 'react' import { VideoUploadContent } from './VideoUploadContent' @@ -39,14 +40,17 @@ export const ContentPopover = ({ step }: Props) => { export const StepContent = ({ step }: Props) => { const { updateStep } = useTypebot() - const handleContentChange = (content: BubbleStepContent) => - updateStep(step.id, { content } as Partial) + const handleContentChange = (url: string) => + updateStep(step.id, { content: { url } } as Partial) + + const handleVideoContentChange = (content: VideoBubbleContent) => + updateStep(step.id, { content } as Partial) switch (step.type) { case BubbleStepType.IMAGE: { return ( ) @@ -55,7 +59,7 @@ export const StepContent = ({ step }: Props) => { return ( ) } diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/ContentPopover/VideoUploadContent.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/ContentPopover/VideoUploadContent.tsx index 814eef702..93eeeeb1c 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/ContentPopover/VideoUploadContent.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/ContentPopover/VideoUploadContent.tsx @@ -1,5 +1,5 @@ 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 urlParser from 'js-video-url-parser/lib/base' import 'js-video-url-parser/lib/provider/vimeo' diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ComparisonsItem.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ComparisonsItem.tsx index aaee21757..add61e00e 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ComparisonsItem.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ComparisonsItem.tsx @@ -1,6 +1,6 @@ import { Stack } from '@chakra-ui/react' 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 { VariableSearchInput } from 'components/shared/VariableSearchInput' import { Comparison, Variable, ComparisonOperators } from 'models' diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/DateInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/DateInputSettingsBody.tsx index 7973366ab..7fce60299 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/DateInputSettingsBody.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/DateInputSettingsBody.tsx @@ -6,7 +6,7 @@ import { DateInputOptions, Variable } from 'models' import React from 'react' type DateInputSettingsBodyProps = { - options?: DateInputOptions + options: DateInputOptions onOptionsChange: (options: DateInputOptions) => void } @@ -32,23 +32,23 @@ export const DateInputSettingsBody = ({ - {options?.isRange && ( + {options.isRange && ( From label: @@ -61,7 +61,7 @@ export const DateInputSettingsBody = ({ @@ -73,7 +73,7 @@ export const DateInputSettingsBody = ({ @@ -83,7 +83,7 @@ export const DateInputSettingsBody = ({ Save answer in a variable: diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/EmailInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/EmailInputSettingsBody.tsx index 25d42d814..a276f83f4 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/EmailInputSettingsBody.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/EmailInputSettingsBody.tsx @@ -5,7 +5,7 @@ import { EmailInputOptions, Variable } from 'models' import React from 'react' type EmailInputSettingsBodyProps = { - options?: EmailInputOptions + options: EmailInputOptions onOptionsChange: (options: EmailInputOptions) => void } @@ -14,9 +14,9 @@ export const EmailInputSettingsBody = ({ onOptionsChange, }: EmailInputSettingsBodyProps) => { const handlePlaceholderChange = (placeholder: string) => - onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } }) + onOptionsChange({ ...options, labels: { ...options.labels, placeholder } }) const handleButtonLabelChange = (button: string) => - onOptionsChange({ ...options, labels: { ...options?.labels, button } }) + onOptionsChange({ ...options, labels: { ...options.labels, button } }) const handleVariableChange = (variable?: Variable) => onOptionsChange({ ...options, variableId: variable?.id }) @@ -28,7 +28,7 @@ export const EmailInputSettingsBody = ({ @@ -39,7 +39,7 @@ export const EmailInputSettingsBody = ({ @@ -49,7 +49,7 @@ export const EmailInputSettingsBody = ({ Save answer in a variable: diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleAnalyticsSettings.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleAnalyticsSettings.tsx index 9d6a4673c..eddd87979 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleAnalyticsSettings.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleAnalyticsSettings.tsx @@ -10,7 +10,7 @@ import { Tag, } from '@chakra-ui/react' import { DebouncedInput } from 'components/shared/DebouncedInput' -import { InputWithVariableButton } from 'components/shared/InputWithVariableButton' +import { InputWithVariableButton } from 'components/shared/TextboxWithVariableButton/InputWithVariableButton' import { GoogleAnalyticsOptions } from 'models' import React from 'react' diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/CellWithValueStack.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/CellWithValueStack.tsx index 13db46c39..f222143aa 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/CellWithValueStack.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/CellWithValueStack.tsx @@ -1,6 +1,6 @@ import { Stack } from '@chakra-ui/react' 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 { Cell } from 'models' diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/NumberInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/NumberInputSettingsBody.tsx index b91350b72..c46d53fb7 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/NumberInputSettingsBody.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/NumberInputSettingsBody.tsx @@ -1,5 +1,5 @@ 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 { VariableSearchInput } from 'components/shared/VariableSearchInput' import { NumberInputOptions, Variable } from 'models' @@ -7,7 +7,7 @@ import React from 'react' import { removeUndefinedFields } from 'services/utils' type NumberInputSettingsBodyProps = { - options?: NumberInputOptions + options: NumberInputOptions onOptionsChange: (options: NumberInputOptions) => void } @@ -16,9 +16,9 @@ export const NumberInputSettingsBody = ({ onOptionsChange, }: NumberInputSettingsBodyProps) => { const handlePlaceholderChange = (placeholder: string) => - onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } }) + onOptionsChange({ ...options, labels: { ...options.labels, placeholder } }) const handleButtonLabelChange = (button: string) => - onOptionsChange({ ...options, labels: { ...options?.labels, button } }) + onOptionsChange({ ...options, labels: { ...options.labels, button } }) const handleMinChange = (min?: number) => onOptionsChange(removeUndefinedFields({ ...options, min })) const handleMaxChange = (max?: number) => @@ -36,7 +36,7 @@ export const NumberInputSettingsBody = ({ @@ -58,7 +58,7 @@ export const NumberInputSettingsBody = ({ @@ -68,7 +68,7 @@ export const NumberInputSettingsBody = ({ @@ -78,7 +78,7 @@ export const NumberInputSettingsBody = ({ @@ -87,7 +87,7 @@ export const NumberInputSettingsBody = ({ Save answer in a variable: diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/PhoneNumberSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/PhoneNumberSettingsBody.tsx index 3a9cdd257..70b30f949 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/PhoneNumberSettingsBody.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/PhoneNumberSettingsBody.tsx @@ -5,7 +5,7 @@ import { EmailInputOptions, Variable } from 'models' import React from 'react' type PhoneNumberSettingsBodyProps = { - options?: EmailInputOptions + options: EmailInputOptions onOptionsChange: (options: EmailInputOptions) => void } @@ -14,9 +14,9 @@ export const PhoneNumberSettingsBody = ({ onOptionsChange, }: PhoneNumberSettingsBodyProps) => { const handlePlaceholderChange = (placeholder: string) => - onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } }) + onOptionsChange({ ...options, labels: { ...options.labels, placeholder } }) const handleButtonLabelChange = (button: string) => - onOptionsChange({ ...options, labels: { ...options?.labels, button } }) + onOptionsChange({ ...options, labels: { ...options.labels, button } }) const handleVariableChange = (variable?: Variable) => onOptionsChange({ ...options, variableId: variable?.id }) @@ -28,7 +28,7 @@ export const PhoneNumberSettingsBody = ({ @@ -39,7 +39,7 @@ export const PhoneNumberSettingsBody = ({ @@ -49,7 +49,7 @@ export const PhoneNumberSettingsBody = ({ Save answer in a variable: diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/RedirectSettings.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/RedirectSettings.tsx index 2206cc712..082d9e091 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/RedirectSettings.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/RedirectSettings.tsx @@ -5,14 +5,14 @@ import { RedirectOptions } from 'models' import React from 'react' type Props = { - options?: RedirectOptions + options: RedirectOptions onOptionsChange: (options: RedirectOptions) => void } export const RedirectSettings = ({ options, onOptionsChange }: Props) => { const handleUrlChange = (url?: string) => onOptionsChange({ ...options, url }) - const handleIsNewTabChange = (isNewTab?: boolean) => + const handleIsNewTabChange = (isNewTab: boolean) => onOptionsChange({ ...options, isNewTab }) return ( @@ -23,7 +23,7 @@ export const RedirectSettings = ({ options, onOptionsChange }: Props) => { { diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/SetVariableSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/SetVariableSettingsBody.tsx index 9aed047cc..1ee7140b0 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/SetVariableSettingsBody.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/SetVariableSettingsBody.tsx @@ -5,7 +5,7 @@ import { SetVariableOptions, Variable } from 'models' import React from 'react' type Props = { - options?: SetVariableOptions + options: SetVariableOptions onOptionsChange: (options: SetVariableOptions) => void } @@ -26,7 +26,7 @@ export const SetVariableSettingsBody = ({ @@ -36,7 +36,7 @@ export const SetVariableSettingsBody = ({ diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/TextInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/TextInputSettingsBody.tsx index 428f78203..f61f1e288 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/TextInputSettingsBody.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/TextInputSettingsBody.tsx @@ -6,7 +6,7 @@ import { TextInputOptions, Variable } from 'models' import React from 'react' type TextInputSettingsBodyProps = { - options?: TextInputOptions + options: TextInputOptions onOptionsChange: (options: TextInputOptions) => void } @@ -15,9 +15,9 @@ export const TextInputSettingsBody = ({ onOptionsChange, }: TextInputSettingsBodyProps) => { const handlePlaceholderChange = (placeholder: string) => - onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } }) + onOptionsChange({ ...options, labels: { ...options.labels, placeholder } }) const handleButtonLabelChange = (button: string) => - onOptionsChange({ ...options, labels: { ...options?.labels, button } }) + onOptionsChange({ ...options, labels: { ...options.labels, button } }) const handleLongChange = (isLong: boolean) => onOptionsChange({ ...options, isLong }) const handleVariableChange = (variable?: Variable) => @@ -37,7 +37,7 @@ export const TextInputSettingsBody = ({ @@ -48,7 +48,7 @@ export const TextInputSettingsBody = ({ @@ -58,7 +58,7 @@ export const TextInputSettingsBody = ({ Save answer in a variable: diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/UrlInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/UrlInputSettingsBody.tsx index f2c34beee..157c3dc7a 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/UrlInputSettingsBody.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/UrlInputSettingsBody.tsx @@ -5,7 +5,7 @@ import { UrlInputOptions, Variable } from 'models' import React from 'react' type UrlInputSettingsBodyProps = { - options?: UrlInputOptions + options: UrlInputOptions onOptionsChange: (options: UrlInputOptions) => void } @@ -14,9 +14,9 @@ export const UrlInputSettingsBody = ({ onOptionsChange, }: UrlInputSettingsBodyProps) => { const handlePlaceholderChange = (placeholder: string) => - onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } }) + onOptionsChange({ ...options, labels: { ...options.labels, placeholder } }) const handleButtonLabelChange = (button: string) => - onOptionsChange({ ...options, labels: { ...options?.labels, button } }) + onOptionsChange({ ...options, labels: { ...options.labels, button } }) const handleVariableChange = (variable?: Variable) => onOptionsChange({ ...options, variableId: variable?.id }) @@ -28,7 +28,7 @@ export const UrlInputSettingsBody = ({ @@ -39,7 +39,7 @@ export const UrlInputSettingsBody = ({ @@ -49,7 +49,7 @@ export const UrlInputSettingsBody = ({ Save answer in a variable: diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/KeyValueInputs.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/KeyValueInputs.tsx index 93d8f88ab..dba14789a 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/KeyValueInputs.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/KeyValueInputs.tsx @@ -1,5 +1,5 @@ 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 { KeyValue } from 'models' diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/WebhookSettings.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/WebhookSettings.tsx index 506bddd6b..db212ba41 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/WebhookSettings.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/WebhookSettings.tsx @@ -10,7 +10,7 @@ import { Stack, useToast, } from '@chakra-ui/react' -import { InputWithVariableButton } from 'components/shared/InputWithVariableButton' +import { InputWithVariableButton } from 'components/shared/TextboxWithVariableButton/InputWithVariableButton' import { useTypebot } from 'contexts/TypebotContext' import { HttpMethod, diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/StepNodeContent.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/StepNodeContent.tsx index 0c9842682..aa3bcc41c 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/StepNodeContent.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/StepNodeContent.tsx @@ -103,7 +103,7 @@ export const StepNodeContent = ({ step }: Props) => { return } case LogicStepType.REDIRECT: { - if (!step.options) return Configure... + if (!step.options.url) return Configure... return Redirect to {step.options?.url} } case IntegrationStepType.GOOGLE_SHEETS: { diff --git a/apps/builder/components/settings/GeneralSettingsForm.tsx b/apps/builder/components/settings/GeneralSettingsForm.tsx new file mode 100644 index 000000000..80886fa43 --- /dev/null +++ b/apps/builder/components/settings/GeneralSettingsForm.tsx @@ -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 ( + + + + Typebot.io branding + + + + + ) +} diff --git a/apps/builder/components/settings/MetadataForm.tsx b/apps/builder/components/settings/MetadataForm.tsx new file mode 100644 index 000000000..b1d944421 --- /dev/null +++ b/apps/builder/components/settings/MetadataForm.tsx @@ -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 ( + + + + Icon: + + + + Fav icon + + + + + + + + + Image: + + + + Website image + + + + + + + + + Title: + + + + + + Description: + + + + + ) +} diff --git a/apps/builder/components/settings/SettingsContent.tsx b/apps/builder/components/settings/SettingsContent.tsx deleted file mode 100644 index 9d9ab2eae..000000000 --- a/apps/builder/components/settings/SettingsContent.tsx +++ /dev/null @@ -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 ( - - - - - - ) -} diff --git a/apps/builder/components/settings/SettingsSideMenu.tsx b/apps/builder/components/settings/SettingsSideMenu.tsx new file mode 100644 index 000000000..df45f9a14 --- /dev/null +++ b/apps/builder/components/settings/SettingsSideMenu.tsx @@ -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 ( + + + Settings + + + + + + + General + + + + + {typebot && ( + + )} + + + + + + + Typing emulation + + + + + {typebot && ( + + )} + + + + + + + Metadata + + + + + {typebot && ( + + )} + + + + + ) +} diff --git a/apps/builder/components/settings/TypingEmulation.tsx b/apps/builder/components/settings/TypingEmulation.tsx deleted file mode 100644 index a949ab84d..000000000 --- a/apps/builder/components/settings/TypingEmulation.tsx +++ /dev/null @@ -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 ( - - - Typing emulation - - - {typingEmulation?.enabled && ( - - - Words per minutes: - - - - Max delay (in seconds): - - - - )} - - ) -} diff --git a/apps/builder/components/settings/TypingEmulationForm.tsx b/apps/builder/components/settings/TypingEmulationForm.tsx new file mode 100644 index 000000000..49ad7f0ab --- /dev/null +++ b/apps/builder/components/settings/TypingEmulationForm.tsx @@ -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 ( + + + + Typing emulation + + + + {typingEmulation.enabled && ( + + + + Words per minutes: + + + + + + Max delay (in seconds): + + + + + )} + + ) +} diff --git a/apps/builder/components/shared/ImageUploadContent/ImageUploadContent.tsx b/apps/builder/components/shared/ImageUploadContent/ImageUploadContent.tsx index 5c6474501..2a5b07839 100644 --- a/apps/builder/components/shared/ImageUploadContent/ImageUploadContent.tsx +++ b/apps/builder/components/shared/ImageUploadContent/ImageUploadContent.tsx @@ -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 { SearchContextManager } from '@giphy/react-components' import { UploadButton } from '../buttons/UploadButton' import { GiphySearch } from './GiphySearch' import { useTypebot } from 'contexts/TypebotContext' -import { ImageBubbleContent } from 'models' +import { useDebounce } from 'use-debounce' type Props = { - content?: ImageBubbleContent - onSubmit: (content: ImageBubbleContent) => void + url?: string + 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'>( 'upload' ) - const handleSubmit = (url: string) => onSubmit({ url }) + const handleSubmit = (url: string) => onSubmit(url) return ( @@ -34,7 +39,7 @@ export const ImageUploadContent = ({ content, onSubmit }: Props) => { > Embed link - {process.env.NEXT_PUBLIC_GIPHY_API_KEY && ( + {process.env.NEXT_PUBLIC_GIPHY_API_KEY && isGiphyEnabled && ( - + ) } diff --git a/apps/builder/components/shared/Info.tsx b/apps/builder/components/shared/Info.tsx new file mode 100644 index 000000000..6e87da4fe --- /dev/null +++ b/apps/builder/components/shared/Info.tsx @@ -0,0 +1,9 @@ +import { Alert, AlertIcon, AlertProps } from '@chakra-ui/react' +import React from 'react' + +export const Info = (props: AlertProps) => ( + + + {props.children} + +) diff --git a/apps/builder/components/settings/SmartNumberInput.tsx b/apps/builder/components/shared/SmartNumberInput.tsx similarity index 64% rename from apps/builder/components/settings/SmartNumberInput.tsx rename to apps/builder/components/shared/SmartNumberInput.tsx index 367c6f4ba..90bf8fbe4 100644 --- a/apps/builder/components/settings/SmartNumberInput.tsx +++ b/apps/builder/components/shared/SmartNumberInput.tsx @@ -6,29 +6,29 @@ import { NumberIncrementStepper, NumberDecrementStepper, } from '@chakra-ui/react' -import { useState, useEffect } from 'react' +import { useState } from 'react' export const SmartNumberInput = ({ - initialValue, + value, onValueChange, ...props }: { - initialValue?: number + value?: number onValueChange: (value?: number) => void } & 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 === '') onValueChange(undefined) + if (value === '') return onValueChange(undefined) const newValue = parseFloat(value) if (isNaN(newValue)) return onValueChange(newValue) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value]) + } return ( - + diff --git a/apps/builder/components/shared/TextboxWithVariableButton/InputWithVariableButton.tsx b/apps/builder/components/shared/TextboxWithVariableButton/InputWithVariableButton.tsx new file mode 100644 index 000000000..9d1f63801 --- /dev/null +++ b/apps/builder/components/shared/TextboxWithVariableButton/InputWithVariableButton.tsx @@ -0,0 +1,10 @@ +import { Input } from '@chakra-ui/react' +import React from 'react' +import { + TextBoxWithVariableButton, + TextBoxWithVariableButtonProps, +} from './TextboxWithVariableButton' + +export const InputWithVariableButton = ( + props: Omit +) => diff --git a/apps/builder/components/shared/TextboxWithVariableButton/TextareaWithVariableButton.tsx b/apps/builder/components/shared/TextboxWithVariableButton/TextareaWithVariableButton.tsx new file mode 100644 index 000000000..4bb9be196 --- /dev/null +++ b/apps/builder/components/shared/TextboxWithVariableButton/TextareaWithVariableButton.tsx @@ -0,0 +1,10 @@ +import { Textarea } from '@chakra-ui/react' +import React from 'react' +import { + TextBoxWithVariableButton, + TextBoxWithVariableButtonProps, +} from './TextboxWithVariableButton' + +export const TextareaWithVariableButton = ( + props: Omit +) => diff --git a/apps/builder/components/shared/InputWithVariableButton.tsx b/apps/builder/components/shared/TextboxWithVariableButton/TextboxWithVariableButton.tsx similarity index 62% rename from apps/builder/components/shared/InputWithVariableButton.tsx rename to apps/builder/components/shared/TextboxWithVariableButton/TextboxWithVariableButton.tsx index 4b61cfe11..4fee2f350 100644 --- a/apps/builder/components/shared/InputWithVariableButton.tsx +++ b/apps/builder/components/shared/TextboxWithVariableButton/TextboxWithVariableButton.tsx @@ -1,31 +1,40 @@ import { + ComponentWithAs, Flex, HStack, IconButton, - Input, InputProps, Popover, PopoverContent, PopoverTrigger, + TextareaProps, Tooltip, } from '@chakra-ui/react' import { UserIcon } from 'assets/icons' import { Variable } from 'models' import React, { useEffect, useRef, useState } from 'react' import { useDebounce } from 'use-debounce' -import { VariableSearchInput } from './VariableSearchInput' +import { VariableSearchInput } from '../VariableSearchInput' -export const InputWithVariableButton = ({ - initialValue, - onChange, - delay, - ...props -}: { +export type TextBoxWithVariableButtonProps = { initialValue: string onChange: (value: string) => void delay?: number -} & Omit) => { - const inputRef = useRef(null) + TextBox: + | ComponentWithAs<'textarea', TextareaProps> + | ComponentWithAs<'input', InputProps> +} & Omit + +export const TextBoxWithVariableButton = ({ + initialValue, + onChange, + delay, + TextBox, + ...props +}: TextBoxWithVariableButtonProps) => { + const textBoxRef = useRef<(HTMLInputElement & HTMLTextAreaElement) | null>( + null + ) const [value, setValue] = useState(initialValue) const [debouncedValue] = useDebounce(value, delay ?? 100) const [carretPosition, setCarretPosition] = useState(0) @@ -36,48 +45,49 @@ export const InputWithVariableButton = ({ }, [debouncedValue]) const handleVariableSelected = (variable?: Variable) => { - if (!inputRef.current || !variable) return + if (!textBoxRef.current || !variable) return const cursorPosition = carretPosition - const textBeforeCursorPosition = inputRef.current.value.substring( + const textBeforeCursorPosition = textBoxRef.current.value.substring( 0, cursorPosition ) - const textAfterCursorPosition = inputRef.current.value.substring( + const textAfterCursorPosition = textBoxRef.current.value.substring( cursorPosition, - inputRef.current.value.length + textBoxRef.current.value.length ) setValue( textBeforeCursorPosition + `{{${variable.name}}}` + textAfterCursorPosition ) - inputRef.current.focus() + textBoxRef.current.focus() setTimeout(() => { - if (!inputRef.current) return - inputRef.current.selectionStart = inputRef.current.selectionEnd = + if (!textBoxRef.current) return + textBoxRef.current.selectionStart = textBoxRef.current.selectionEnd = carretPosition + `{{${variable.name}}}`.length - setCarretPosition(inputRef.current.selectionStart) + setCarretPosition(textBoxRef.current.selectionStart) }, 100) } const handleKeyUp = () => { - if (!inputRef.current?.selectionStart) return - setCarretPosition(inputRef.current.selectionStart) + if (!textBoxRef.current?.selectionStart) return + setCarretPosition(textBoxRef.current.selectionStart) } - const handleChange = (e: React.ChangeEvent) => - setValue(e.target.value) + const handleChange = ( + e: React.ChangeEvent + ) => setValue(e.target.value) return ( - - + diff --git a/apps/builder/components/shared/TextboxWithVariableButton/index.tsx b/apps/builder/components/shared/TextboxWithVariableButton/index.tsx new file mode 100644 index 000000000..db443c541 --- /dev/null +++ b/apps/builder/components/shared/TextboxWithVariableButton/index.tsx @@ -0,0 +1,2 @@ +export { InputWithVariableButton } from './InputWithVariableButton' +export { TextareaWithVariableButton } from './TextareaWithVariableButton' diff --git a/apps/builder/components/theme/ChatSettings/ButtonsTheme.tsx b/apps/builder/components/theme/ChatSettings/ButtonsTheme.tsx index e1c3db744..134cfa1da 100644 --- a/apps/builder/components/theme/ChatSettings/ButtonsTheme.tsx +++ b/apps/builder/components/theme/ChatSettings/ButtonsTheme.tsx @@ -4,13 +4,10 @@ import React from 'react' import { ColorPicker } from '../GeneralSettings/ColorPicker' type Props = { - buttons?: ContainerColors + buttons: ContainerColors onButtonsChange: (buttons: ContainerColors) => void } -const defaultBackgroundColor = '#0042da' -const defaultTextColor = '#ffffff' - export const ButtonsTheme = ({ buttons, onButtonsChange }: Props) => { const handleBackgroundChange = (backgroundColor: string) => onButtonsChange({ ...buttons, backgroundColor }) @@ -22,14 +19,14 @@ export const ButtonsTheme = ({ buttons, onButtonsChange }: Props) => { Background: Text: diff --git a/apps/builder/components/theme/ChatSettings/ChatThemeSettings.tsx b/apps/builder/components/theme/ChatSettings/ChatThemeSettings.tsx index 49b0b1017..e68f2840c 100644 --- a/apps/builder/components/theme/ChatSettings/ChatThemeSettings.tsx +++ b/apps/builder/components/theme/ChatSettings/ChatThemeSettings.tsx @@ -7,7 +7,7 @@ import { HostBubbles } from './HostBubbles' import { InputsTheme } from './InputsTheme' type Props = { - chatTheme?: ChatTheme + chatTheme: ChatTheme onChatThemeChange: (chatTheme: ChatTheme) => void } @@ -26,28 +26,28 @@ export const ChatThemeSettings = ({ chatTheme, onChatThemeChange }: Props) => { Bot bubbles User bubbles Buttons Inputs diff --git a/apps/builder/components/theme/ChatSettings/GuestBubbles.tsx b/apps/builder/components/theme/ChatSettings/GuestBubbles.tsx index 938fb9ea2..5f400339f 100644 --- a/apps/builder/components/theme/ChatSettings/GuestBubbles.tsx +++ b/apps/builder/components/theme/ChatSettings/GuestBubbles.tsx @@ -4,12 +4,10 @@ import React from 'react' import { ColorPicker } from '../GeneralSettings/ColorPicker' type Props = { - guestBubbles?: ContainerColors + guestBubbles: ContainerColors onGuestBubblesChange: (hostBubbles: ContainerColors) => void } -const defaultBackgroundColor = '#ff8e21' -const defaultTextColor = '#ffffff' export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => { const handleBackgroundChange = (backgroundColor: string) => onGuestBubblesChange({ ...guestBubbles, backgroundColor }) @@ -21,14 +19,14 @@ export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => { Background: Text: diff --git a/apps/builder/components/theme/ChatSettings/HostBubbles.tsx b/apps/builder/components/theme/ChatSettings/HostBubbles.tsx index bbee42b07..4333a20cb 100644 --- a/apps/builder/components/theme/ChatSettings/HostBubbles.tsx +++ b/apps/builder/components/theme/ChatSettings/HostBubbles.tsx @@ -4,13 +4,10 @@ import React from 'react' import { ColorPicker } from '../GeneralSettings/ColorPicker' type Props = { - hostBubbles?: ContainerColors + hostBubbles: ContainerColors onHostBubblesChange: (hostBubbles: ContainerColors) => void } -const defaultBackgroundColor = '#f7f8ff' -const defaultTextColor = '#303235' - export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => { const handleBackgroundChange = (backgroundColor: string) => onHostBubblesChange({ ...hostBubbles, backgroundColor }) @@ -22,14 +19,14 @@ export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => { Background: Text: diff --git a/apps/builder/components/theme/ChatSettings/InputsTheme.tsx b/apps/builder/components/theme/ChatSettings/InputsTheme.tsx index 935e9d941..04ef1dc77 100644 --- a/apps/builder/components/theme/ChatSettings/InputsTheme.tsx +++ b/apps/builder/components/theme/ChatSettings/InputsTheme.tsx @@ -4,14 +4,10 @@ import React from 'react' import { ColorPicker } from '../GeneralSettings/ColorPicker' type Props = { - inputs?: InputColors + inputs: InputColors onInputsChange: (buttons: InputColors) => void } -const defaultBackgroundColor = '#ffffff' -const defaultTextColor = '#303235' -const defaultPlaceholderColor = '#9095A0' - export const InputsTheme = ({ inputs, onInputsChange }: Props) => { const handleBackgroundChange = (backgroundColor: string) => onInputsChange({ ...inputs, backgroundColor }) @@ -25,21 +21,21 @@ export const InputsTheme = ({ inputs, onInputsChange }: Props) => { Background: Text: Placeholder text: diff --git a/apps/builder/components/theme/GeneralSettings/ColorPicker.tsx b/apps/builder/components/theme/GeneralSettings/ColorPicker.tsx index 633aa230f..b27f5c33a 100644 --- a/apps/builder/components/theme/GeneralSettings/ColorPicker.tsx +++ b/apps/builder/components/theme/GeneralSettings/ColorPicker.tsx @@ -11,7 +11,7 @@ import { Input, Button, } from '@chakra-ui/react' -import React, { useEffect, useState } from 'react' +import React, { ChangeEvent, useEffect, useState } from 'react' const colorsSelection: `#${string}`[] = [ '#264653', @@ -38,6 +38,9 @@ export const ColorPicker = ({ initialColor, onColorChange }: Props) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [color]) + const handleColorChange = (e: ChangeEvent) => + setColor(e.target.value) + return ( @@ -89,9 +92,7 @@ export const ColorPicker = ({ initialColor, onColorChange }: Props) => { aria-label="Color value" size="sm" value={color} - onChange={(e) => { - setColor(e.target.value) - }} + onChange={handleColorChange} /> diff --git a/apps/builder/components/theme/GeneralSettings/GeneralSettings.tsx b/apps/builder/components/theme/GeneralSettings/GeneralSettings.tsx index d18b61aa9..f6867f812 100644 --- a/apps/builder/components/theme/GeneralSettings/GeneralSettings.tsx +++ b/apps/builder/components/theme/GeneralSettings/GeneralSettings.tsx @@ -1,16 +1,14 @@ import { Stack } from '@chakra-ui/react' -import { Background, BackgroundType, GeneralTheme } from 'models' +import { Background, GeneralTheme } from 'models' import React from 'react' import { BackgroundSelector } from './BackgroundSelector' import { FontSelector } from './FontSelector' type Props = { - generalTheme?: GeneralTheme + generalTheme: GeneralTheme onGeneralThemeChange: (general: GeneralTheme) => void } -const defaultFont = 'Open Sans' - export const GeneralSettings = ({ generalTheme, onGeneralThemeChange, @@ -24,11 +22,11 @@ export const GeneralSettings = ({ return ( diff --git a/apps/builder/components/theme/SideMenu.tsx b/apps/builder/components/theme/ThemeSideMenu.tsx similarity index 72% rename from apps/builder/components/theme/SideMenu.tsx rename to apps/builder/components/theme/ThemeSideMenu.tsx index 8c4eee3f0..08a120c3e 100644 --- a/apps/builder/components/theme/SideMenu.tsx +++ b/apps/builder/components/theme/ThemeSideMenu.tsx @@ -17,17 +17,17 @@ import { ChatThemeSettings } from './ChatSettings' import { CustomCssSettings } from './CustomCssSettings/CustomCssSettings' import { GeneralSettings } from './GeneralSettings' -export const SideMenu = () => { +export const ThemeSideMenu = () => { const { typebot, updateTypebot } = useTypebot() const handleChatThemeChange = (chat: ChatTheme) => - updateTypebot({ theme: { ...typebot?.theme, chat } }) + typebot && updateTypebot({ theme: { ...typebot.theme, chat } }) const handleGeneralThemeChange = (general: GeneralTheme) => - updateTypebot({ theme: { ...typebot?.theme, general } }) + typebot && updateTypebot({ theme: { ...typebot.theme, general } }) const handleCustomCssChange = (customCss: string) => - updateTypebot({ theme: { ...typebot?.theme, customCss } }) + typebot && updateTypebot({ theme: { ...typebot.theme, customCss } }) return ( { - + {typebot && ( + + )} @@ -68,10 +70,12 @@ export const SideMenu = () => { - + {typebot && ( + + )} @@ -83,10 +87,12 @@ export const SideMenu = () => { - + {typebot && ( + + )} diff --git a/apps/builder/contexts/UserContext.tsx b/apps/builder/contexts/UserContext.tsx index 08b232ee0..8b95a68f7 100644 --- a/apps/builder/contexts/UserContext.tsx +++ b/apps/builder/contexts/UserContext.tsx @@ -62,9 +62,12 @@ export const UserContext = ({ children }: { children: ReactNode }) => { useEffect(() => { if (!router.isReady) return if (status === 'loading') return - if (status === 'unauthenticated') router.replace('/signin') + if (status === 'unauthenticated' && !isSigningIn()) + router.replace('/signin') }, [status, router]) + const isSigningIn = () => ['/signin', '/register'].includes(router.pathname) + const updateUser = (newUser: Partial) => { if (!isDefined(user)) return setUser({ ...user, ...newUser }) diff --git a/apps/builder/cypress/fixtures/typebots/integrations/googleSheets.json b/apps/builder/cypress/fixtures/typebots/integrations/googleSheets.json index a32e52f59..ee6a36063 100644 --- a/apps/builder/cypress/fixtures/typebots/integrations/googleSheets.json +++ b/apps/builder/cypress/fixtures/typebots/integrations/googleSheets.json @@ -94,12 +94,23 @@ "allIds": ["benDCcLMUWNvi6Fg6CXE9H", "6Tax9rw7L8kmRn9JRD2Mrg"] }, "theme": { - "general": { - "font": "Open Sans", - "background": { "type": "None", "content": "#ffffff" } - } + "chat": { + "inputs": { + "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": { + "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 } }, "publicId": null diff --git a/apps/builder/cypress/fixtures/typebots/integrations/googleSheetsGet.json b/apps/builder/cypress/fixtures/typebots/integrations/googleSheetsGet.json index 9ca1e2cc1..c0fc51813 100644 --- a/apps/builder/cypress/fixtures/typebots/integrations/googleSheetsGet.json +++ b/apps/builder/cypress/fixtures/typebots/integrations/googleSheetsGet.json @@ -132,12 +132,23 @@ ] }, "theme": { - "general": { - "font": "Open Sans", - "background": { "type": "None", "content": "#ffffff" } - } + "chat": { + "inputs": { + "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": { + "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 } }, "publicId": null diff --git a/apps/builder/cypress/fixtures/typebots/integrations/webhook.json b/apps/builder/cypress/fixtures/typebots/integrations/webhook.json index d7ac6c320..a2c37c1d9 100644 --- a/apps/builder/cypress/fixtures/typebots/integrations/webhook.json +++ b/apps/builder/cypress/fixtures/typebots/integrations/webhook.json @@ -138,12 +138,23 @@ "allIds": ["25yX9DnQgdafpdAjfAu5Fp", "oxEEtym3NfDf34NCipzjRQ"] }, "theme": { - "general": { - "font": "Open Sans", - "background": { "type": "None", "content": "#ffffff" } - } + "chat": { + "inputs": { + "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": { + "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 } }, "publicId": null diff --git a/apps/builder/cypress/fixtures/typebots/integrations/webhookPreview.json b/apps/builder/cypress/fixtures/typebots/integrations/webhookPreview.json index 631a48a17..a941e34dc 100644 --- a/apps/builder/cypress/fixtures/typebots/integrations/webhookPreview.json +++ b/apps/builder/cypress/fixtures/typebots/integrations/webhookPreview.json @@ -252,12 +252,23 @@ ] }, "theme": { - "general": { - "font": "Open Sans", - "background": { "type": "None", "content": "#ffffff" } - } + "chat": { + "inputs": { + "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": { + "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 } }, "publicId": null diff --git a/apps/builder/cypress/fixtures/typebots/logic/condition.json b/apps/builder/cypress/fixtures/typebots/logic/condition.json index e2d3be1a7..7404b46c8 100644 --- a/apps/builder/cypress/fixtures/typebots/logic/condition.json +++ b/apps/builder/cypress/fixtures/typebots/logic/condition.json @@ -235,12 +235,23 @@ ] }, "theme": { - "general": { - "font": "Open Sans", - "background": { "type": "None", "content": "#ffffff" } - } + "chat": { + "inputs": { + "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": { + "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 } }, "publicId": null diff --git a/apps/builder/cypress/fixtures/typebots/logic/redirect.json b/apps/builder/cypress/fixtures/typebots/logic/redirect.json index bb50d7c4e..3ec1cb2e9 100644 --- a/apps/builder/cypress/fixtures/typebots/logic/redirect.json +++ b/apps/builder/cypress/fixtures/typebots/logic/redirect.json @@ -52,7 +52,8 @@ "sqNGop2aYkXRvJqb9nGtFbD": { "id": "sqNGop2aYkXRvJqb9nGtFbD", "blockId": "bnsxmer7DD2R9DogoXTsvHJ", - "type": "Redirect" + "type": "Redirect", + "options": { "isNewTab": false } } }, "allIds": [ @@ -96,12 +97,23 @@ "allIds": ["totLsWG6AQfcFT39CsZwDy", "7KgqWB88ufzhDwzvwHuEbN"] }, "theme": { - "general": { - "font": "Open Sans", - "background": { "type": "None", "content": "#ffffff" } - } + "chat": { + "inputs": { + "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": { + "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 } }, "publicId": null diff --git a/apps/builder/cypress/fixtures/typebots/logic/setVariable.json b/apps/builder/cypress/fixtures/typebots/logic/setVariable.json index f253e4a0f..9574ffb98 100644 --- a/apps/builder/cypress/fixtures/typebots/logic/setVariable.json +++ b/apps/builder/cypress/fixtures/typebots/logic/setVariable.json @@ -54,7 +54,12 @@ "id": "s8n3nJajsBaYqrFeRYVvcf6", "blockId": "bwWRAaX5m6NZyZ9jjpXmWSb", "type": "number input", - "edgeId": "dcJedLC7qsLtsmm1wbiFFc" + "edgeId": "dcJedLC7qsLtsmm1wbiFFc", + "options": { + "labels": { + "placeholder": "Type a number..." + } + } }, "sqMVMXeRYp4inLcRqej2Wac": { "id": "sqMVMXeRYp4inLcRqej2Wac", @@ -71,13 +76,15 @@ "shfL5ueQDuj2RPcJPWZGArT": { "id": "shfL5ueQDuj2RPcJPWZGArT", "blockId": "baUyUnNBxZzPe1z5PqE4NkD", - "type": "Set variable" + "type": "Set variable", + "options": {} }, "sugJ6xN3jFys1CjWfsxGhiJ": { "id": "sugJ6xN3jFys1CjWfsxGhiJ", "blockId": "baUyUnNBxZzPe1z5PqE4NkD", "type": "Set variable", - "edgeId": "sA5gvCVVBVYdGsdeSGF5ei" + "edgeId": "sA5gvCVVBVYdGsdeSGF5ei", + "options": {} }, "shR7ae3iNEvB6arCSu7wVFF": { "id": "shR7ae3iNEvB6arCSu7wVFF", @@ -141,12 +148,23 @@ ] }, "theme": { - "general": { - "font": "Open Sans", - "background": { "type": "None", "content": "#ffffff" } - } + "chat": { + "inputs": { + "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": { + "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 } }, "publicId": null diff --git a/apps/builder/cypress/fixtures/typebots/singleChoiceTarget.json b/apps/builder/cypress/fixtures/typebots/singleChoiceTarget.json index b8724261d..53753b8a2 100644 --- a/apps/builder/cypress/fixtures/typebots/singleChoiceTarget.json +++ b/apps/builder/cypress/fixtures/typebots/singleChoiceTarget.json @@ -154,12 +154,23 @@ ] }, "theme": { - "general": { - "font": "Open Sans", - "background": { "type": "None", "content": "#ffffff" } - } + "chat": { + "inputs": { + "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": { + "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 } }, "publicId": null diff --git a/apps/builder/cypress/fixtures/typebots/theme/theme.json b/apps/builder/cypress/fixtures/typebots/theme/theme.json index 96cdb2bd8..4a6f2d86a 100644 --- a/apps/builder/cypress/fixtures/typebots/theme/theme.json +++ b/apps/builder/cypress/fixtures/typebots/theme/theme.json @@ -136,12 +136,23 @@ "allIds": ["25yX9DnQgdafpdAjfAu5Fp", "6e4Sbp8pGTvBQYtCk2qXbN"] }, "theme": { - "general": { - "font": "Open Sans", - "background": { "type": "None", "content": "#ffffff" } - } + "chat": { + "inputs": { + "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": { + "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 } }, "publicId": null diff --git a/apps/builder/cypress/plugins/data.ts b/apps/builder/cypress/plugins/data.ts index a2a8eb983..c51b9ffdb 100644 --- a/apps/builder/cypress/plugins/data.ts +++ b/apps/builder/cypress/plugins/data.ts @@ -1,7 +1,10 @@ -import { Step, InputStepType } from 'models' +import { Step } from 'models' 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) => { cy.task( @@ -9,21 +12,15 @@ export const createTypebotWithStep = (step: Omit) => { parseTestTypebot({ id: 'typebot3', name: 'Typebot #3', - ownerId: userIds[1], + ownerId: users[1].id, steps: { byId: { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore step1: { ...step, id: 'step1', 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'], @@ -39,13 +36,10 @@ export const createTypebotWithStep = (step: Omit) => { }, allIds: ['block1'], }, - choiceItems: - step.type === InputStepType.CHOICE - ? { - byId: { item1: { stepId: 'step1', id: 'item1' } }, - allIds: ['item1'], - } - : undefined, + choiceItems: { + byId: { item1: { stepId: 'step1', id: 'item1' } }, + allIds: ['item1'], + }, }) ) } diff --git a/apps/builder/cypress/plugins/database.ts b/apps/builder/cypress/plugins/database.ts index cc30caae4..16403a363 100644 --- a/apps/builder/cypress/plugins/database.ts +++ b/apps/builder/cypress/plugins/database.ts @@ -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 { parseTestTypebot } from './utils' -import { userIds } from './data' +import { users } from './data' const prisma = new PrismaClient() @@ -23,10 +29,9 @@ export const createTypebot = (typebot: Typebot) => const createUsers = () => prisma.user.createMany({ data: [ - { id: userIds[0], email: 'test1@gmail.com', emailVerified: new Date() }, + { ...users[0], emailVerified: new Date() }, { - id: userIds[1], - email: 'test2@gmail.com', + ...users[1], emailVerified: new Date(), plan: Plan.PRO, stripeId: 'stripe-test2', @@ -39,7 +44,7 @@ const createCredentials = (refresh_token: string) => data: [ { name: 'test2@gmail.com', - ownerId: userIds[1], + ownerId: users[1].id, type: CredentialsType.GOOGLE_SHEETS, data: { expiry_date: 1642441058842, @@ -53,7 +58,7 @@ const createCredentials = (refresh_token: string) => const createFolders = () => 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 () => { @@ -61,7 +66,7 @@ const createTypebots = async () => { ...parseTestTypebot({ id: 'typebot2', name: 'Typebot #2', - ownerId: userIds[1], + ownerId: users[1].id, blocks: { byId: { block1: { @@ -79,7 +84,8 @@ const createTypebots = async () => { id: 'step1', type: InputStepType.TEXT, blockId: 'block1', - }, + options: defaultTextInputOptions, + } as TextInputStep, }, allIds: ['step1'], }, @@ -91,7 +97,7 @@ const createTypebots = async () => { ...parseTestTypebot({ id: 'typebot1', name: 'Typebot #1', - ownerId: userIds[1], + ownerId: users[1].id, blocks: { byId: {}, allIds: [] }, steps: { byId: {}, allIds: [] }, }), @@ -115,6 +121,7 @@ const createResults = () => { createdAt: new Date( today.setTime(today.getTime() + 1000 * 60 * 60 * 24 * idx) ), + isCompleted: false, } }), ], @@ -153,5 +160,5 @@ const parseTypebotToPublicTypebot = ( export const loadRawTypebotInDatabase = (typebot: Typebot) => prisma.typebot.create({ - data: { ...typebot, id: 'typebot4', ownerId: userIds[1] } as any, + data: { ...typebot, id: 'typebot4', ownerId: users[1].id } as any, }) diff --git a/apps/builder/cypress/plugins/utils.ts b/apps/builder/cypress/plugins/utils.ts index 1493df390..a03fbc180 100644 --- a/apps/builder/cypress/plugins/utils.ts +++ b/apps/builder/cypress/plugins/utils.ts @@ -1,12 +1,11 @@ import { Block, - Theme, - BackgroundType, - Settings, Typebot, Table, Step, ChoiceItem, + defaultTheme, + defaultSettings, } from 'models' export const parseTestTypebot = ({ @@ -24,26 +23,13 @@ export const parseTestTypebot = ({ steps: Table choiceItems?: Table }): 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 { id, folderId: null, name, ownerId, - theme, - settings, + theme: defaultTheme, + settings: defaultSettings, createdAt: new Date(), blocks: { byId: { diff --git a/apps/builder/cypress/support/index.ts b/apps/builder/cypress/support/index.ts index c058aaf5b..b4b952f2f 100644 --- a/apps/builder/cypress/support/index.ts +++ b/apps/builder/cypress/support/index.ts @@ -40,12 +40,25 @@ declare global { } } +const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/ Cypress.on('uncaught:exception', (err) => { - if (err.message.includes('ResizeObserver loop limit exceeded')) { + if (resizeObserverLoopErrRe.test(err.message)) { 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 = () => { return cy .get('#typebot-iframe') @@ -57,6 +70,8 @@ export const getIframeBody = () => { // Import commands.js using ES2015 syntax: import '@testing-library/cypress/add-commands' import 'cypress-file-upload' +import { users } from 'cypress/plugins/data' +import { preventUserFromRefreshing } from 'cypress/plugins/utils' import './commands' // Alternatively you can use CommonJS syntax: diff --git a/apps/builder/cypress/tests/account.ts b/apps/builder/cypress/tests/account.ts index 4f1b035d8..93488d3a9 100644 --- a/apps/builder/cypress/tests/account.ts +++ b/apps/builder/cypress/tests/account.ts @@ -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', () => { before(() => { @@ -11,13 +12,12 @@ describe('Account page', () => { ) }) - beforeEach(() => { - cy.task('seed') - cy.signOut() - }) + beforeEach(prepareDbAndSignIn) + + afterEach(removePreventReload) it('should edit user info properly', () => { - cy.signIn('test1@gmail.com') + cy.signIn(users[0].email) cy.visit('/account') cy.findByRole('button', { name: 'Save' }).should('not.exist') cy.findByRole('textbox', { name: 'Email address' }).should( @@ -35,23 +35,19 @@ describe('Account page', () => { .should('have.attr', 'src') .should( '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.wait('@getUpdatedSession') - cy.reload() - cy.findByRole('textbox', { name: 'Name' }).should('have.value', 'John Doe') - cy.findByRole('img') - .should('have.attr', 'src') - .should( - 'include', - `https://s3.eu-west-3.amazonaws.com/typebot/users/${userIds[0]}/avatar` - ) + .then((interception) => { + return interception.response?.statusCode + }) + .should('eq', 200) cy.findByRole('button', { name: 'Save' }).should('not.exist') }) it('should display valid plans', () => { - cy.signIn('test1@gmail.com') + cy.signIn(users[0].email) cy.visit('/account') cy.findByText('Free plan').should('exist') cy.findByRole('link', { name: 'Manage my subscription' }).should( @@ -59,7 +55,7 @@ describe('Account page', () => { ) cy.findByRole('button', { name: 'Upgrade' }).should('exist') cy.signOut() - cy.signIn('test2@gmail.com') + cy.signIn(users[1].email) cy.visit('/account') cy.findByText('Pro plan').should('exist') cy.findByRole('link', { name: 'Manage my subscription' }) diff --git a/apps/builder/cypress/tests/bubbles/image.ts b/apps/builder/cypress/tests/bubbles/image.ts index 622c5cb60..2d9d11e34 100644 --- a/apps/builder/cypress/tests/bubbles/image.ts +++ b/apps/builder/cypress/tests/bubbles/image.ts @@ -1,7 +1,10 @@ import { createTypebotWithStep } from 'cypress/plugins/data' -import { preventUserFromRefreshing } from 'cypress/plugins/utils' -import { getIframeBody } from 'cypress/support' -import { BubbleStepType, Step } from 'models' +import { + getIframeBody, + prepareDbAndSignIn, + removePreventReload, +} from 'cypress/support' +import { BubbleStepType, defaultImageBubbleContent, Step } from 'models' const unsplashImageSrc = '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', }).as('postImage') }) - afterEach(() => { - cy.window().then((win) => { - win.removeEventListener('beforeunload', preventUserFromRefreshing) - }) - }) + describe('Content settings', () => { beforeEach(() => { - cy.task('seed') + prepareDbAndSignIn() createTypebotWithStep({ type: BubbleStepType.IMAGE, - } as Omit) - cy.signOut() - cy.signIn('test2@gmail.com') + content: defaultImageBubbleContent, + } as Step) cy.visit('/typebots/typebot3/edit') cy.findByText('Click to edit...').click() }) @@ -46,7 +44,6 @@ describe('Image bubbles', () => { cy.findByPlaceholderText('Paste the image link...') .clear() .type(unsplashImageSrc) - cy.findByRole('button', { name: 'Embed image' }).click() cy.findByRole('img') .should('have.attr', 'src') .should('include', unsplashImageSrc) @@ -62,20 +59,18 @@ describe('Image bubbles', () => { }) }) describe('Preview', () => { - beforeEach(() => { - cy.task('seed') + before(() => { + prepareDbAndSignIn() createTypebotWithStep({ type: BubbleStepType.IMAGE, content: { url: unsplashImageSrc, }, } as Omit) - cy.signOut() - cy.signIn('test2@gmail.com') - cy.visit('/typebots/typebot3/edit') }) it('should display correctly', () => { + cy.visit('/typebots/typebot3/edit') cy.findByRole('button', { name: 'Preview' }).click() getIframeBody() .findByRole('img') diff --git a/apps/builder/cypress/tests/bubbles/text.ts b/apps/builder/cypress/tests/bubbles/text.ts index 826d21ddb..8552fce48 100644 --- a/apps/builder/cypress/tests/bubbles/text.ts +++ b/apps/builder/cypress/tests/bubbles/text.ts @@ -1,27 +1,23 @@ import { createTypebotWithStep } from 'cypress/plugins/data' -import { preventUserFromRefreshing } from 'cypress/plugins/utils' -import { getIframeBody } from 'cypress/support' -import { BubbleStepType, Step } from 'models' +import { + getIframeBody, + prepareDbAndSignIn, + removePreventReload, +} from 'cypress/support' +import { BubbleStepType, defaultTextBubbleContent, Step } from 'models' describe('Text bubbles', () => { beforeEach(() => { - cy.task('seed') + prepareDbAndSignIn() createTypebotWithStep({ type: BubbleStepType.TEXT, - content: { html: '', plainText: '', richText: [] }, + content: defaultTextBubbleContent, } as Omit) - cy.signOut() - }) - - afterEach(() => { - cy.window().then((win) => { - win.removeEventListener('beforeunload', preventUserFromRefreshing) - }) + cy.visit('/typebots/typebot3/edit') }) + afterEach(removePreventReload) it('rich text features should work', () => { - cy.signIn('test2@gmail.com') - cy.visit('/typebots/typebot3/edit') cy.findByTestId('bold-button').click() cy.findByRole('textbox', { name: 'Text editor' }).type('Bold text{enter}') cy.findByTestId('bold-button').click() diff --git a/apps/builder/cypress/tests/bubbles/video.ts b/apps/builder/cypress/tests/bubbles/video.ts index 6bec574f6..4af14006e 100644 --- a/apps/builder/cypress/tests/bubbles/video.ts +++ b/apps/builder/cypress/tests/bubbles/video.ts @@ -1,7 +1,15 @@ import { createTypebotWithStep } from 'cypress/plugins/data' -import { preventUserFromRefreshing } from 'cypress/plugins/utils' -import { getIframeBody } from 'cypress/support' -import { BubbleStepType, Step, VideoBubbleContentType } from 'models' +import { + getIframeBody, + prepareDbAndSignIn, + removePreventReload, +} from 'cypress/support' +import { + BubbleStepType, + defaultVideoBubbleContent, + Step, + VideoBubbleContentType, +} from 'models' const videoSrc = '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' describe('Video bubbles', () => { - afterEach(() => { - cy.window().then((win) => { - win.removeEventListener('beforeunload', preventUserFromRefreshing) - }) - }) describe('Content settings', () => { beforeEach(() => { - cy.task('seed') + prepareDbAndSignIn() createTypebotWithStep({ type: BubbleStepType.VIDEO, + content: defaultVideoBubbleContent, } as Omit) - cy.signOut() }) + afterEach(removePreventReload) it('upload image file correctly', () => { - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot3/edit') cy.findByText('Click to edit...').click() cy.findByPlaceholderText('Paste the video link...').type(videoSrc, { @@ -53,10 +56,8 @@ describe('Video bubbles', () => { }) describe('Preview', () => { - beforeEach(() => { - cy.task('seed') - cy.signOut() - }) + beforeEach(prepareDbAndSignIn) + afterEach(removePreventReload) it('should display video correctly', () => { createTypebotWithStep({ @@ -66,7 +67,6 @@ describe('Video bubbles', () => { url: videoSrc, }, } as Omit) - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot3/edit') cy.findByRole('button', { name: 'Preview' }).click() getIframeBody() @@ -84,7 +84,6 @@ describe('Video bubbles', () => { id: 'dQw4w9WgXcQ', }, } as Omit) - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot3/edit') cy.findByRole('button', { name: 'Preview' }).click() getIframeBody() @@ -103,7 +102,6 @@ describe('Video bubbles', () => { id: '649301125', }, } as Omit) - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot3/edit') cy.findByRole('button', { name: 'Preview' }).click() getIframeBody() diff --git a/apps/builder/cypress/tests/dashboard.ts b/apps/builder/cypress/tests/dashboard.ts index a4d0fbe6e..1221d0a91 100644 --- a/apps/builder/cypress/tests/dashboard.ts +++ b/apps/builder/cypress/tests/dashboard.ts @@ -1,11 +1,13 @@ +import { users } from 'cypress/plugins/data' +import { prepareDbAndSignIn, removePreventReload } from 'cypress/support' + describe('Dashboard page', () => { - beforeEach(() => { - cy.task('seed') - cy.signOut() - }) + beforeEach(prepareDbAndSignIn) + + afterEach(removePreventReload) it('folders navigation should work', () => { - cy.signIn('test1@gmail.com') + cy.signIn(users[0].email) cy.visit('/typebots') createFolder('My folder #1') cy.findByTestId('folder-button').click() @@ -27,7 +29,6 @@ describe('Dashboard page', () => { }) it('folders and typebots should be deletable', () => { - cy.signIn('test2@gmail.com') cy.visit('/typebots') cy.findByText('Folder #1').should('exist') cy.findAllByRole('button', { name: 'Show folder menu' }).first().click() @@ -42,7 +43,6 @@ describe('Dashboard page', () => { }) it('folders should be draggable and droppable', () => { - cy.signIn('test2@gmail.com') cy.visit('/typebots') cy.findByTestId('typebot-button-typebot1').mouseMoveBy(-100, 0, { delay: 120, diff --git a/apps/builder/cypress/tests/inputs/button.ts b/apps/builder/cypress/tests/inputs/button.ts index 2c091c76c..685498020 100644 --- a/apps/builder/cypress/tests/inputs/button.ts +++ b/apps/builder/cypress/tests/inputs/button.ts @@ -1,16 +1,22 @@ import { createTypebotWithStep } from 'cypress/plugins/data' -import { getIframeBody } from 'cypress/support' -import { InputStepType } from 'models' +import { + getIframeBody, + prepareDbAndSignIn, + removePreventReload, +} from 'cypress/support' +import { defaultChoiceInputOptions, InputStepType, Step } from 'models' describe('Button input', () => { beforeEach(() => { - cy.task('seed') - createTypebotWithStep({ type: InputStepType.CHOICE }) - cy.signOut() + prepareDbAndSignIn() + createTypebotWithStep({ + type: InputStepType.CHOICE, + options: { ...defaultChoiceInputOptions, itemIds: ['item1'] }, + } as Step) }) + afterEach(removePreventReload) it('Can edit choice items', () => { - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot3/edit') cy.findByDisplayValue('Click to edit').type('Item 1{enter}') cy.findByText('Item 1').trigger('mouseover') @@ -46,7 +52,6 @@ describe('Button input', () => { it('Single choice targets should work', () => { cy.loadTypebotFixtureInDatabase('typebots/singleChoiceTarget.json') - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot4/edit') cy.findByRole('button', { name: 'Preview' }).click() getIframeBody().findByRole('button', { name: 'Burgers' }).click() diff --git a/apps/builder/cypress/tests/inputs/date.ts b/apps/builder/cypress/tests/inputs/date.ts index 40ca77467..2da08ae66 100644 --- a/apps/builder/cypress/tests/inputs/date.ts +++ b/apps/builder/cypress/tests/inputs/date.ts @@ -1,16 +1,22 @@ import { createTypebotWithStep } from 'cypress/plugins/data' -import { getIframeBody } from 'cypress/support' -import { InputStepType } from 'models' +import { + getIframeBody, + prepareDbAndSignIn, + removePreventReload, +} from 'cypress/support' +import { defaultDateInputOptions, InputStepType, Step } from 'models' describe('Date input', () => { beforeEach(() => { - cy.task('seed') - createTypebotWithStep({ type: InputStepType.DATE }) - cy.signOut() + prepareDbAndSignIn() + createTypebotWithStep({ + type: InputStepType.DATE, + options: defaultDateInputOptions, + } as Step) }) + afterEach(removePreventReload) it('options should work', () => { - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot3/edit') cy.findByRole('button', { name: 'Preview' }).click() getIframeBody() diff --git a/apps/builder/cypress/tests/inputs/email.ts b/apps/builder/cypress/tests/inputs/email.ts index dbedc33a8..67da821e2 100644 --- a/apps/builder/cypress/tests/inputs/email.ts +++ b/apps/builder/cypress/tests/inputs/email.ts @@ -1,16 +1,22 @@ import { createTypebotWithStep } from 'cypress/plugins/data' -import { getIframeBody } from 'cypress/support' -import { InputStepType } from 'models' +import { + getIframeBody, + prepareDbAndSignIn, + removePreventReload, +} from 'cypress/support' +import { defaultEmailInputOptions, InputStepType, Step } from 'models' describe('Email input', () => { beforeEach(() => { - cy.task('seed') - createTypebotWithStep({ type: InputStepType.EMAIL }) - cy.signOut() + prepareDbAndSignIn() + createTypebotWithStep({ + type: InputStepType.EMAIL, + options: defaultEmailInputOptions, + } as Step) }) + afterEach(removePreventReload) it('options should work', () => { - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot3/edit') cy.findByRole('button', { name: 'Preview' }).click() getIframeBody() @@ -18,7 +24,9 @@ describe('Email input', () => { .should('have.attr', 'type') .should('equal', 'email') 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') cy.findByTestId('step-step1').click({ force: true }) cy.findByRole('textbox', { name: 'Placeholder:' }) diff --git a/apps/builder/cypress/tests/inputs/number.ts b/apps/builder/cypress/tests/inputs/number.ts index eb18715a3..74dc174b6 100644 --- a/apps/builder/cypress/tests/inputs/number.ts +++ b/apps/builder/cypress/tests/inputs/number.ts @@ -1,20 +1,26 @@ import { createTypebotWithStep } from 'cypress/plugins/data' -import { getIframeBody } from 'cypress/support' -import { InputStepType } from 'models' +import { + getIframeBody, + prepareDbAndSignIn, + removePreventReload, +} from 'cypress/support' +import { defaultNumberInputOptions, InputStepType, Step } from 'models' describe('Number input', () => { beforeEach(() => { - cy.task('seed') - createTypebotWithStep({ type: InputStepType.NUMBER }) - cy.signOut() + prepareDbAndSignIn() + createTypebotWithStep({ + type: InputStepType.NUMBER, + options: defaultNumberInputOptions, + } as Step) }) + afterEach(removePreventReload) it('options should work', () => { - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot3/edit') cy.findByRole('button', { name: 'Preview' }).click() getIframeBody() - .findByPlaceholderText('Type your answer...') + .findByPlaceholderText(defaultNumberInputOptions.labels.placeholder) .should('have.attr', 'type') .should('equal', 'number') getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled') diff --git a/apps/builder/cypress/tests/inputs/phone.ts b/apps/builder/cypress/tests/inputs/phone.ts index 2c3a59757..47785950b 100644 --- a/apps/builder/cypress/tests/inputs/phone.ts +++ b/apps/builder/cypress/tests/inputs/phone.ts @@ -1,20 +1,26 @@ import { createTypebotWithStep } from 'cypress/plugins/data' -import { getIframeBody } from 'cypress/support' -import { InputStepType } from 'models' +import { + getIframeBody, + prepareDbAndSignIn, + removePreventReload, +} from 'cypress/support' +import { defaultPhoneInputOptions, InputStepType, Step } from 'models' describe('Phone number input', () => { beforeEach(() => { - cy.task('seed') - createTypebotWithStep({ type: InputStepType.PHONE }) - cy.signOut() + prepareDbAndSignIn() + createTypebotWithStep({ + type: InputStepType.PHONE, + options: defaultPhoneInputOptions, + } as Step) }) + afterEach(removePreventReload) it('options should work', () => { - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot3/edit') cy.findByRole('button', { name: 'Preview' }).click() getIframeBody() - .findByPlaceholderText('Your phone number...') + .findByPlaceholderText(defaultPhoneInputOptions.labels.placeholder) .should('have.attr', 'type') .should('eq', 'tel') getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled') diff --git a/apps/builder/cypress/tests/inputs/text.ts b/apps/builder/cypress/tests/inputs/text.ts index 22e13659c..89640528c 100644 --- a/apps/builder/cypress/tests/inputs/text.ts +++ b/apps/builder/cypress/tests/inputs/text.ts @@ -1,27 +1,26 @@ import { createTypebotWithStep } from 'cypress/plugins/data' -import { preventUserFromRefreshing } from 'cypress/plugins/utils' -import { getIframeBody } from 'cypress/support' -import { InputStepType } from 'models' +import { + getIframeBody, + prepareDbAndSignIn, + removePreventReload, +} from 'cypress/support' +import { defaultTextInputOptions, InputStepType, Step } from 'models' describe('Text input', () => { beforeEach(() => { - cy.task('seed') - createTypebotWithStep({ type: InputStepType.TEXT }) - cy.signOut() - }) - - afterEach(() => { - cy.window().then((win) => { - win.removeEventListener('beforeunload', preventUserFromRefreshing) - }) + prepareDbAndSignIn() + createTypebotWithStep({ + type: InputStepType.TEXT, + options: defaultTextInputOptions, + } as Step) }) + afterEach(removePreventReload) it('options should work', () => { - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot3/edit') cy.findByRole('button', { name: 'Preview' }).click() getIframeBody() - .findByPlaceholderText('Type your answer...') + .findByPlaceholderText(defaultTextInputOptions.labels.placeholder) .should('have.attr', 'type') .should('equal', 'text') getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled') diff --git a/apps/builder/cypress/tests/inputs/url.ts b/apps/builder/cypress/tests/inputs/url.ts index aee8e02ff..781bf206a 100644 --- a/apps/builder/cypress/tests/inputs/url.ts +++ b/apps/builder/cypress/tests/inputs/url.ts @@ -1,20 +1,27 @@ import { createTypebotWithStep } from 'cypress/plugins/data' -import { getIframeBody } from 'cypress/support' -import { InputStepType } from 'models' +import { + getIframeBody, + prepareDbAndSignIn, + removePreventReload, +} from 'cypress/support' +import { defaultUrlInputOptions, InputStepType, Step } from 'models' describe('URL input', () => { + afterEach(removePreventReload) + beforeEach(() => { - cy.task('seed') - createTypebotWithStep({ type: InputStepType.URL }) - cy.signOut() + prepareDbAndSignIn() + createTypebotWithStep({ + type: InputStepType.URL, + options: defaultUrlInputOptions, + } as Step) }) it('options should work', () => { - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot3/edit') cy.findByRole('button', { name: 'Preview' }).click() getIframeBody() - .findByPlaceholderText('Type your URL...') + .findByPlaceholderText(defaultUrlInputOptions.labels.placeholder) .should('have.attr', 'type') .should('eq', 'url') getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled') diff --git a/apps/builder/cypress/tests/integrations/googleAnalytics.ts b/apps/builder/cypress/tests/integrations/googleAnalytics.ts index c1e0f2166..190296a82 100644 --- a/apps/builder/cypress/tests/integrations/googleAnalytics.ts +++ b/apps/builder/cypress/tests/integrations/googleAnalytics.ts @@ -1,22 +1,23 @@ import { createTypebotWithStep } from 'cypress/plugins/data' -import { preventUserFromRefreshing } from 'cypress/plugins/utils' -import { IntegrationStepType } from 'models' +import { prepareDbAndSignIn, removePreventReload } from 'cypress/support' +import { + defaultGoogleAnalyticsOptions, + IntegrationStepType, + Step, +} from 'models' describe('Google Analytics', () => { - beforeEach(() => { - cy.task('seed') - createTypebotWithStep({ type: IntegrationStepType.GOOGLE_ANALYTICS }) - cy.signOut() - }) + afterEach(removePreventReload) - afterEach(() => { - cy.window().then((win) => { - win.removeEventListener('beforeunload', preventUserFromRefreshing) - }) + beforeEach(() => { + prepareDbAndSignIn() + createTypebotWithStep({ + type: IntegrationStepType.GOOGLE_ANALYTICS, + options: defaultGoogleAnalyticsOptions, + } as Step) }) it('can be filled correctly', () => { - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot3/edit') cy.intercept({ url: '/g/collect', diff --git a/apps/builder/cypress/tests/integrations/googleSheets.ts b/apps/builder/cypress/tests/integrations/googleSheets.ts index b1dab4561..a8f1a3f1d 100644 --- a/apps/builder/cypress/tests/integrations/googleSheets.ts +++ b/apps/builder/cypress/tests/integrations/googleSheets.ts @@ -1,16 +1,13 @@ -import { preventUserFromRefreshing } from 'cypress/plugins/utils' -import { getIframeBody } from 'cypress/support' +import { users } from 'cypress/plugins/data' +import { getIframeBody, removePreventReload } from 'cypress/support' describe('Google sheets', () => { - beforeEach(() => { - cy.task('seed', Cypress.env('GOOGLE_SHEETS_REFRESH_TOKEN')) - cy.signOut() - }) + afterEach(removePreventReload) - afterEach(() => { - cy.window().then((win) => { - win.removeEventListener('beforeunload', preventUserFromRefreshing) - }) + beforeEach(() => { + cy.signOut() + cy.task('seed', Cypress.env('GOOGLE_SHEETS_REFRESH_TOKEN')) + cy.signIn(users[1].email) }) it('Insert row should work', () => { @@ -19,7 +16,6 @@ describe('Google sheets', () => { method: 'POST', }).as('insertRowInGoogleSheets') cy.loadTypebotFixtureInDatabase('typebots/integrations/googleSheets.json') - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot4/edit') fillInSpreadsheetInfo() @@ -55,7 +51,6 @@ describe('Google sheets', () => { method: 'PATCH', }).as('updateRowInGoogleSheets') cy.loadTypebotFixtureInDatabase('typebots/integrations/googleSheets.json') - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot4/edit') fillInSpreadsheetInfo() @@ -87,7 +82,6 @@ describe('Google sheets', () => { cy.loadTypebotFixtureInDatabase( 'typebots/integrations/googleSheetsGet.json' ) - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot4/edit') fillInSpreadsheetInfo() diff --git a/apps/builder/cypress/tests/integrations/webhooks.ts b/apps/builder/cypress/tests/integrations/webhooks.ts index 8319aa2dd..39a15a329 100644 --- a/apps/builder/cypress/tests/integrations/webhooks.ts +++ b/apps/builder/cypress/tests/integrations/webhooks.ts @@ -1,22 +1,17 @@ -import { preventUserFromRefreshing } from 'cypress/plugins/utils' -import { getIframeBody } from 'cypress/support' +import { + getIframeBody, + prepareDbAndSignIn, + removePreventReload, +} from 'cypress/support' describe('Webhook step', () => { - beforeEach(() => { - cy.task('seed') - cy.signOut() - }) + beforeEach(prepareDbAndSignIn) - afterEach(() => { - cy.window().then((win) => { - win.removeEventListener('beforeunload', preventUserFromRefreshing) - }) - }) + afterEach(removePreventReload) describe('Configuration', () => { it('configuration is working', () => { cy.loadTypebotFixtureInDatabase('typebots/integrations/webhook.json') - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot4/edit') cy.findByText('Configure...').click() cy.findByRole('button', { name: 'GET' }).click() @@ -80,7 +75,6 @@ describe('Webhook step', () => { cy.loadTypebotFixtureInDatabase( 'typebots/integrations/webhookPreview.json' ) - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot4/edit') cy.findByRole('button', { name: 'Preview' }).click() getIframeBody().findByRole('button', { name: 'Go' }).click() diff --git a/apps/builder/cypress/tests/logic/condition.ts b/apps/builder/cypress/tests/logic/condition.ts index 4a7bec8c9..9406e7395 100644 --- a/apps/builder/cypress/tests/logic/condition.ts +++ b/apps/builder/cypress/tests/logic/condition.ts @@ -1,21 +1,16 @@ -import { preventUserFromRefreshing } from 'cypress/plugins/utils' -import { getIframeBody } from 'cypress/support' +import { + getIframeBody, + prepareDbAndSignIn, + removePreventReload, +} from 'cypress/support' describe('Condition step', () => { - beforeEach(() => { - cy.task('seed') - cy.signOut() - }) + beforeEach(prepareDbAndSignIn) - afterEach(() => { - cy.window().then((win) => { - win.removeEventListener('beforeunload', preventUserFromRefreshing) - }) - }) + afterEach(removePreventReload) it('options should work', () => { cy.loadTypebotFixtureInDatabase('typebots/logic/condition.json') - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot4/edit') cy.findAllByText('Equal to').first().click() diff --git a/apps/builder/cypress/tests/logic/redirect.ts b/apps/builder/cypress/tests/logic/redirect.ts index eea4bbce1..63ed98749 100644 --- a/apps/builder/cypress/tests/logic/redirect.ts +++ b/apps/builder/cypress/tests/logic/redirect.ts @@ -1,21 +1,16 @@ -import { preventUserFromRefreshing } from 'cypress/plugins/utils' -import { getIframeBody } from 'cypress/support' +import { + getIframeBody, + prepareDbAndSignIn, + removePreventReload, +} from 'cypress/support' describe('Redirect', () => { - beforeEach(() => { - cy.task('seed') - cy.signOut() - }) + beforeEach(prepareDbAndSignIn) - afterEach(() => { - cy.window().then((win) => { - win.removeEventListener('beforeunload', preventUserFromRefreshing) - }) - }) + afterEach(removePreventReload) it('should redirect to URL correctly', () => { cy.loadTypebotFixtureInDatabase('typebots/logic/redirect.json') - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot4/edit') cy.findByText('Configure...').click() cy.findByPlaceholderText('Type a URL...').type('google.com') diff --git a/apps/builder/cypress/tests/logic/setVariable.ts b/apps/builder/cypress/tests/logic/setVariable.ts index 050bf5a3a..00ce6f391 100644 --- a/apps/builder/cypress/tests/logic/setVariable.ts +++ b/apps/builder/cypress/tests/logic/setVariable.ts @@ -1,23 +1,18 @@ -import { preventUserFromRefreshing } from 'cypress/plugins/utils' -import { getIframeBody } from 'cypress/support' +import { + getIframeBody, + prepareDbAndSignIn, + removePreventReload, +} from 'cypress/support' describe('Set variables', () => { - beforeEach(() => { - cy.task('seed') - cy.signOut() - }) + beforeEach(prepareDbAndSignIn) - afterEach(() => { - cy.window().then((win) => { - win.removeEventListener('beforeunload', preventUserFromRefreshing) - }) - }) + afterEach(removePreventReload) - it('options should work', () => { + it.only('options should work', () => { cy.loadTypebotFixtureInDatabase('typebots/logic/setVariable.json') - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot4/edit') - cy.findByText('Type your answer...').click() + cy.findByText('Type a number...').click() cy.createVariable('Num') cy.findAllByText('Click to edit...').first().click() cy.createVariable('Total') @@ -31,9 +26,7 @@ describe('Set variables', () => { 'Custom value' ) cy.findByRole('button', { name: 'Preview' }).click() - getIframeBody() - .findByPlaceholderText('Type your answer...') - .type('365{enter}') + getIframeBody().findByPlaceholderText('Type a number...').type('365{enter}') getIframeBody().findByText('Total: 365000').should('exist') getIframeBody().findByText('Custom var: Custom value') }) diff --git a/apps/builder/cypress/tests/results.ts b/apps/builder/cypress/tests/results.ts index 5eabb5881..0901bcdd3 100644 --- a/apps/builder/cypress/tests/results.ts +++ b/apps/builder/cypress/tests/results.ts @@ -1,19 +1,21 @@ import path from 'path' import { parse } from 'papaparse' +import { prepareDbAndSignIn, removePreventReload } from 'cypress/support' + +const downloadsFolder = Cypress.config('downloadsFolder') describe('Results page', () => { beforeEach(() => { + prepareDbAndSignIn() cy.intercept({ url: '/api/typebots/typebot2/results*', method: 'GET' }).as( 'getResults' ) - cy.task('seed') - cy.signOut() }) + afterEach(removePreventReload) + it('results should be deletable', () => { - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot2/results') - cy.wait('@getResults') cy.findByText('content198').should('exist') cy.findByText('content197').should('exist') cy.findAllByRole('checkbox').eq(2).check({ force: true }) @@ -30,7 +32,6 @@ describe('Results page', () => { }) it('submissions table should have infinite scroll', () => { - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot2/results') cy.findByText('content50').should('not.exist') cy.findByText('content199').should('exist') @@ -44,8 +45,6 @@ describe('Results page', () => { }) it('should correctly export selection in CSV', () => { - const downloadsFolder = Cypress.config('downloadsFolder') - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot2/results') cy.wait('@getResults') cy.findByRole('button', { name: 'Export' }).should('not.exist') diff --git a/apps/builder/cypress/tests/settings/general.ts b/apps/builder/cypress/tests/settings/general.ts new file mode 100644 index 000000000..5faa35067 --- /dev/null +++ b/apps/builder/cypress/tests/settings/general.ts @@ -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') + }) +}) diff --git a/apps/builder/cypress/tests/settings/metadata.ts b/apps/builder/cypress/tests/settings/metadata.ts new file mode 100644 index 000000000..f684c8cff --- /dev/null +++ b/apps/builder/cypress/tests/settings/metadata.ts @@ -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') + }) +}) diff --git a/apps/builder/cypress/tests/settings/typingEmulation.ts b/apps/builder/cypress/tests/settings/typingEmulation.ts new file mode 100644 index 000000000..f790cdd36 --- /dev/null +++ b/apps/builder/cypress/tests/settings/typingEmulation.ts @@ -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') + }) +}) diff --git a/apps/builder/cypress/tests/theme/chat.ts b/apps/builder/cypress/tests/theme/chat.ts index d351fdd26..4691c40a3 100644 --- a/apps/builder/cypress/tests/theme/chat.ts +++ b/apps/builder/cypress/tests/theme/chat.ts @@ -1,14 +1,16 @@ -import { getIframeBody } from 'cypress/support' +import { + getIframeBody, + prepareDbAndSignIn, + removePreventReload, +} from 'cypress/support' describe('General theme settings', () => { - beforeEach(() => { - cy.task('seed') - cy.signOut() - }) + beforeEach(prepareDbAndSignIn) + + afterEach(removePreventReload) it('should reflect changes in real time', () => { cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json') - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot4/theme') getIframeBody().findByText('Ready?').should('exist') cy.findByRole('button', { name: 'Chat' }).click() @@ -56,9 +58,6 @@ describe('General theme settings', () => { .eq(3) .click({ force: true }) 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() .findByTestId('guest-bubble') @@ -68,8 +67,17 @@ describe('General theme settings', () => { .findByTestId('guest-bubble') .should('have.css', 'color') .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' }) .eq(7) .click({ force: true }) @@ -85,6 +93,6 @@ describe('General theme settings', () => { getIframeBody() .findByTestId('input') .should('have.css', 'color') - .should('eq', 'rgb(2, 61, 138)') + .should('eq', 'rgb(2, 62, 138)') }) }) diff --git a/apps/builder/cypress/tests/theme/customCss.ts b/apps/builder/cypress/tests/theme/customCss.ts index 2fb1da2d4..b237610a6 100644 --- a/apps/builder/cypress/tests/theme/customCss.ts +++ b/apps/builder/cypress/tests/theme/customCss.ts @@ -1,14 +1,16 @@ -import { getIframeBody } from 'cypress/support' +import { + getIframeBody, + prepareDbAndSignIn, + removePreventReload, +} from 'cypress/support' describe('Custom CSS settings', () => { - beforeEach(() => { - cy.task('seed') - cy.signOut() - }) + beforeEach(prepareDbAndSignIn) + + afterEach(removePreventReload) it('should reflect changes in real time', () => { cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json') - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot4/theme') cy.findByRole('button', { name: 'Custom CSS' }).click() diff --git a/apps/builder/cypress/tests/theme/general.ts b/apps/builder/cypress/tests/theme/general.ts index b50b0636f..5a8dba1a1 100644 --- a/apps/builder/cypress/tests/theme/general.ts +++ b/apps/builder/cypress/tests/theme/general.ts @@ -1,14 +1,16 @@ -import { getIframeBody } from 'cypress/support' +import { + getIframeBody, + prepareDbAndSignIn, + removePreventReload, +} from 'cypress/support' describe('General theme settings', () => { - beforeEach(() => { - cy.task('seed') - cy.signOut() - }) + beforeEach(prepareDbAndSignIn) + + afterEach(removePreventReload) it('should reflect changes in real time', () => { cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json') - cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot4/theme') cy.findByRole('button', { name: 'General' }).click() diff --git a/apps/builder/layouts/settings/SettingsContent.tsx b/apps/builder/layouts/settings/SettingsContent.tsx new file mode 100644 index 000000000..c4c66f84b --- /dev/null +++ b/apps/builder/layouts/settings/SettingsContent.tsx @@ -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 ( + + + + {publicTypebot && } + + + ) +} diff --git a/apps/builder/layouts/theme/ThemeContent.tsx b/apps/builder/layouts/theme/ThemeContent.tsx index 38c6ffcf2..ddb32207c 100644 --- a/apps/builder/layouts/theme/ThemeContent.tsx +++ b/apps/builder/layouts/theme/ThemeContent.tsx @@ -3,7 +3,7 @@ import { TypebotViewer } from 'bot-engine' import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import React, { useMemo } from 'react' import { parseTypebotToPublicTypebot } from 'services/publicTypebot' -import { SideMenu } from '../../components/theme/SideMenu' +import { ThemeSideMenu } from '../../components/theme/ThemeSideMenu' export const ThemeContent = () => { const { typebot } = useTypebot() @@ -14,7 +14,7 @@ export const ThemeContent = () => { ) return ( - + {publicTypebot && } diff --git a/apps/builder/pages/api/typebots/[typebotId].ts b/apps/builder/pages/api/typebots/[typebotId].ts index 085c16ad8..d92630d57 100644 --- a/apps/builder/pages/api/typebots/[typebotId].ts +++ b/apps/builder/pages/api/typebots/[typebotId].ts @@ -31,7 +31,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const data = JSON.parse(req.body) const typebots = await prisma.typebot.update({ where: { id: typebotId }, - data, + data: { + ...data, + theme: data.theme ?? undefined, + settings: data.settings ?? undefined, + }, }) return res.send({ typebots }) } diff --git a/apps/builder/pages/typebots/[typebotId]/settings.tsx b/apps/builder/pages/typebots/[typebotId]/settings.tsx index 703f430bc..c736bf701 100644 --- a/apps/builder/pages/typebots/[typebotId]/settings.tsx +++ b/apps/builder/pages/typebots/[typebotId]/settings.tsx @@ -1,6 +1,6 @@ import { Flex } from '@chakra-ui/layout' import { Seo } from 'components/Seo' -import { SettingsContent } from 'components/settings/SettingsContent' +import { SettingsContent } from 'layouts/settings/SettingsContent' import { TypebotHeader } from 'components/shared/TypebotHeader' import React from 'react' diff --git a/apps/builder/public/viewer-preview.png b/apps/builder/public/viewer-preview.png new file mode 100644 index 000000000..895cdb371 Binary files /dev/null and b/apps/builder/public/viewer-preview.png differ diff --git a/apps/builder/services/typebots.ts b/apps/builder/services/typebots.ts index 62f1353b4..f98321890 100644 --- a/apps/builder/services/typebots.ts +++ b/apps/builder/services/typebots.ts @@ -1,23 +1,41 @@ import { Block, - TextBubbleStep, PublicTypebot, StartStep, BubbleStepType, InputStepType, - ChoiceInputStep, LogicStepType, Step, - ConditionStep, - ComparisonOperators, - LogicalOperator, DraggableStepType, 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' -import shortId, { generate } from 'short-uuid' +import shortId from 'short-uuid' import { Typebot } from 'models' import useSWR from 'swr' import { fetcher, toKebabCase } from './utils' +import { isBubbleStepType, stepTypeHasOption } from 'utils' import { deepEqual } from 'fast-equals' import { stringify } from 'qs' import { isChoiceInput, isConditionStep, sendRequest } from 'utils' @@ -114,56 +132,56 @@ export const parseNewStep = ( blockId: string ): DraggableStep => { 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) { - case BubbleStepType.TEXT: { - const textStep: Pick = { - type, - content: { html: '', richText: [], plainText: '' }, - } - return { - id, - blockId, - ...textStep, - } - } - case InputStepType.CHOICE: { - const choiceInput: Pick = { - type, - options: { itemIds: [] }, - } - return { - id, - blockId, - ...choiceInput, - } - } - case LogicStepType.CONDITION: { - const id = generate() - const conditionStep: Pick = { - type, - options: { - comparisons: { - byId: { - [id]: { id, comparisonOperator: ComparisonOperators.EQUAL }, - }, - allIds: [id], - }, - logicalOperator: LogicalOperator.AND, - }, - } - return { - id, - blockId, - ...conditionStep, - } - } - default: { - return { - id, - blockId, - type, - } - } + case BubbleStepType.TEXT: + return defaultTextBubbleContent + case BubbleStepType.IMAGE: + return defaultImageBubbleContent + case BubbleStepType.VIDEO: + return defaultVideoBubbleContent + } +} + +const parseDefaultStepOptions = (type: StepWithOptionsType): StepOptions => { + switch (type) { + case InputStepType.TEXT: + return defaultTextInputOptions + case InputStepType.NUMBER: + return defaultNumberInputOptions + case InputStepType.EMAIL: + return defaultEmailInputOptions + case InputStepType.DATE: + return defaultDateInputOptions + case InputStepType.PHONE: + return defaultPhoneInputOptions + case InputStepType.URL: + return defaultUrlInputOptions + case InputStepType.CHOICE: + return defaultChoiceInputOptions + case LogicStepType.SET_VARIABLE: + return defaultSetVariablesOptions + case LogicStepType.CONDITION: + return defaultConditionOptions + case LogicStepType.REDIRECT: + return defaultRedirectOptions + case IntegrationStepType.GOOGLE_SHEETS: + return defaultGoogleSheetsOptions + case IntegrationStepType.GOOGLE_ANALYTICS: + return defaultGoogleAnalyticsOptions + case IntegrationStepType.WEBHOOK: + return defaultWebhookOptions } } @@ -223,6 +241,8 @@ export const parseNewTypebot = ({ variables: { byId: {}, allIds: [] }, edges: { byId: {}, allIds: [] }, webhooks: { byId: {}, allIds: [] }, + theme: defaultTheme, + settings: defaultSettings, } } diff --git a/apps/viewer/pages/api/results.ts b/apps/viewer/pages/api/results.ts index 80192340a..59d255d02 100644 --- a/apps/viewer/pages/api/results.ts +++ b/apps/viewer/pages/api/results.ts @@ -4,9 +4,9 @@ import { methodNotAllowed } from 'utils' const handler = async (req: NextApiRequest, res: NextApiResponse) => { 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({ - data: { typebotId }, + data: { typebotId, isCompleted: false }, }) return res.send(result) } diff --git a/apps/viewer/pages/api/results/[id].ts b/apps/viewer/pages/api/results/[id].ts index a67ace73e..97c84f231 100644 --- a/apps/viewer/pages/api/results/[id].ts +++ b/apps/viewer/pages/api/results/[id].ts @@ -1,10 +1,11 @@ import prisma from 'libs/prisma' +import { Result } from 'models' import { NextApiRequest, NextApiResponse } from 'next' import { methodNotAllowed } from 'utils' const handler = async (req: NextApiRequest, res: NextApiResponse) => { 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 result = await prisma.result.update({ where: { id }, diff --git a/packages/bot-engine/.eslintrc.js b/packages/bot-engine/.eslintrc.js new file mode 100644 index 000000000..01723142b --- /dev/null +++ b/packages/bot-engine/.eslintrc.js @@ -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], + }, +} diff --git a/packages/bot-engine/lib/gtag.ts b/packages/bot-engine/lib/gtag.ts index b2f11d8f7..ea6e9f936 100644 --- a/packages/bot-engine/lib/gtag.ts +++ b/packages/bot-engine/lib/gtag.ts @@ -1,5 +1,6 @@ import { GoogleAnalyticsOptions } from 'models' +// eslint-disable-next-line @typescript-eslint/no-explicit-any declare const gtag: any const initGoogleAnalytics = (id: string): Promise => diff --git a/packages/bot-engine/package.json b/packages/bot-engine/package.json index ab804989f..888508d89 100644 --- a/packages/bot-engine/package.json +++ b/packages/bot-engine/package.json @@ -34,13 +34,20 @@ "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-terser": "^7.0.2", "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": { "react": "^17.0.2" }, "scripts": { "build": "yarn rollup -c", - "dev": "yarn rollup -c --watch" + "dev": "yarn rollup -c --watch", + "lint": "eslint --fix -c ./.eslintrc.js \"./src/**/*.ts*\"" } } diff --git a/packages/bot-engine/src/components/ChatBlock/AvatarSideContainer.tsx b/packages/bot-engine/src/components/ChatBlock/AvatarSideContainer.tsx index a96cbdeef..7407c0d7f 100644 --- a/packages/bot-engine/src/components/ChatBlock/AvatarSideContainer.tsx +++ b/packages/bot-engine/src/components/ChatBlock/AvatarSideContainer.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react' +import React, { useEffect, useState } from 'react' import { useTypebot } from '../../contexts/TypebotContext' import { HostAvatar } from '../avatars/HostAvatar' import { useFrame } from 'react-frame-component' @@ -22,6 +22,7 @@ export const AvatarSideContainer = () => { return () => { resizeObserver.disconnect() } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return ( diff --git a/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx b/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx index 77836fcb6..b774c382f 100644 --- a/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx +++ b/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx @@ -35,11 +35,13 @@ export const ChatBlock = ({ const nextStep = typebot.steps.byId[startStepId ?? stepIds[displayedSteps.length]] if (nextStep) setDisplayedSteps([...displayedSteps, nextStep]) + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { autoScrollToBottom() onNewStepDisplayed() + // eslint-disable-next-line react-hooks/exhaustive-deps }, [displayedSteps]) const onNewStepDisplayed = async () => { diff --git a/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx b/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx index 55a980b02..a018e4bd0 100644 --- a/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx +++ b/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx @@ -42,6 +42,7 @@ const InputChatStep = ({ useEffect(() => { addNewAvatarOffset() + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const handleSubmit = (value: string) => { diff --git a/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/ImageBubble.tsx b/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/ImageBubble.tsx index f0ccde49c..8e973140d 100644 --- a/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/ImageBubble.tsx +++ b/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/ImageBubble.tsx @@ -24,11 +24,12 @@ export const ImageBubble = ({ step, onTransitionEnd }: Props) => { const url = useMemo( () => parseVariables({ text: step.content?.url, variables: typebot.variables }), - [typebot.variables] + [step.content?.url, typebot.variables] ) useEffect(() => { showContentAfterMediaLoad() + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const showContentAfterMediaLoad = () => { @@ -83,6 +84,7 @@ export const ImageBubble = ({ step, onTransitionEnd }: Props) => { height: isTyping ? '2rem' : 'auto', maxWidth: '100%', }} + alt="Bubble image" /> diff --git a/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/TextBubble.tsx b/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/TextBubble.tsx index 590890242..d83f94d43 100644 --- a/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/TextBubble.tsx +++ b/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/TextBubble.tsx @@ -28,6 +28,7 @@ export const TextBubble = ({ step, onTransitionEnd }: Props) => { const content = useMemo( () => parseVariables({ text: step.content.html, variables: typebot.variables }), + // eslint-disable-next-line react-hooks/exhaustive-deps [typebot.variables] ) @@ -40,6 +41,7 @@ export const TextBubble = ({ step, onTransitionEnd }: Props) => { setTimeout(() => { onTypingEnd() }, typingTimeout) + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const onTypingEnd = () => { diff --git a/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/VideoBubble.tsx b/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/VideoBubble.tsx index 03537df75..fef80fe04 100644 --- a/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/VideoBubble.tsx +++ b/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/VideoBubble.tsx @@ -28,6 +28,7 @@ export const VideoBubble = ({ step, onTransitionEnd }: Props) => { useEffect(() => { showContentAfterMediaLoad() + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const showContentAfterMediaLoad = () => { @@ -86,6 +87,7 @@ const VideoContent = ({ }) => { const url = useMemo( () => parseVariables({ text: content?.url, variables: variables }), + // eslint-disable-next-line react-hooks/exhaustive-deps [variables] ) if (!content?.type) return <> diff --git a/packages/bot-engine/src/components/ChatBlock/ChatStep/inputs/ChoiceForm.tsx b/packages/bot-engine/src/components/ChatBlock/ChatStep/inputs/ChoiceForm.tsx index 7b939eebf..8959d48f4 100644 --- a/packages/bot-engine/src/components/ChatBlock/ChatStep/inputs/ChoiceForm.tsx +++ b/packages/bot-engine/src/components/ChatBlock/ChatStep/inputs/ChoiceForm.tsx @@ -13,6 +13,7 @@ export const ChoiceForm = ({ options, onSubmit }: ChoiceFormProps) => { const { typebot } = useTypebot() const items = useMemo( () => filterTable(options?.itemIds ?? [], typebot.choiceItems), + // eslint-disable-next-line react-hooks/exhaustive-deps [] ) const [selectedIds, setSelectedIds] = useState([]) @@ -41,6 +42,7 @@ export const ChoiceForm = ({ options, onSubmit }: ChoiceFormProps) => {
{options?.itemIds.map((itemId) => (
diff --git a/packages/bot-engine/src/contexts/AnswersContext.tsx b/packages/bot-engine/src/contexts/AnswersContext.tsx index 0ca48e1b8..69b4a1c02 100644 --- a/packages/bot-engine/src/contexts/AnswersContext.tsx +++ b/packages/bot-engine/src/contexts/AnswersContext.tsx @@ -4,16 +4,11 @@ import React, { createContext, ReactNode, useContext, useState } from 'react' const answersContext = createContext<{ answers: Answer[] addAnswer: (answer: Answer) => void + // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore }>({}) -export const AnswersContext = ({ - children, - typebotId, -}: { - children: ReactNode - typebotId: string -}) => { +export const AnswersContext = ({ children }: { children: ReactNode }) => { const [answers, setAnswers] = useState([]) const addAnswer = (answer: Answer) => diff --git a/packages/bot-engine/src/contexts/HostAvatarsContext.tsx b/packages/bot-engine/src/contexts/HostAvatarsContext.tsx index 55014e13f..4b5342678 100644 --- a/packages/bot-engine/src/contexts/HostAvatarsContext.tsx +++ b/packages/bot-engine/src/contexts/HostAvatarsContext.tsx @@ -5,6 +5,7 @@ const hostAvatarsContext = createContext<{ lastBubblesTopOffset: number[] addNewAvatarOffset: () => void updateLastAvatarOffset: (newOffset: number) => void + // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore }>({}) diff --git a/packages/bot-engine/src/contexts/TypebotContext.tsx b/packages/bot-engine/src/contexts/TypebotContext.tsx index 78644a436..ee4d4e03e 100644 --- a/packages/bot-engine/src/contexts/TypebotContext.tsx +++ b/packages/bot-engine/src/contexts/TypebotContext.tsx @@ -4,6 +4,7 @@ import React, { createContext, ReactNode, useContext, useState } from 'react' const typebotContext = createContext<{ typebot: PublicTypebot updateVariableValue: (variableId: string, value: string) => void + // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore }>({}) diff --git a/packages/bot-engine/src/services/chat.ts b/packages/bot-engine/src/services/chat.ts index 50d5d2e07..311bb35c5 100644 --- a/packages/bot-engine/src/services/chat.ts +++ b/packages/bot-engine/src/services/chat.ts @@ -1,8 +1,8 @@ -import { TypingEmulationSettings } from 'models' +import { TypingEmulation } from 'models' export const computeTypingTimeout = ( bubbleContent: string, - typingSettings: TypingEmulationSettings + typingSettings: TypingEmulation ) => { const wordCount = bubbleContent.match(/(\w+)/g)?.length ?? 0 const typedWordsPerMinute = typingSettings.speed diff --git a/packages/bot-engine/src/services/inputs.ts b/packages/bot-engine/src/services/inputs.ts index 0938033e6..828857d88 100644 --- a/packages/bot-engine/src/services/inputs.ts +++ b/packages/bot-engine/src/services/inputs.ts @@ -1,4 +1,4 @@ -import { ChoiceInputStep, ChoiceItem, Table, Target } from 'models' +import { ChoiceInputStep, ChoiceItem, Table } from 'models' export const getSingleChoiceTargetId = ( currentStep: ChoiceInputStep, diff --git a/packages/bot-engine/src/services/integration.ts b/packages/bot-engine/src/services/integration.ts index 5b28d2f90..515c784fb 100644 --- a/packages/bot-engine/src/services/integration.ts +++ b/packages/bot-engine/src/services/integration.ts @@ -10,7 +10,6 @@ import { Cell, GoogleSheetsGetOptions, GoogleAnalyticsStep, - Webhook, WebhookStep, } from 'models' import { stringify } from 'qs' diff --git a/packages/bot-engine/src/services/variable.ts b/packages/bot-engine/src/services/variable.ts index 501ea136c..b7f6e11eb 100644 --- a/packages/bot-engine/src/services/variable.ts +++ b/packages/bot-engine/src/services/variable.ts @@ -28,7 +28,7 @@ export const isMathFormula = (str?: string) => ['*', '/', '+', '-'].some((val) => str && str.includes(val)) export const evaluateExpression = (str: string) => { - let result = replaceCommasWithDots(str) + const result = replaceCommasWithDots(str) try { const evaluatedNumber = safeEval(result) as number if (countDecimals(evaluatedNumber) > 2) { diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 85b0a5219..166e285f9 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -138,7 +138,7 @@ model Result { typebotId String typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade) answers Answer[] - isCompleted Boolean? + isCompleted Boolean } model Answer { diff --git a/packages/models/src/publicTypebot.ts b/packages/models/src/publicTypebot.ts index 551a71d87..41ab43eb0 100644 --- a/packages/models/src/publicTypebot.ts +++ b/packages/models/src/publicTypebot.ts @@ -18,6 +18,6 @@ export type PublicTypebot = Omit< choiceItems: Table variables: Table edges: Table - theme?: Theme - settings?: Settings + theme: Theme + settings: Settings } diff --git a/packages/models/src/typebot/settings.ts b/packages/models/src/typebot/settings.ts index 255c24624..8f67f1473 100644 --- a/packages/models/src/typebot/settings.ts +++ b/packages/models/src/typebot/settings.ts @@ -1,9 +1,31 @@ export type Settings = { - typingEmulation?: TypingEmulationSettings + general: GeneralSettings + typingEmulation: TypingEmulation + metadata: Metadata } -export type TypingEmulationSettings = { - enabled?: boolean - speed?: number - maxDelay?: number +export type GeneralSettings = { + isBrandingEnabled: boolean +} + +export type TypingEmulation = { + enabled: boolean + speed: number + maxDelay: number +} + +export type Metadata = { + title?: string + description: string + imageUrl?: string + favIconUrl?: string +} + +export const defaultSettings: Settings = { + general: { isBrandingEnabled: true }, + typingEmulation: { enabled: true, speed: 300, maxDelay: 1.5 }, + 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.', + }, } diff --git a/packages/models/src/typebot/steps/bubble.ts b/packages/models/src/typebot/steps/bubble.ts index 39299191d..dc1a8388f 100644 --- a/packages/models/src/typebot/steps/bubble.ts +++ b/packages/models/src/typebot/steps/bubble.ts @@ -20,12 +20,12 @@ export type TextBubbleStep = StepBase & { export type ImageBubbleStep = StepBase & { type: BubbleStepType.IMAGE - content?: ImageBubbleContent + content: ImageBubbleContent } export type VideoBubbleStep = StepBase & { type: BubbleStepType.VIDEO - content?: VideoBubbleContent + content: VideoBubbleContent } export type TextBubbleContent = { @@ -49,3 +49,13 @@ export type VideoBubbleContent = { url?: string id?: string } + +export const defaultTextBubbleContent: TextBubbleContent = { + html: '', + richText: [], + plainText: '', +} + +export const defaultImageBubbleContent: ImageBubbleContent = {} + +export const defaultVideoBubbleContent: VideoBubbleContent = {} diff --git a/packages/models/src/typebot/steps/inputs.ts b/packages/models/src/typebot/steps/inputs.ts index dc25e96a9..9e3d98738 100644 --- a/packages/models/src/typebot/steps/inputs.ts +++ b/packages/models/src/typebot/steps/inputs.ts @@ -25,37 +25,41 @@ export type InputStepOptions = | EmailInputOptions | DateInputOptions | UrlInputOptions + | PhoneNumberInputOptions + | ChoiceInputOptions export type TextInputStep = StepBase & { type: InputStepType.TEXT - options?: TextInputOptions + options: TextInputOptions } export type NumberInputStep = StepBase & { type: InputStepType.NUMBER - options?: NumberInputOptions + options: NumberInputOptions } export type EmailInputStep = StepBase & { type: InputStepType.EMAIL - options?: EmailInputOptions + options: EmailInputOptions } export type UrlInputStep = StepBase & { type: InputStepType.URL - options?: UrlInputOptions + options: UrlInputOptions } export type DateInputStep = StepBase & { type: InputStepType.DATE - options?: DateInputOptions + options: DateInputOptions } export type PhoneNumberInputStep = StepBase & { type: InputStepType.PHONE - options?: OptionBase & InputTextOptionsBase + options: OptionBase & InputTextOptionsBase } +export type PhoneNumberInputOptions = OptionBase & InputTextOptionsBase + export type ChoiceInputStep = StepBase & { type: InputStepType.CHOICE options: ChoiceInputOptions @@ -70,19 +74,19 @@ export type ChoiceItem = { type OptionBase = { variableId?: string } type InputTextOptionsBase = { - labels?: { placeholder?: string; button?: string } + labels: { placeholder: string; button: string } } export type ChoiceInputOptions = OptionBase & { itemIds: string[] - isMultipleChoice?: boolean - buttonLabel?: string + isMultipleChoice: boolean + buttonLabel: string } export type DateInputOptions = OptionBase & { - labels?: { button?: string; from?: string; to?: string } - hasTime?: boolean - isRange?: boolean + labels: { button: string; from: string; to: string } + hasTime: boolean + isRange: boolean } export type EmailInputOptions = OptionBase & InputTextOptionsBase @@ -91,7 +95,7 @@ export type UrlInputOptions = OptionBase & InputTextOptionsBase export type TextInputOptions = OptionBase & InputTextOptionsBase & { - isLong?: boolean + isLong: boolean } export type NumberInputOptions = OptionBase & @@ -100,3 +104,41 @@ export type NumberInputOptions = OptionBase & max?: number step?: number } + +const defaultButtonLabel = 'Send' + +export const defaultTextInputOptions: TextInputOptions = { + isLong: false, + labels: { button: defaultButtonLabel, placeholder: 'Type your answer...' }, +} + +export const defaultNumberInputOptions: NumberInputOptions = { + labels: { button: defaultButtonLabel, placeholder: 'Type a number...' }, +} + +export const defaultEmailInputOptions: EmailInputOptions = { + labels: { button: defaultButtonLabel, placeholder: 'Type your email...' }, +} + +export const defaultUrlInputOptions: UrlInputOptions = { + labels: { button: defaultButtonLabel, placeholder: 'Type a URL...' }, +} + +export const defaultDateInputOptions: DateInputOptions = { + hasTime: false, + isRange: false, + labels: { button: defaultButtonLabel, from: 'From:', to: 'To:' }, +} + +export const defaultPhoneInputOptions: PhoneNumberInputOptions = { + labels: { + button: defaultButtonLabel, + placeholder: 'Type your phone number...', + }, +} + +export const defaultChoiceInputOptions: ChoiceInputOptions = { + buttonLabel: defaultButtonLabel, + isMultipleChoice: false, + itemIds: [], +} diff --git a/packages/models/src/typebot/steps/integration.ts b/packages/models/src/typebot/steps/integration.ts index b25b5e6dd..ada9d6ec5 100644 --- a/packages/models/src/typebot/steps/integration.ts +++ b/packages/models/src/typebot/steps/integration.ts @@ -19,17 +19,17 @@ export enum IntegrationStepType { export type GoogleSheetsStep = StepBase & { type: IntegrationStepType.GOOGLE_SHEETS - options?: GoogleSheetsOptions + options: GoogleSheetsOptions } export type GoogleAnalyticsStep = StepBase & { type: IntegrationStepType.GOOGLE_ANALYTICS - options?: GoogleAnalyticsOptions + options: GoogleAnalyticsOptions } export type WebhookStep = StepBase & { type: IntegrationStepType.WEBHOOK - options?: WebhookOptions + options: WebhookOptions } export type GoogleAnalyticsOptions = { @@ -113,3 +113,9 @@ export type WebhookResponse = { statusCode: number data?: unknown } + +export const defaultGoogleSheetsOptions: GoogleSheetsOptions = {} + +export const defaultGoogleAnalyticsOptions: GoogleAnalyticsOptions = {} + +export const defaultWebhookOptions: WebhookOptions = {} diff --git a/packages/models/src/typebot/steps/logic.ts b/packages/models/src/typebot/steps/logic.ts index 143af354b..6504ed310 100644 --- a/packages/models/src/typebot/steps/logic.ts +++ b/packages/models/src/typebot/steps/logic.ts @@ -16,7 +16,7 @@ export type LogicStepOptions = export type SetVariableStep = StepBase & { type: LogicStepType.SET_VARIABLE - options?: SetVariableOptions + options: SetVariableOptions } export type ConditionStep = StepBase & { @@ -28,7 +28,7 @@ export type ConditionStep = StepBase & { export type RedirectStep = StepBase & { type: LogicStepType.REDIRECT - options?: RedirectOptions + options: RedirectOptions } export enum LogicalOperator { @@ -47,13 +47,13 @@ export enum ComparisonOperators { export type ConditionOptions = { comparisons: Table - logicalOperator?: LogicalOperator + logicalOperator: LogicalOperator } export type Comparison = { id: string variableId?: string - comparisonOperator: ComparisonOperators + comparisonOperator?: ComparisonOperators value?: string } @@ -64,5 +64,14 @@ export type SetVariableOptions = { export type RedirectOptions = { url?: string - isNewTab?: boolean + isNewTab: boolean } + +export const defaultSetVariablesOptions: SetVariableOptions = {} + +export const defaultConditionOptions: ConditionOptions = { + comparisons: { byId: {}, allIds: [] }, + logicalOperator: LogicalOperator.AND, +} + +export const defaultRedirectOptions: RedirectOptions = { isNewTab: false } diff --git a/packages/models/src/typebot/steps/steps.ts b/packages/models/src/typebot/steps/steps.ts index 3aeef382b..04189eccd 100644 --- a/packages/models/src/typebot/steps/steps.ts +++ b/packages/models/src/typebot/steps/steps.ts @@ -31,6 +31,13 @@ export type DraggableStepType = | LogicStepType | IntegrationStepType +export type StepWithOptions = InputStep | LogicStep | IntegrationStep + +export type StepWithOptionsType = + | InputStepType + | LogicStepType + | IntegrationStepType + export type StepOptions = | InputStepOptions | LogicStepOptions diff --git a/packages/models/src/typebot/theme.ts b/packages/models/src/typebot/theme.ts index 93c6f2555..4cf79b86d 100644 --- a/packages/models/src/typebot/theme.ts +++ b/packages/models/src/typebot/theme.ts @@ -1,28 +1,28 @@ export type Theme = { - general?: GeneralTheme - chat?: ChatTheme + general: GeneralTheme + chat: ChatTheme customCss?: string } export type GeneralTheme = { - font?: string - background?: Background + font: string + background: Background } export type ChatTheme = { - hostBubbles?: ContainerColors - guestBubbles?: ContainerColors - buttons?: ContainerColors - inputs?: InputColors + hostBubbles: ContainerColors + guestBubbles: ContainerColors + buttons: ContainerColors + inputs: InputColors } export type ContainerColors = { - backgroundColor?: string - color?: string + backgroundColor: string + color: string } export type InputColors = ContainerColors & { - placeholderColor?: string + placeholderColor: string } export enum BackgroundType { @@ -35,3 +35,17 @@ export type Background = { type: BackgroundType content?: string } + +export const defaultTheme: Theme = { + chat: { + hostBubbles: { backgroundColor: '#F7F8FF', color: '#303235' }, + guestBubbles: { backgroundColor: '#FF8E21', color: '#FFFFFF' }, + buttons: { backgroundColor: '#0042DA', color: '#FFFFFF' }, + inputs: { + backgroundColor: '#FFFFFF', + color: '#303235', + placeholderColor: '#9095A0', + }, + }, + general: { font: 'Open Sans', background: { type: BackgroundType.NONE } }, +} diff --git a/packages/models/src/typebot/typebot.ts b/packages/models/src/typebot/typebot.ts index f8cd98639..b55ba53d7 100644 --- a/packages/models/src/typebot/typebot.ts +++ b/packages/models/src/typebot/typebot.ts @@ -23,8 +23,8 @@ export type Typebot = Omit< variables: Table edges: Table webhooks: Table - theme?: Theme - settings?: Settings + theme: Theme + settings: Settings } export type Block = { diff --git a/packages/utils/src/utils.ts b/packages/utils/src/utils.ts index 1d17b747b..48b21c5ad 100644 --- a/packages/utils/src/utils.ts +++ b/packages/utils/src/utils.ts @@ -14,6 +14,8 @@ import { TextInputStep, TextBubbleStep, WebhookStep, + StepType, + StepWithOptionsType, } from 'models' export const sendRequest = async ({ @@ -78,3 +80,14 @@ export const isIntegrationStep = (step: Step): step is IntegrationStep => export const isWebhookStep = (step: Step): step is WebhookStep => step.type === IntegrationStepType.WEBHOOK + +export const isBubbleStepType = (type: StepType): type is BubbleStepType => + (Object.values(BubbleStepType) as string[]).includes(type) + +export const stepTypeHasOption = ( + type: StepType +): type is StepWithOptionsType => + (Object.values(InputStepType) as string[]) + .concat(Object.values(LogicStepType)) + .concat(Object.values(IntegrationStepType)) + .includes(type)