2
0

Merge branch 'main' into internal/add-vercel-functions-whatsapp-webhook

This commit is contained in:
Baptiste Arnaud
2024-04-15 12:04:54 +02:00
77 changed files with 2339 additions and 631 deletions

View File

@@ -13,6 +13,7 @@
"format:check": "prettier --check ./src --ignore-path ../../.prettierignore"
},
"dependencies": {
"@typebot.io/theme": "workspace:*",
"@braintree/sanitize-url": "7.0.1",
"@chakra-ui/anatomy": "2.1.1",
"@chakra-ui/react": "2.7.1",

View File

@@ -35,10 +35,16 @@ const colorsSelection: `#${string}`[] = [
type Props = {
value?: string
defaultValue?: string
isDisabled?: boolean
onColorChange: (color: string) => void
}
export const ColorPicker = ({ value, defaultValue, onColorChange }: Props) => {
export const ColorPicker = ({
value,
defaultValue,
isDisabled,
onColorChange,
}: Props) => {
const { t } = useTranslate()
const [color, setColor] = useState(defaultValue ?? '')
const displayedValue = value ?? color
@@ -63,6 +69,7 @@ export const ColorPicker = ({ value, defaultValue, onColorChange }: Props) => {
padding={0}
borderRadius={3}
borderWidth={1}
isDisabled={isDisabled}
>
<Box rounded="full" boxSize="14px" bgColor={displayedValue} />
</Button>

View File

@@ -141,6 +141,8 @@ export const UnsplashPicker = ({ imageSize, onImageSelect }: Props) => {
fetchNewImages(query, 0)
}}
withVariableButton={false}
debounceTimeout={500}
forceDebounce
/>
<Link
isExternal

View File

@@ -24,6 +24,7 @@ import { env } from '@typebot.io/env'
import { MoreInfoTooltip } from '../MoreInfoTooltip'
export type TextInputProps = {
forceDebounce?: boolean
defaultValue?: string
onChange?: (value: string) => void
debounceTimeout?: number
@@ -62,6 +63,7 @@ export const TextInput = forwardRef(function TextInput(
autoComplete,
isDisabled,
autoFocus,
forceDebounce,
onChange: _onChange,
onFocus,
onKeyUp,
@@ -83,7 +85,7 @@ export const TextInput = forwardRef(function TextInput(
const onChange = useDebouncedCallback(
// eslint-disable-next-line @typescript-eslint/no-empty-function
_onChange ?? (() => {}),
env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout
env.NEXT_PUBLIC_E2E_TEST && !forceDebounce ? 0 : debounceTimeout
)
useEffect(() => {

View File

@@ -1,6 +1,7 @@
import { Text } from '@chakra-ui/react'
import { useTranslate } from '@tolgee/react'
import { PaymentInputBlock } from '@typebot.io/schemas'
import { defaultPaymentInputOptions } from '@typebot.io/schemas/features/blocks/inputs/payment/constants'
type Props = {
block: PaymentInputBlock
@@ -9,11 +10,7 @@ type Props = {
export const PaymentInputContent = ({ block }: Props) => {
const { t } = useTranslate()
if (
!block.options?.amount ||
!block.options.credentialsId ||
!block.options.currency
)
if (!block.options?.amount || !block.options.credentialsId)
return (
<Text color="gray.500">
{t('blocks.inputs.payment.placeholder.label')}
@@ -22,7 +19,7 @@ export const PaymentInputContent = ({ block }: Props) => {
return (
<Text noOfLines={1} pr="6">
{t('blocks.inputs.payment.collect.label')} {block.options.amount}{' '}
{block.options.currency}
{block.options.currency ?? defaultPaymentInputOptions.currency}
</Text>
)
}

View File

@@ -163,6 +163,7 @@ export const StripeConfigModal = ({
placeholder="sk_test_..."
withVariableButton={false}
debounceTimeout={0}
type="password"
/>
</HStack>
</Stack>
@@ -187,6 +188,7 @@ export const StripeConfigModal = ({
placeholder="sk_live_..."
withVariableButton={false}
debounceTimeout={0}
type="password"
/>
</FormControl>
</HStack>

View File

@@ -70,7 +70,7 @@ export const CountryCodeSelect = ({ countryCode, onSelect }: Props) => {
<option value="DK">Denmark (+45)</option>
<option value="DJ">Djibouti (+253)</option>
<option value="DM">Dominica (+1809)</option>
<option value="DO">Dominican Republic (+1809)</option>
<option value="DO">Dominican Republic (+18XX)</option>
<option value="EC">Ecuador (+593)</option>
<option value="EG">Egypt (+20)</option>
<option value="SV">El Salvador (+503)</option>

View File

@@ -1,19 +1,16 @@
import { Text } from '@chakra-ui/react'
import { MakeComBlock } from '@typebot.io/schemas'
import { isNotDefined } from '@typebot.io/lib'
type Props = {
block: MakeComBlock
}
export const MakeComContent = ({ block }: Props) => {
const webhook = block.options?.webhook
if (isNotDefined(webhook?.body))
if (!block.options?.webhook?.url)
return <Text color="gray.500">Configure...</Text>
return (
<Text noOfLines={1} pr="6">
{webhook?.url ? 'Trigger scenario' : 'Disabled'}
Trigger scenario
</Text>
)
}

View File

@@ -1,19 +1,16 @@
import { Text } from '@chakra-ui/react'
import { PabblyConnectBlock } from '@typebot.io/schemas'
import { isNotDefined } from '@typebot.io/lib'
type Props = {
block: PabblyConnectBlock
}
export const PabblyConnectContent = ({ block }: Props) => {
const webhook = block.options?.webhook
if (isNotDefined(webhook?.body))
if (!block.options?.webhook?.url)
return <Text color="gray.500">Configure...</Text>
return (
<Text noOfLines={1} pr="6">
{webhook?.url ? 'Trigger scenario' : 'Disabled'}
Trigger scenario
</Text>
)
}

View File

@@ -1,19 +1,16 @@
import { Text } from '@chakra-ui/react'
import { ZapierBlock } from '@typebot.io/schemas'
import { isNotDefined } from '@typebot.io/lib'
type Props = {
block: ZapierBlock
}
export const ZapierContent = ({ block }: Props) => {
const webhook = block.options?.webhook
if (isNotDefined(webhook?.body))
if (!block.options?.webhook?.url)
return <Text color="gray.500">Configure...</Text>
return (
<Text noOfLines={1} pr="6">
{webhook?.url ? 'Trigger zap' : 'Disabled'}
Trigger zap
</Text>
)
}

View File

@@ -26,7 +26,7 @@ import { useRouter } from 'next/router'
export const BoardMenuButton = (props: FlexProps) => {
const { query } = useRouter()
const { typebot } = useTypebot()
const { typebot, currentUserMode } = useTypebot()
const { user } = useUser()
const [isDownloading, setIsDownloading] = useState(false)
const { isOpen, onOpen, onClose } = useDisclosure()
@@ -78,9 +78,11 @@ export const BoardMenuButton = (props: FlexProps) => {
<MenuItem icon={<SettingsIcon />} onClick={onOpen}>
{t('editor.graph.menu.editorSettingsItem.label')}
</MenuItem>
<MenuItem icon={<DownloadIcon />} onClick={downloadFlow}>
{t('editor.graph.menu.exportFlowItem.label')}
</MenuItem>
{currentUserMode !== 'guest' ? (
<MenuItem icon={<DownloadIcon />} onClick={downloadFlow}>
{t('editor.graph.menu.exportFlowItem.label')}
</MenuItem>
) : null}
</MenuList>
<EditorSettingsModal isOpen={isOpen} onClose={onClose} />
</Menu>

View File

@@ -73,7 +73,6 @@ export const PreviewDrawer = () => {
right="0"
top={`0`}
h={`100%`}
w={`${width}px`}
bgColor={useColorModeValue('white', 'gray.900')}
borderLeftWidth={'1px'}
shadow="lg"
@@ -82,6 +81,7 @@ export const PreviewDrawer = () => {
onMouseLeave={() => setIsResizeHandleVisible(false)}
p="6"
zIndex={10}
style={{ width: `${width}px` }}
>
<Fade in={isResizeHandleVisible}>
<ResizeHandle

View File

@@ -5,13 +5,13 @@ import { Typebot } from '@typebot.io/schemas'
import { useState } from 'react'
import { BubbleSettings } from '../../../settings/BubbleSettings/BubbleSettings'
import { JavascriptBubbleSnippet } from '../JavascriptBubbleSnippet'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { defaultButtonsBackgroundColor } from '@typebot.io/schemas/features/typebot/theme/constants'
export const parseDefaultBubbleTheme = (typebot?: Typebot) => ({
button: {
backgroundColor:
typebot?.theme.chat?.buttons?.backgroundColor ??
defaultTheme.chat.buttons.backgroundColor,
defaultButtonsBackgroundColor,
},
})

View File

@@ -158,6 +158,10 @@ export const WhatsAppCredentialsModal = ({
setIsVerifying(false)
showToast({
description: 'Could not get system info',
details:
err instanceof Error
? { content: err.message, lang: 'json' }
: undefined,
})
return false
}
@@ -204,7 +208,10 @@ export const WhatsAppCredentialsModal = ({
setIsVerifying(false)
showToast({
description: 'Could not get phone number info',
details: { content: JSON.stringify(err), lang: 'json' },
details:
err instanceof Error
? { content: err.message, lang: 'json' }
: undefined,
})
return false
}

View File

@@ -8,12 +8,17 @@ import {
Link,
Stack,
Text,
Code,
} from '@chakra-ui/react'
import { BubbleProps } from '@typebot.io/nextjs'
import { useState } from 'react'
import { BubbleSettings } from '../../../settings/BubbleSettings/BubbleSettings'
import { parseApiHostValue, parseInitBubbleCode } from '../../../snippetParsers'
import { parseDefaultBubbleTheme } from '../../Javascript/instructions/JavascriptBubbleInstructions'
import packageJson from '../../../../../../../../../../packages/embeds/js/package.json'
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
const typebotCloudLibraryVersion = '0.2'
type Props = {
publicId: string
@@ -52,6 +57,14 @@ export const WordpressBubbleInstructions = ({ publicId }: Props) => {
<ExternalLinkIcon mx="2px" />
</Link>
</ListItem>
<ListItem>
Set <Code>Library version</Code> to{' '}
<Code>
{isCloudProdInstance()
? typebotCloudLibraryVersion
: packageJson.version}
</Code>
</ListItem>
<ListItem>
<Stack spacing={4}>
<BubbleSettings

View File

@@ -7,11 +7,16 @@ import {
Link,
Stack,
Text,
Code,
} from '@chakra-ui/react'
import { useState } from 'react'
import { PopupSettings } from '../../../settings/PopupSettings'
import { parseInitPopupCode } from '../../../snippetParsers/popup'
import { parseApiHostValue } from '../../../snippetParsers'
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
import packageJson from '../../../../../../../../../../packages/embeds/js/package.json'
const typebotCloudLibraryVersion = '0.2'
type Props = {
publicId: string
@@ -42,6 +47,14 @@ export const WordpressPopupInstructions = ({
<ExternalLinkIcon mx="2px" />
</Link>
</ListItem>
<ListItem>
Set <Code>Library version</Code> to{' '}
<Code>
{isCloudProdInstance()
? typebotCloudLibraryVersion
: packageJson.version}
</Code>
</ListItem>
<ListItem>
<Stack spacing={4}>
<PopupSettings

View File

@@ -8,6 +8,7 @@ export const DefaultAvatar = (props: IconProps) => {
fill="none"
xmlns="http://www.w3.org/2000/svg"
boxSize="40px"
borderRadius="full"
data-testid="default-avatar"
{...props}
>

View File

@@ -35,7 +35,7 @@ export const ThemeSideMenu = () => {
typebot &&
updateTypebot({ updates: { theme: { ...typebot.theme, customCss } } })
const selectedTemplate = (
const selectTemplate = (
selectedTemplate: Partial<Pick<ThemeTemplate, 'id' | 'theme'>>
) => {
if (!typebot) return
@@ -56,6 +56,8 @@ export const ThemeSideMenu = () => {
},
})
const templateId = typebot?.selectedThemeTemplateId ?? undefined
return (
<Stack
flex="1"
@@ -84,12 +86,10 @@ export const ThemeSideMenu = () => {
<AccordionPanel pb={12}>
{typebot && (
<ThemeTemplates
selectedTemplateId={
typebot.selectedThemeTemplateId ?? undefined
}
selectedTemplateId={templateId}
currentTheme={typebot.theme}
workspaceId={typebot.workspaceId}
onTemplateSelect={selectedTemplate}
onTemplateSelect={selectTemplate}
/>
)}
</AccordionPanel>
@@ -106,6 +106,7 @@ export const ThemeSideMenu = () => {
<AccordionPanel pb={4}>
{typebot && (
<GeneralSettings
key={templateId}
isBrandingEnabled={
typebot.settings.general?.isBrandingEnabled ??
defaultSettings.general.isBrandingEnabled
@@ -128,9 +129,11 @@ export const ThemeSideMenu = () => {
<AccordionPanel pb={4}>
{typebot && (
<ChatThemeSettings
key={templateId}
workspaceId={typebot.workspaceId}
typebotId={typebot.id}
chatTheme={typebot.theme.chat}
generalBackground={typebot.theme.general?.background}
onChatThemeChange={updateChatTheme}
/>
)}
@@ -147,6 +150,7 @@ export const ThemeSideMenu = () => {
<AccordionPanel pb={4}>
{typebot && (
<CustomCssSettings
key={templateId}
customCss={typebot.theme.customCss}
onCustomCssChange={updateCustomCss}
/>

View File

@@ -19,8 +19,13 @@ import { Theme, ThemeTemplate } from '@typebot.io/schemas'
import { useState } from 'react'
import { DefaultAvatar } from './DefaultAvatar'
import {
defaultButtonsBackgroundColor,
BackgroundType,
defaultTheme,
defaultGuestAvatarIsEnabled,
defaultGuestBubblesBackgroundColor,
defaultHostAvatarIsEnabled,
defaultBackgroundColor,
defaultHostBubblesBackgroundColor,
} from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react'
@@ -71,28 +76,28 @@ export const ThemeTemplateCard = ({
const hostAvatar = {
isEnabled:
themeTemplate.theme.chat?.hostAvatar?.isEnabled ??
defaultTheme.chat.hostAvatar.isEnabled,
defaultHostAvatarIsEnabled,
url: themeTemplate.theme.chat?.hostAvatar?.url,
}
const hostBubbleBgColor =
themeTemplate.theme.chat?.hostBubbles?.backgroundColor ??
defaultTheme.chat.hostBubbles.backgroundColor
defaultHostBubblesBackgroundColor
const guestAvatar = {
isEnabled:
themeTemplate.theme.chat?.guestAvatar?.isEnabled ??
defaultTheme.chat.guestAvatar.isEnabled,
defaultGuestAvatarIsEnabled,
url: themeTemplate.theme.chat?.guestAvatar?.url,
}
const guestBubbleBgColor =
themeTemplate.theme.chat?.guestBubbles?.backgroundColor ??
defaultTheme.chat.guestBubbles.backgroundColor
defaultGuestBubblesBackgroundColor
const buttonBgColor =
themeTemplate.theme.chat?.buttons?.backgroundColor ??
defaultTheme.chat.buttons.backgroundColor
defaultButtonsBackgroundColor
return (
<Stack
@@ -197,8 +202,7 @@ const parseBackground = (
case undefined:
case BackgroundType.COLOR:
return {
backgroundColor:
background?.content ?? defaultTheme.general.background.content,
backgroundColor: background?.content ?? defaultBackgroundColor,
}
case BackgroundType.IMAGE:
return { backgroundImage: `url(${background.content})` }

View File

@@ -0,0 +1,134 @@
import { FormLabel, HStack, Stack } from '@chakra-ui/react'
import { ChatTheme, GeneralTheme } from '@typebot.io/schemas'
import React from 'react'
import {
defaultBlur,
defaultContainerBackgroundColor,
defaultContainerMaxHeight,
defaultContainerMaxWidth,
defaultDarkTextColor,
defaultLightTextColor,
defaultOpacity,
defaultRoundness,
} from '@typebot.io/schemas/features/typebot/theme/constants'
import { ContainerThemeForm } from './ContainerThemeForm'
import { NumberInput } from '@/components/inputs'
import { DropdownList } from '@/components/DropdownList'
import { isChatContainerLight } from '@typebot.io/theme/isChatContainerLight'
type Props = {
generalBackground: GeneralTheme['background']
container: ChatTheme['container']
onContainerChange: (container: ChatTheme['container'] | undefined) => void
}
export const ChatContainerForm = ({
generalBackground,
container,
onContainerChange,
}: Props) => {
const updateMaxWidth = (maxWidth?: number) =>
updateDimension('maxWidth', maxWidth, maxWidthUnit)
const updateMaxWidthUnit = (unit: string) =>
updateDimension('maxWidth', maxWidth, unit)
const updateMaxHeight = (maxHeight?: number) =>
updateDimension('maxHeight', maxHeight, maxHeightUnit)
const updateMaxHeightUnit = (unit: string) =>
updateDimension('maxHeight', maxHeight, unit)
const updateDimension = (
dimension: 'maxWidth' | 'maxHeight',
value: number | undefined,
unit: string
) =>
onContainerChange({
...container,
[dimension]: `${value}${unit}`,
})
const { value: maxWidth, unit: maxWidthUnit } = parseValueAndUnit(
container?.maxWidth ?? defaultContainerMaxWidth
)
const { value: maxHeight, unit: maxHeightUnit } = parseValueAndUnit(
container?.maxHeight ?? defaultContainerMaxHeight
)
return (
<Stack>
<HStack justifyContent="space-between">
<FormLabel mb="0" mr="0">
Max width:
</FormLabel>
<HStack>
<NumberInput
size="sm"
width="100px"
defaultValue={maxWidth}
min={0}
step={10}
withVariableButton={false}
onValueChange={updateMaxWidth}
/>
<DropdownList
size="sm"
items={['px', '%', 'vh', 'vw']}
currentItem={maxWidthUnit}
onItemSelect={updateMaxWidthUnit}
/>
</HStack>
</HStack>
<HStack justifyContent="space-between">
<FormLabel mb="0" mr="0">
Max height:
</FormLabel>
<HStack>
<NumberInput
size="sm"
width="100px"
defaultValue={maxHeight}
min={0}
step={10}
onValueChange={updateMaxHeight}
withVariableButton={false}
/>
<DropdownList
size="sm"
items={['px', '%', 'vh', 'vw']}
currentItem={maxHeightUnit}
onItemSelect={updateMaxHeightUnit}
/>
</HStack>
</HStack>
<ContainerThemeForm
theme={container}
defaultTheme={{
backgroundColor: defaultContainerBackgroundColor,
border: {
roundeness: defaultRoundness,
},
blur: defaultBlur,
opacity: defaultOpacity,
color: isChatContainerLight({
chatContainer: container,
generalBackground,
})
? defaultLightTextColor
: defaultDarkTextColor,
}}
onThemeChange={onContainerChange}
/>
</Stack>
)
}
const parseValueAndUnit = (valueWithUnit: string) => {
const value = parseFloat(valueWithUnit)
const unit = valueWithUnit.replace(value.toString(), '')
return { value, unit }
}

View File

@@ -1,23 +1,36 @@
import {
LargeRadiusIcon,
MediumRadiusIcon,
NoRadiusIcon,
} from '@/components/icons'
import { RadioButtons } from '@/components/inputs/RadioButtons'
import { Heading, Stack } from '@chakra-ui/react'
import { AvatarProps, ChatTheme, Theme } from '@typebot.io/schemas'
import {
AvatarProps,
ChatTheme,
GeneralTheme,
Theme,
} from '@typebot.io/schemas'
import React from 'react'
import { AvatarForm } from './AvatarForm'
import { ButtonsTheme } from './ButtonsTheme'
import { GuestBubbles } from './GuestBubbles'
import { HostBubbles } from './HostBubbles'
import { InputsTheme } from './InputsTheme'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react'
import { ChatContainerForm } from './ChatContainerForm'
import { ContainerThemeForm } from './ContainerThemeForm'
import {
defaultButtonsBackgroundColor,
defaultButtonsColor,
defaultButtonsBorderThickness,
defaultGuestBubblesBackgroundColor,
defaultGuestBubblesColor,
defaultHostBubblesBackgroundColor,
defaultHostBubblesColor,
defaultInputsBackgroundColor,
defaultInputsColor,
defaultInputsPlaceholderColor,
defaultInputsShadow,
defaultOpacity,
defaultBlur,
defaultRoundness,
} from '@typebot.io/schemas/features/typebot/theme/constants'
type Props = {
workspaceId: string
typebotId: string
generalBackground: GeneralTheme['background']
chatTheme: Theme['chat']
onChatThemeChange: (chatTheme: ChatTheme) => void
}
@@ -26,6 +39,7 @@ export const ChatThemeSettings = ({
workspaceId,
typebotId,
chatTheme,
generalBackground,
onChatThemeChange,
}: Props) => {
const { t } = useTranslate()
@@ -44,6 +58,16 @@ export const ChatThemeSettings = ({
const updateInputs = (inputs: NonNullable<Theme['chat']>['inputs']) =>
onChatThemeChange({ ...chatTheme, inputs })
const updateChatContainer = (
container: NonNullable<Theme['chat']>['container']
) => onChatThemeChange({ ...chatTheme, container })
const updateInputsPlaceholderColor = (placeholderColor: string) =>
onChatThemeChange({
...chatTheme,
inputs: { ...chatTheme?.inputs, placeholderColor },
})
const updateHostAvatar = (hostAvatar: AvatarProps) =>
onChatThemeChange({ ...chatTheme, hostAvatar })
@@ -52,6 +76,14 @@ export const ChatThemeSettings = ({
return (
<Stack spacing={6}>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">Container</Heading>
<ChatContainerForm
generalBackground={generalBackground}
container={chatTheme?.container}
onContainerChange={updateChatContainer}
/>
</Stack>
<AvatarForm
uploadFileProps={{
workspaceId,
@@ -59,7 +91,7 @@ export const ChatThemeSettings = ({
fileName: 'hostAvatar',
}}
title={t('theme.sideMenu.chat.botAvatar')}
avatarProps={chatTheme?.hostAvatar ?? defaultTheme.chat.hostAvatar}
avatarProps={chatTheme?.hostAvatar}
isDefaultCheck
onAvatarChange={updateHostAvatar}
/>
@@ -70,63 +102,83 @@ export const ChatThemeSettings = ({
fileName: 'guestAvatar',
}}
title={t('theme.sideMenu.chat.userAvatar')}
avatarProps={chatTheme?.guestAvatar ?? defaultTheme.chat.guestAvatar}
avatarProps={chatTheme?.guestAvatar}
onAvatarChange={updateGuestAvatar}
/>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">{t('theme.sideMenu.chat.botBubbles')}</Heading>
<HostBubbles
hostBubbles={chatTheme?.hostBubbles ?? defaultTheme.chat.hostBubbles}
onHostBubblesChange={updateHostBubbles}
<ContainerThemeForm
testId="hostBubblesTheme"
theme={chatTheme?.hostBubbles}
onThemeChange={updateHostBubbles}
defaultTheme={{
backgroundColor: defaultHostBubblesBackgroundColor,
color: defaultHostBubblesColor,
opacity: defaultOpacity,
blur: defaultBlur,
border: {
roundeness: defaultRoundness,
},
}}
/>
</Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">{t('theme.sideMenu.chat.userBubbles')}</Heading>
<GuestBubbles
guestBubbles={
chatTheme?.guestBubbles ?? defaultTheme.chat.guestBubbles
}
onGuestBubblesChange={updateGuestBubbles}
<ContainerThemeForm
testId="guestBubblesTheme"
theme={chatTheme?.guestBubbles}
onThemeChange={updateGuestBubbles}
defaultTheme={{
backgroundColor: defaultGuestBubblesBackgroundColor,
color: defaultGuestBubblesColor,
opacity: defaultOpacity,
blur: defaultBlur,
border: {
roundeness: defaultRoundness,
},
}}
/>
</Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">{t('theme.sideMenu.chat.buttons')}</Heading>
<ButtonsTheme
buttons={chatTheme?.buttons ?? defaultTheme.chat.buttons}
onButtonsChange={updateButtons}
<ContainerThemeForm
testId="buttonsTheme"
theme={chatTheme?.buttons}
onThemeChange={updateButtons}
defaultTheme={{
backgroundColor: defaultButtonsBackgroundColor,
color: defaultButtonsColor,
opacity: defaultOpacity,
blur: defaultBlur,
border: {
roundeness: defaultRoundness,
thickness: defaultButtonsBorderThickness,
color:
chatTheme?.buttons?.backgroundColor ??
defaultButtonsBackgroundColor,
},
}}
/>
</Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">{t('theme.sideMenu.chat.inputs')}</Heading>
<InputsTheme
inputs={chatTheme?.inputs ?? defaultTheme.chat.inputs}
onInputsChange={updateInputs}
/>
</Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">
{t('theme.sideMenu.chat.cornersRoundness')}
</Heading>
<RadioButtons
options={[
{
label: <NoRadiusIcon />,
value: 'none',
<ContainerThemeForm
testId="inputsTheme"
theme={chatTheme?.inputs}
onThemeChange={updateInputs}
onPlaceholderColorChange={updateInputsPlaceholderColor}
defaultTheme={{
backgroundColor: defaultInputsBackgroundColor,
color: defaultInputsColor,
placeholderColor: defaultInputsPlaceholderColor,
shadow: defaultInputsShadow,
opacity: defaultOpacity,
blur: defaultBlur,
border: {
roundeness: defaultRoundness,
},
{
label: <MediumRadiusIcon />,
value: 'medium',
},
{
label: <LargeRadiusIcon />,
value: 'large',
},
]}
value={chatTheme?.roundness ?? defaultTheme.chat.roundness}
onSelect={(roundness) =>
onChatThemeChange({ ...chatTheme, roundness })
}
}}
/>
</Stack>
</Stack>

View File

@@ -0,0 +1,297 @@
import {
Stack,
FormLabel,
HStack,
Switch,
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
} from '@chakra-ui/react'
import {
ContainerTheme,
ContainerBorderTheme,
InputTheme,
} from '@typebot.io/schemas'
import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
import { useTranslate } from '@tolgee/react'
import { NumberInput } from '@/components/inputs'
import { DropdownList } from '@/components/DropdownList'
import {
borderRoundness,
defaultOpacity,
shadows,
} from '@typebot.io/schemas/features/typebot/theme/constants'
type Props<T extends ((placeholder: string) => void) | undefined> = {
theme: (T extends undefined ? ContainerTheme : InputTheme) | undefined
defaultTheme: T extends undefined ? ContainerTheme : InputTheme
placeholderColor?: T extends undefined ? never : string
testId?: string
onThemeChange: (
theme: T extends undefined ? ContainerTheme : InputTheme
) => void
onPlaceholderColorChange?: T
}
export const ContainerThemeForm = <
T extends ((placeholder: string) => void) | undefined
>({
theme,
testId,
defaultTheme,
onPlaceholderColorChange,
onThemeChange,
}: Props<T>) => {
const { t } = useTranslate()
const updateBackgroundColor = (backgroundColor: string) =>
onThemeChange({ ...theme, backgroundColor })
const toggleBackgroundColor = () =>
onThemeChange({
...theme,
backgroundColor:
backgroundColor === 'transparent' ? '#ffffff' : 'transparent',
})
const updateTextColor = (color: string) => onThemeChange({ ...theme, color })
const updateShadow = (shadow?: ContainerTheme['shadow']) =>
onThemeChange({ ...theme, shadow })
const updateBlur = (blur?: number) => onThemeChange({ ...theme, blur })
const updateOpacity = (opacity?: number) =>
onThemeChange({ ...theme, opacity })
const updateBorder = (border: ContainerBorderTheme) =>
onThemeChange({ ...theme, border })
const updatePlaceholderColor = (color: string) =>
onThemeChange({ ...theme, placeholderColor: color } as InputTheme)
const backgroundColor =
theme?.backgroundColor ?? defaultTheme?.backgroundColor
const shadow = theme?.shadow ?? defaultTheme?.shadow ?? 'none'
return (
<Stack spacing={4} data-testid={testId}>
<HStack justify="space-between">
<FormLabel mb="0" mr="0">
{t('theme.sideMenu.chat.theme.background')}
</FormLabel>
<HStack>
<Switch
defaultChecked={backgroundColor !== 'transparent'}
onChange={toggleBackgroundColor}
/>
<ColorPicker
isDisabled={backgroundColor === 'transparent'}
value={backgroundColor}
onColorChange={updateBackgroundColor}
/>
</HStack>
</HStack>
<HStack justify="space-between">
<FormLabel mb="0" mr="0">
{t('theme.sideMenu.chat.theme.text')}
</FormLabel>
<ColorPicker
value={theme?.color ?? defaultTheme?.color}
onColorChange={updateTextColor}
/>
</HStack>
{onPlaceholderColorChange && (
<HStack justify="space-between">
<FormLabel mb="0" mr="0">
{t('theme.sideMenu.chat.theme.placeholder')}
</FormLabel>
<ColorPicker
value={
theme && 'placeholderColor' in theme
? theme.placeholderColor
: defaultTheme && 'placeholderColor' in defaultTheme
? defaultTheme.placeholderColor
: undefined
}
onColorChange={updatePlaceholderColor}
/>
</HStack>
)}
<Accordion allowToggle>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Border
<AccordionIcon />
</AccordionButton>
<AccordionPanel>
<BorderThemeForm
border={theme?.border}
defaultBorder={defaultTheme.border}
onBorderChange={updateBorder}
/>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Advanced
<AccordionIcon />
</AccordionButton>
<AccordionPanel as={Stack}>
{backgroundColor !== 'transparent' && (
<>
<NumberInput
size="sm"
direction="row"
label="Opacity:"
width="100px"
min={0}
max={1}
step={0.1}
defaultValue={theme?.opacity ?? defaultTheme?.opacity}
onValueChange={updateOpacity}
withVariableButton={false}
/>
{(theme?.opacity ?? defaultTheme?.opacity) !== 1 && (
<NumberInput
size="sm"
direction="row"
label="Blur:"
suffix="px"
width="100px"
min={0}
defaultValue={theme?.blur ?? defaultTheme?.blur}
onValueChange={updateBlur}
withVariableButton={false}
/>
)}
</>
)}
<HStack justify="space-between">
<FormLabel mb="0" mr="0">
Shadow:
</FormLabel>
<HStack>
<DropdownList
currentItem={shadow}
onItemSelect={updateShadow}
items={shadows}
size="sm"
/>
</HStack>
</HStack>
</AccordionPanel>
</AccordionItem>
</Accordion>
</Stack>
)
}
const BorderThemeForm = ({
border,
defaultBorder,
onBorderChange,
}: {
border: ContainerBorderTheme | undefined
defaultBorder: ContainerBorderTheme | undefined
onBorderChange: (border: ContainerBorderTheme) => void
}) => {
const updateRoundness = (roundeness: (typeof borderRoundness)[number]) => {
onBorderChange({ ...border, roundeness })
}
const updateCustomRoundeness = (customRoundeness: number | undefined) => {
onBorderChange({ ...border, customRoundeness })
}
const updateThickness = (thickness: number | undefined) => {
onBorderChange({ ...border, thickness })
}
const updateColor = (color: string | undefined) => {
onBorderChange({ ...border, color })
}
const updateOpacity = (opacity: number | undefined) => {
onBorderChange({ ...border, opacity })
}
const thickness = border?.thickness ?? defaultBorder?.thickness ?? 0
return (
<Stack>
<HStack justifyContent="space-between">
<FormLabel mb="0" mr="0">
Roundness:
</FormLabel>
<HStack>
<DropdownList
currentItem={border?.roundeness ?? defaultBorder?.roundeness}
onItemSelect={updateRoundness}
items={borderRoundness}
placeholder="md"
size="sm"
/>
{(border?.roundeness ?? defaultBorder?.roundeness) === 'custom' && (
<NumberInput
size="sm"
suffix="px"
width="60px"
min={0}
defaultValue={border?.customRoundeness}
onValueChange={updateCustomRoundeness}
withVariableButton={false}
/>
)}
</HStack>
</HStack>
<HStack justifyContent="space-between">
<FormLabel mb="0" mr="0">
Thickness:
</FormLabel>
<NumberInput
size="sm"
suffix="px"
width="60px"
min={0}
defaultValue={thickness}
onValueChange={updateThickness}
withVariableButton={false}
/>
</HStack>
{thickness > 0 && (
<>
<HStack justifyContent="space-between">
<FormLabel mb="0" mr="0">
Color:
</FormLabel>
<ColorPicker
value={border?.color ?? defaultBorder?.color}
onColorChange={updateColor}
/>
</HStack>
<NumberInput
size="sm"
direction="row"
label="Opacity:"
width="100px"
min={0}
max={1}
step={0.1}
defaultValue={border?.opacity ?? defaultOpacity}
onValueChange={updateOpacity}
withVariableButton={false}
/>
</>
)}
</Stack>
)
}

View File

@@ -1,13 +1,16 @@
import { Stack, Flex, Text } from '@chakra-ui/react'
import { ContainerColors } from '@typebot.io/schemas'
import { ContainerTheme } from '@typebot.io/schemas'
import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react'
import {
defaultGuestBubblesBackgroundColor,
defaultGuestBubblesColor,
} from '@typebot.io/schemas/features/typebot/theme/constants'
type Props = {
guestBubbles: ContainerColors | undefined
onGuestBubblesChange: (hostBubbles: ContainerColors | undefined) => void
guestBubbles: ContainerTheme | undefined
onGuestBubblesChange: (hostBubbles: ContainerTheme | undefined) => void
}
export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
@@ -25,8 +28,7 @@ export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
<Text>{t('theme.sideMenu.chat.theme.background')}</Text>
<ColorPicker
value={
guestBubbles?.backgroundColor ??
defaultTheme.chat.guestBubbles.backgroundColor
guestBubbles?.backgroundColor ?? defaultGuestBubblesBackgroundColor
}
onColorChange={updateBackground}
/>
@@ -34,7 +36,7 @@ export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
<Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.chat.theme.text')}</Text>
<ColorPicker
value={guestBubbles?.color ?? defaultTheme.chat.guestBubbles.color}
value={guestBubbles?.color ?? defaultGuestBubblesColor}
onColorChange={updateText}
/>
</Flex>

View File

@@ -1,13 +1,16 @@
import { Stack, Flex, Text } from '@chakra-ui/react'
import { ContainerColors } from '@typebot.io/schemas'
import { ContainerTheme } from '@typebot.io/schemas'
import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react'
import {
defaultHostBubblesBackgroundColor,
defaultHostBubblesColor,
} from '@typebot.io/schemas/features/typebot/theme/constants'
type Props = {
hostBubbles: ContainerColors | undefined
onHostBubblesChange: (hostBubbles: ContainerColors | undefined) => void
hostBubbles: ContainerTheme | undefined
onHostBubblesChange: (hostBubbles: ContainerTheme | undefined) => void
}
export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
@@ -24,8 +27,7 @@ export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
<Text>{t('theme.sideMenu.chat.theme.background')}</Text>
<ColorPicker
value={
hostBubbles?.backgroundColor ??
defaultTheme.chat.hostBubbles.backgroundColor
hostBubbles?.backgroundColor ?? defaultHostBubblesBackgroundColor
}
onColorChange={handleBackgroundChange}
/>
@@ -33,7 +35,7 @@ export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
<Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.chat.theme.text')}</Text>
<ColorPicker
value={hostBubbles?.color ?? defaultTheme.chat.hostBubbles.color}
value={hostBubbles?.color ?? defaultHostBubblesColor}
onColorChange={handleTextChange}
/>
</Flex>

View File

@@ -1,44 +0,0 @@
import { Stack, Flex, Text } from '@chakra-ui/react'
import { InputColors, Theme } from '@typebot.io/schemas'
import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
import { useTranslate } from '@tolgee/react'
type Props = {
inputs: NonNullable<Theme['chat']>['inputs']
onInputsChange: (buttons: InputColors) => void
}
export const InputsTheme = ({ inputs, onInputsChange }: Props) => {
const { t } = useTranslate()
const handleBackgroundChange = (backgroundColor: string) =>
onInputsChange({ ...inputs, backgroundColor })
const handleTextChange = (color: string) =>
onInputsChange({ ...inputs, color })
const handlePlaceholderChange = (placeholderColor: string) =>
onInputsChange({ ...inputs, placeholderColor })
return (
<Stack data-testid="inputs-theme">
<Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.chat.theme.background')}</Text>
<ColorPicker
value={inputs?.backgroundColor}
onColorChange={handleBackgroundChange}
/>
</Flex>
<Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.chat.theme.text')}</Text>
<ColorPicker value={inputs?.color} onColorChange={handleTextChange} />
</Flex>
<Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.chat.theme.placeholder')}</Text>
<ColorPicker
value={inputs?.placeholderColor}
onColorChange={handlePlaceholderChange}
/>
</Flex>
</Stack>
)
}

View File

@@ -16,7 +16,8 @@ import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
import {
BackgroundType,
defaultTheme,
defaultBackgroundColor,
defaultBackgroundType,
} from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react'
@@ -34,10 +35,7 @@ export const BackgroundContent = ({
const handleContentChange = (content: string) =>
onBackgroundContentChange(content)
if (
(background?.type ?? defaultTheme.general.background.type) ===
BackgroundType.IMAGE
) {
if ((background?.type ?? defaultBackgroundType) === BackgroundType.IMAGE) {
if (!typebot) return null
return (
<Popover isLazy placement="top">
@@ -76,15 +74,12 @@ export const BackgroundContent = ({
</Popover>
)
}
if (
(background?.type ?? defaultTheme.general.background.type) ===
BackgroundType.COLOR
) {
if ((background?.type ?? defaultBackgroundType) === BackgroundType.COLOR) {
return (
<Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.global.background.color')}</Text>
<ColorPicker
value={background?.content ?? defaultTheme.general.background.content}
value={background?.content ?? defaultBackgroundColor}
onColorChange={handleContentChange}
/>
</Flex>

View File

@@ -1,11 +1,11 @@
import { RadioButtons } from '@/components/inputs/RadioButtons'
import { Stack, Text } from '@chakra-ui/react'
import { Stack } from '@chakra-ui/react'
import { Background } from '@typebot.io/schemas'
import React from 'react'
import { BackgroundContent } from './BackgroundContent'
import {
BackgroundType,
defaultTheme,
defaultBackgroundType,
} from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react'
@@ -28,7 +28,6 @@ export const BackgroundSelector = ({
return (
<Stack spacing={4}>
<Text>{t('theme.sideMenu.global.background')}</Text>
<RadioButtons
options={[
{
@@ -44,7 +43,7 @@ export const BackgroundSelector = ({
value: BackgroundType.NONE,
},
]}
value={background?.type ?? defaultTheme.general.background.type}
value={background?.type ?? defaultBackgroundType}
onSelect={handleBackgroundTypeChange}
/>
<BackgroundContent

View File

@@ -4,7 +4,11 @@ import {
Stack,
Switch,
useDisclosure,
Text,
Accordion,
AccordionItem,
AccordionButton,
AccordionPanel,
AccordionIcon,
} from '@chakra-ui/react'
import { Background, Font, ProgressBar, Theme } from '@typebot.io/schemas'
import React from 'react'
@@ -16,7 +20,7 @@ import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
import { useTranslate } from '@tolgee/react'
import {
defaultTheme,
defaultFontType,
fontTypes,
} from '@typebot.io/schemas/features/typebot/theme/constants'
import { trpc } from '@/lib/trpc'
@@ -91,7 +95,7 @@ export const GeneralSettings = ({
const fontType =
(typeof generalTheme?.font === 'string'
? 'Google'
: generalTheme?.font?.type) ?? defaultTheme.general.font.type
: generalTheme?.font?.type) ?? defaultFontType
return (
<Stack spacing={6}>
@@ -115,23 +119,46 @@ export const GeneralSettings = ({
onChange={updateBranding}
/>
</Flex>
<ProgressBarForm
progressBar={generalTheme?.progressBar}
onProgressBarChange={updateProgressBar}
/>
<Stack>
<Text>{t('theme.sideMenu.global.font')}</Text>
<RadioButtons
options={fontTypes}
defaultValue={fontType}
onSelect={updateFontType}
/>
<FontForm font={generalTheme?.font} onFontChange={updateFont} />
</Stack>
<BackgroundSelector
background={generalTheme?.background}
onBackgroundChange={handleBackgroundChange}
/>
<Accordion allowToggle>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Progress Bar
<AccordionIcon />
</AccordionButton>
<AccordionPanel>
<ProgressBarForm
progressBar={generalTheme?.progressBar}
onProgressBarChange={updateProgressBar}
/>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton justifyContent="space-between">
{t('theme.sideMenu.global.font')}
<AccordionIcon />
</AccordionButton>
<AccordionPanel as={Stack}>
<RadioButtons
options={fontTypes}
defaultValue={fontType}
onSelect={updateFontType}
/>
<FontForm font={generalTheme?.font} onFontChange={updateFont} />
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton justifyContent="space-between">
{t('theme.sideMenu.global.background')}
<AccordionIcon />
</AccordionButton>
<AccordionPanel>
<BackgroundSelector
background={generalTheme?.background}
onBackgroundChange={handleBackgroundChange}
/>
</AccordionPanel>
</AccordionItem>
</Accordion>
</Stack>
)
}

View File

@@ -1,7 +1,7 @@
import { Select } from '@/components/inputs/Select'
import { env } from '@typebot.io/env'
import { GoogleFont } from '@typebot.io/schemas'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { defaultFontFamily } from '@typebot.io/schemas/features/typebot/theme/constants'
import { useState, useEffect } from 'react'
type Props = {
@@ -11,8 +11,7 @@ type Props = {
export const GoogleFontForm = ({ font, onFontChange }: Props) => {
const [currentFont, setCurrentFont] = useState(
(typeof font === 'string' ? font : font?.family) ??
defaultTheme.general.font.family
(typeof font === 'string' ? font : font?.family) ?? defaultFontFamily
)
const [googleFonts, setGoogleFonts] = useState<string[]>([])

View File

@@ -5,7 +5,12 @@ import { NumberInput } from '@/components/inputs'
import { FormLabel, HStack } from '@chakra-ui/react'
import { ProgressBar } from '@typebot.io/schemas'
import {
defaultTheme,
defaultProgressBarBackgroundColor,
defaultProgressBarColor,
defaultProgressBarIsEnabled,
defaultProgressBarPlacement,
defaultProgressBarPosition,
defaultProgressBarThickness,
progressBarPlacements,
progressBarPositions,
} from '@typebot.io/schemas/features/typebot/theme/constants'
@@ -34,33 +39,41 @@ export const ProgressBarForm = ({
const updateThickness = (thickness?: number) =>
onProgressBarChange({ ...progressBar, thickness })
const updateBackgroundColor = (backgroundColor: string) =>
onProgressBarChange({ ...progressBar, backgroundColor })
return (
<SwitchWithRelatedSettings
label={'Enable progress bar?'}
initialValue={
progressBar?.isEnabled ?? defaultTheme.general.progressBar.isEnabled
}
initialValue={progressBar?.isEnabled ?? defaultProgressBarIsEnabled}
onCheckChange={updateEnabled}
>
<DropdownList
size="sm"
direction="row"
label="Placement:"
currentItem={
progressBar?.placement ?? defaultTheme.general.progressBar.placement
}
currentItem={progressBar?.placement ?? defaultProgressBarPlacement}
onItemSelect={updatePlacement}
items={progressBarPlacements}
/>
<HStack justifyContent="space-between">
<FormLabel mb="0" mr="0">
Color:
Background color:
</FormLabel>
<ColorPicker
defaultValue={
progressBar?.color ?? defaultTheme.general.progressBar.color
progressBar?.backgroundColor ?? defaultProgressBarBackgroundColor
}
onColorChange={updateBackgroundColor}
/>
</HStack>
<HStack justifyContent="space-between">
<FormLabel mb="0" mr="0">
Color:
</FormLabel>
<ColorPicker
defaultValue={progressBar?.color ?? defaultProgressBarColor}
onColorChange={updateColor}
/>
</HStack>
@@ -69,9 +82,7 @@ export const ProgressBarForm = ({
direction="row"
withVariableButton={false}
maxW="100px"
defaultValue={
progressBar?.thickness ?? defaultTheme.general.progressBar.thickness
}
defaultValue={progressBar?.thickness ?? defaultProgressBarThickness}
onValueChange={updateThickness}
size="sm"
/>
@@ -80,9 +91,7 @@ export const ProgressBarForm = ({
direction="row"
label="Position when embedded:"
moreInfoTooltip='Select "fixed" to always position the progress bar at the top of the window even though your bot is embedded. Select "absolute" to position the progress bar at the top of the chat container.'
currentItem={
progressBar?.position ?? defaultTheme.general.progressBar.position
}
currentItem={progressBar?.position ?? defaultProgressBarPosition}
onItemSelect={updatePosition}
items={progressBarPositions}
/>

View File

@@ -3,6 +3,10 @@ import test, { expect } from '@playwright/test'
import { createId } from '@paralleldrive/cuid2'
import { importTypebotInDatabase } from '@typebot.io/playwright/databaseActions'
import { freeWorkspaceId } from '@typebot.io/playwright/databaseSetup'
import {
defaultContainerMaxHeight,
defaultContainerMaxWidth,
} from '@typebot.io/schemas/features/typebot/theme/constants'
const hostAvatarUrl =
'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80'
@@ -30,6 +34,7 @@ test.describe.parallel('Theme page', () => {
await expect(page.locator('a:has-text("Made with Typebot")')).toBeHidden()
// Font
await page.getByRole('button', { name: 'Font' }).click()
await page.getByRole('textbox').fill('Roboto Slab')
await page.getByRole('menuitem', { name: 'Roboto Slab' }).click()
await expect(page.locator('.typebot-container')).toHaveCSS(
@@ -42,6 +47,7 @@ test.describe.parallel('Theme page', () => {
'background-color',
'rgba(0, 0, 0, 0)'
)
await page.getByRole('button', { name: 'Background' }).click()
await page.click('text=Color')
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Pick a color' }).click()
@@ -82,6 +88,38 @@ test.describe.parallel('Theme page', () => {
await expect(page.getByRole('button', { name: 'Go' })).toBeVisible()
await page.click('button:has-text("Chat")')
// Container
await expect(page.locator('.typebot-chat-view')).toHaveCSS(
'max-width',
defaultContainerMaxWidth
)
await page
.locator('div')
.filter({ hasText: /^Max width:px$/ })
.getByRole('spinbutton')
.fill('600')
await expect(page.locator('.typebot-chat-view')).toHaveCSS(
'max-width',
'600px'
)
await expect(page.locator('.typebot-chat-view')).toHaveCSS(
'max-height',
defaultContainerMaxHeight
)
await page
.locator('div')
.filter({ hasText: /^Max height:%$/ })
.getByRole('spinbutton')
.fill('80')
await expect(page.locator('.typebot-chat-view')).toHaveCSS(
'max-height',
'80%'
)
await expect(page.locator('.typebot-chat-view')).toHaveCSS(
'color',
'rgb(48, 50, 53)'
)
// Host avatar
await expect(
page.locator('[data-testid="default-avatar"]').nth(1)
@@ -102,39 +140,13 @@ test.describe.parallel('Theme page', () => {
await expect(page.locator('.typebot-container img')).toBeHidden()
// Roundness
await expect(page.getByRole('button', { name: 'Go' })).toHaveCSS(
'border-radius',
'6px'
)
await page
.getByRole('region', { name: 'Chat' })
.getByRole('radiogroup')
.locator('div')
.first()
.click()
await expect(page.getByRole('button', { name: 'Go' })).toHaveCSS(
'border-radius',
'0px'
)
await page
.getByRole('region', { name: 'Chat' })
.getByRole('radiogroup')
.locator('div')
.nth(2)
.click()
await expect(page.getByRole('button', { name: 'Go' })).toHaveCSS(
'border-radius',
'20px'
)
// Host bubbles
await page.click(
'[data-testid="host-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=0'
'[data-testid="hostBubblesTheme"] >> [aria-label="Pick a color"] >> nth=0'
)
await page.fill('input[value="#F7F8FF"]', '#2a9d8f')
await page.click(
'[data-testid="host-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=1'
'[data-testid="hostBubblesTheme"] >> [aria-label="Pick a color"] >> nth=1'
)
await page.fill('input[value="#303235"]', '#ffffff')
const hostBubble = page.locator('[data-testid="host-bubble"] >> nth=-1')
@@ -146,11 +158,11 @@ test.describe.parallel('Theme page', () => {
// Buttons
await page.click(
'[data-testid="buttons-theme"] >> [aria-label="Pick a color"] >> nth=0'
'[data-testid="buttonsTheme"] >> [aria-label="Pick a color"] >> nth=0'
)
await page.fill('input[value="#0042DA"]', '#7209b7')
await page.click(
'[data-testid="buttons-theme"] >> [aria-label="Pick a color"] >> nth=1'
'[data-testid="buttonsTheme"] >> [aria-label="Pick a color"] >> nth=1'
)
await page.fill('input[value="#FFFFFF"]', '#e9c46a')
const button = page.getByRole('button', { name: 'Go' })
@@ -159,11 +171,11 @@ test.describe.parallel('Theme page', () => {
// Guest bubbles
await page.click(
'[data-testid="guest-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=0'
'[data-testid="guestBubblesTheme"] >> [aria-label="Pick a color"] >> nth=0'
)
await page.fill('input[value="#FF8E21"]', '#d8f3dc')
await page.click(
'[data-testid="guest-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=1'
'[data-testid="guestBubblesTheme"] >> [aria-label="Pick a color"] >> nth=1'
)
await page.fill('input[value="#FFFFFF"]', '#264653')
await page.getByRole('button', { name: 'Go' }).click()
@@ -192,11 +204,11 @@ test.describe.parallel('Theme page', () => {
await page.waitForTimeout(1000)
// Input
await page.click(
'[data-testid="inputs-theme"] >> [aria-label="Pick a color"] >> nth=0'
'[data-testid="inputsTheme"] >> [aria-label="Pick a color"] >> nth=0'
)
await page.fill('input[value="#FFFFFF"]', '#ffe8d6')
await page.click(
'[data-testid="inputs-theme"] >> [aria-label="Pick a color"] >> nth=1'
'[data-testid="inputsTheme"] >> [aria-label="Pick a color"] >> nth=1'
)
await page.fill('input[value="#303235"]', '#023e8a')
const input = page.locator('.typebot-input')

View File

@@ -143,7 +143,7 @@
"blocks.inputs.file.settings.skip.label": "Skip button label:",
"blocks.inputs.fileUpload.blockCard.tooltip": "Upload Files",
"blocks.inputs.number.settings.step.label": "Step:",
"blocks.inputs.payment.collect.label": "Coletar",
"blocks.inputs.payment.collect.label": "Collect",
"blocks.inputs.payment.placeholder.label": "Configure...",
"blocks.inputs.payment.settings.account.label": "Account:",
"blocks.inputs.payment.settings.accountText.label": "{provider} account",

View File

@@ -173,7 +173,12 @@ export const getAuthOptions = ({
if (disposableEmailDomains.includes(user.email.split('@')[1]))
return false
}
if (env.DISABLE_SIGNUP && isNewUser && user.email) {
if (
env.DISABLE_SIGNUP &&
isNewUser &&
user.email &&
!env.ADMIN_EMAIL?.includes(user.email)
) {
const { invitations, workspaceInvitations } =
await getNewUserInvitations(prisma, user.email)
if (invitations.length === 0 && workspaceInvitations.length === 0)

View File

@@ -21342,6 +21342,70 @@
"chat": {
"type": "object",
"properties": {
"container": {
"type": "object",
"properties": {
"maxWidth": {
"type": "string"
},
"maxHeight": {
"type": "string"
},
"backgroundColor": {
"type": "string"
},
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
}
}
},
"hostAvatar": {
"type": "object",
"properties": {
@@ -21372,6 +21436,53 @@
},
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
}
}
},
@@ -21383,6 +21494,53 @@
},
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
}
}
},
@@ -21394,6 +21552,53 @@
},
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
}
}
},
@@ -21406,6 +21611,53 @@
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
},
"placeholderColor": {
"type": "string"
}
@@ -21417,7 +21669,8 @@
"none",
"medium",
"large"
]
],
"description": "Deprecated, use `container.border.roundeness` instead"
}
}
},

View File

@@ -6821,6 +6821,70 @@
"chat": {
"type": "object",
"properties": {
"container": {
"type": "object",
"properties": {
"maxWidth": {
"type": "string"
},
"maxHeight": {
"type": "string"
},
"backgroundColor": {
"type": "string"
},
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
}
}
},
"hostAvatar": {
"type": "object",
"properties": {
@@ -6851,6 +6915,53 @@
},
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
}
}
},
@@ -6862,6 +6973,53 @@
},
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
}
}
},
@@ -6873,6 +7031,53 @@
},
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
}
}
},
@@ -6885,6 +7090,53 @@
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
},
"placeholderColor": {
"type": "string"
}
@@ -6896,7 +7148,8 @@
"none",
"medium",
"large"
]
],
"description": "Deprecated, use `container.border.roundeness` instead"
}
}
},

View File

@@ -43,7 +43,7 @@ cp .env.example .env
<Note>
The database user should have the `SUPERUSER` role. You can setup and migrate
the database with the `pnpm prisma generate && pnpm db:migrate` command.
the database with the `pnpm db:migrate` command.
</Note>
3. Install dependencies

View File

@@ -7,8 +7,11 @@ import { TypebotPageProps, TypebotPageV2 } from '@/components/TypebotPageV2'
import { TypebotPageV3, TypebotV3PageProps } from '@/components/TypebotPageV3'
import { env } from '@typebot.io/env'
import prisma from '@typebot.io/lib/prisma'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
import {
defaultBackgroundColor,
defaultBackgroundType,
} from '@typebot.io/schemas/features/typebot/theme/constants'
// Browsers that doesn't support ES modules and/or web components
const incompatibleBrowsers = [
@@ -109,9 +112,10 @@ const getTypebotFromPublicId = async (publicId?: string) => {
? ({
name: publishedTypebot.typebot.name,
publicId: publishedTypebot.typebot.publicId ?? null,
background:
publishedTypebot.theme.general?.background ??
defaultTheme.general.background,
background: publishedTypebot.theme.general?.background ?? {
type: defaultBackgroundType,
content: defaultBackgroundColor,
},
isHideQueryParamsEnabled:
publishedTypebot.settings.general?.isHideQueryParamsEnabled ??
defaultSettings.general.isHideQueryParamsEnabled,
@@ -156,9 +160,10 @@ const getTypebotFromCustomDomain = async (customDomain: string) => {
? ({
name: publishedTypebot.typebot.name,
publicId: publishedTypebot.typebot.publicId ?? null,
background:
publishedTypebot.theme.general?.background ??
defaultTheme.general.background,
background: publishedTypebot.theme.general?.background ?? {
type: defaultBackgroundType,
content: defaultBackgroundColor,
},
isHideQueryParamsEnabled:
publishedTypebot.settings.general?.isHideQueryParamsEnabled ??
defaultSettings.general.isHideQueryParamsEnabled,
@@ -233,7 +238,10 @@ const App = ({
defaultSettings.general.isHideQueryParamsEnabled
}
background={
publishedTypebot.background ?? defaultTheme.general.background
publishedTypebot.background ?? {
type: defaultBackgroundType,
content: defaultBackgroundColor,
}
}
metadata={publishedTypebot.metadata ?? {}}
font={publishedTypebot.font}