refactor(♻️ Add defaults everywhere (+ settings page)):
This commit is contained in:
33
apps/builder/components/settings/GeneralSettingsForm.tsx
Normal file
33
apps/builder/components/settings/GeneralSettingsForm.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { Flex, FormLabel, Stack, Switch, Text } from '@chakra-ui/react'
|
||||
import { GeneralSettings } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
generalSettings: GeneralSettings
|
||||
onGeneralSettingsChange: (generalSettings: GeneralSettings) => void
|
||||
}
|
||||
|
||||
export const GeneralSettingsForm = ({
|
||||
generalSettings,
|
||||
onGeneralSettingsChange,
|
||||
}: Props) => {
|
||||
const handleSwitchChange = () =>
|
||||
onGeneralSettingsChange({
|
||||
isBrandingEnabled: !generalSettings?.isBrandingEnabled,
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack spacing={6}>
|
||||
<Flex justifyContent="space-between" align="center">
|
||||
<FormLabel htmlFor="branding" mb="0">
|
||||
Typebot.io branding
|
||||
</FormLabel>
|
||||
<Switch
|
||||
id="branding"
|
||||
isChecked={generalSettings.isBrandingEnabled}
|
||||
onChange={handleSwitchChange}
|
||||
/>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)
|
||||
}
|
112
apps/builder/components/settings/MetadataForm.tsx
Normal file
112
apps/builder/components/settings/MetadataForm.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import React from 'react'
|
||||
import { Metadata } from 'models'
|
||||
import {
|
||||
FormLabel,
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
Stack,
|
||||
Image,
|
||||
PopoverContent,
|
||||
} from '@chakra-ui/react'
|
||||
import { ImageUploadContent } from 'components/shared/ImageUploadContent'
|
||||
import {
|
||||
InputWithVariableButton,
|
||||
TextareaWithVariableButton,
|
||||
} from 'components/shared/TextboxWithVariableButton'
|
||||
|
||||
type Props = {
|
||||
typebotName: string
|
||||
metadata: Metadata
|
||||
onMetadataChange: (metadata: Metadata) => void
|
||||
}
|
||||
|
||||
export const MetadataForm = ({
|
||||
typebotName,
|
||||
metadata,
|
||||
onMetadataChange,
|
||||
}: Props) => {
|
||||
const handleTitleChange = (title: string) =>
|
||||
onMetadataChange({ ...metadata, title })
|
||||
const handleDescriptionChange = (description: string) =>
|
||||
onMetadataChange({ ...metadata, description })
|
||||
const handleFavIconSubmit = (favIconUrl: string) =>
|
||||
onMetadataChange({ ...metadata, favIconUrl })
|
||||
const handleImageSubmit = (imageUrl: string) =>
|
||||
onMetadataChange({ ...metadata, imageUrl })
|
||||
|
||||
return (
|
||||
<Stack spacing="6">
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="icon">
|
||||
Icon:
|
||||
</FormLabel>
|
||||
<Popover isLazy>
|
||||
<PopoverTrigger>
|
||||
<Image
|
||||
src={metadata.favIconUrl ?? '/favicon.png'}
|
||||
w="20px"
|
||||
alt="Fav icon"
|
||||
cursor="pointer"
|
||||
_hover={{ filter: 'brightness(.9)' }}
|
||||
transition="filter 200ms"
|
||||
rounded="md"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent p="4">
|
||||
<ImageUploadContent
|
||||
url={metadata.favIconUrl ?? ''}
|
||||
onSubmit={handleFavIconSubmit}
|
||||
isGiphyEnabled={false}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="image">
|
||||
Image:
|
||||
</FormLabel>
|
||||
<Popover isLazy>
|
||||
<PopoverTrigger>
|
||||
<Image
|
||||
src={metadata.imageUrl ?? '/viewer-preview.png'}
|
||||
alt="Website image"
|
||||
cursor="pointer"
|
||||
_hover={{ filter: 'brightness(.9)' }}
|
||||
transition="filter 200ms"
|
||||
rounded="md"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent p="4">
|
||||
<ImageUploadContent
|
||||
url={metadata.imageUrl}
|
||||
onSubmit={handleImageSubmit}
|
||||
isGiphyEnabled={false}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="title">
|
||||
Title:
|
||||
</FormLabel>
|
||||
<InputWithVariableButton
|
||||
id="title"
|
||||
initialValue={metadata.title ?? typebotName}
|
||||
delay={100}
|
||||
onChange={handleTitleChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="description">
|
||||
Description:
|
||||
</FormLabel>
|
||||
<TextareaWithVariableButton
|
||||
id="description"
|
||||
initialValue={metadata.description}
|
||||
delay={100}
|
||||
onChange={handleDescriptionChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import { Flex, Stack } from '@chakra-ui/react'
|
||||
import { TypingEmulationSettings } from 'models'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import React from 'react'
|
||||
import { TypingEmulation } from './TypingEmulation'
|
||||
|
||||
export const SettingsContent = () => {
|
||||
const { typebot, updateTypebot } = useTypebot()
|
||||
|
||||
const handleTypingEmulationUpdate = (
|
||||
typingEmulation: TypingEmulationSettings
|
||||
) => {
|
||||
if (!typebot) return
|
||||
updateTypebot({ settings: { ...typebot.settings, typingEmulation } })
|
||||
}
|
||||
return (
|
||||
<Flex h="full" w="full" justifyContent="center" align="flex-start">
|
||||
<Stack p="6" rounded="md" borderWidth={1} w="600px" minH="500px" mt={10}>
|
||||
<TypingEmulation
|
||||
typingEmulation={typebot?.settings?.typingEmulation}
|
||||
onUpdate={handleTypingEmulationUpdate}
|
||||
/>
|
||||
</Stack>
|
||||
</Flex>
|
||||
)
|
||||
}
|
103
apps/builder/components/settings/SettingsSideMenu.tsx
Normal file
103
apps/builder/components/settings/SettingsSideMenu.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Heading,
|
||||
HStack,
|
||||
Stack,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChatIcon, CodeIcon, MoreVerticalIcon } from 'assets/icons'
|
||||
import { headerHeight } from 'components/shared/TypebotHeader'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { GeneralSettings, Metadata, TypingEmulation } from 'models'
|
||||
import React from 'react'
|
||||
import { GeneralSettingsForm } from './GeneralSettingsForm'
|
||||
import { MetadataForm } from './MetadataForm'
|
||||
import { TypingEmulationForm } from './TypingEmulationForm'
|
||||
|
||||
export const SettingsSideMenu = () => {
|
||||
const { typebot, updateTypebot } = useTypebot()
|
||||
|
||||
const handleTypingEmulationChange = (typingEmulation: TypingEmulation) =>
|
||||
typebot &&
|
||||
updateTypebot({ settings: { ...typebot.settings, typingEmulation } })
|
||||
|
||||
const handleGeneralSettingsChange = (general: GeneralSettings) =>
|
||||
typebot && updateTypebot({ settings: { ...typebot.settings, general } })
|
||||
|
||||
const handleMetadataChange = (metadata: Metadata) =>
|
||||
typebot && updateTypebot({ settings: { ...typebot.settings, metadata } })
|
||||
|
||||
return (
|
||||
<Stack
|
||||
flex="1"
|
||||
maxW="400px"
|
||||
height={`calc(100vh - ${headerHeight}px)`}
|
||||
borderRightWidth={1}
|
||||
pt={10}
|
||||
spacing={10}
|
||||
overflowY="scroll"
|
||||
pb="20"
|
||||
>
|
||||
<Heading fontSize="xl" textAlign="center">
|
||||
Settings
|
||||
</Heading>
|
||||
<Accordion allowMultiple allowToggle>
|
||||
<AccordionItem>
|
||||
<AccordionButton py={6}>
|
||||
<HStack flex="1" pl={2}>
|
||||
<MoreVerticalIcon transform={'rotate(90deg)'} />
|
||||
<Heading fontSize="lg">General</Heading>
|
||||
</HStack>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} px="6">
|
||||
{typebot && (
|
||||
<GeneralSettingsForm
|
||||
generalSettings={typebot.settings.general}
|
||||
onGeneralSettingsChange={handleGeneralSettingsChange}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton py={6}>
|
||||
<HStack flex="1" pl={2}>
|
||||
<ChatIcon />
|
||||
<Heading fontSize="lg">Typing emulation</Heading>
|
||||
</HStack>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} px="6">
|
||||
{typebot && (
|
||||
<TypingEmulationForm
|
||||
typingEmulation={typebot.settings.typingEmulation}
|
||||
onUpdate={handleTypingEmulationChange}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton py={6}>
|
||||
<HStack flex="1" pl={2}>
|
||||
<CodeIcon />
|
||||
<Heading fontSize="lg">Metadata</Heading>
|
||||
</HStack>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} px="6">
|
||||
{typebot && (
|
||||
<MetadataForm
|
||||
typebotName={typebot.name}
|
||||
metadata={typebot.settings.metadata}
|
||||
onMetadataChange={handleMetadataChange}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
import {
|
||||
NumberInputProps,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper,
|
||||
} from '@chakra-ui/react'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export const SmartNumberInput = ({
|
||||
initialValue,
|
||||
onValueChange,
|
||||
...props
|
||||
}: {
|
||||
initialValue?: number
|
||||
onValueChange: (value?: number) => void
|
||||
} & NumberInputProps) => {
|
||||
const [value, setValue] = useState(initialValue?.toString() ?? '')
|
||||
|
||||
useEffect(() => {
|
||||
if (value.endsWith('.') || value.endsWith(',')) return
|
||||
if (value === '') onValueChange(undefined)
|
||||
const newValue = parseFloat(value)
|
||||
if (isNaN(newValue)) return
|
||||
onValueChange(newValue)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<NumberInput onChange={setValue} value={value} {...props}>
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
)
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
import { Flex, Stack, Switch, Text } from '@chakra-ui/react'
|
||||
import { TypingEmulationSettings } from 'models'
|
||||
import React from 'react'
|
||||
import { SmartNumberInput } from './SmartNumberInput'
|
||||
|
||||
type TypingEmulationProps = {
|
||||
typingEmulation?: TypingEmulationSettings
|
||||
onUpdate: (typingEmulation: TypingEmulationSettings) => void
|
||||
}
|
||||
|
||||
export const TypingEmulation = ({
|
||||
typingEmulation,
|
||||
onUpdate,
|
||||
}: TypingEmulationProps) => {
|
||||
const handleSwitchChange = () => {
|
||||
if (!typingEmulation) return
|
||||
onUpdate({ ...typingEmulation, enabled: !typingEmulation.enabled })
|
||||
}
|
||||
|
||||
const handleSpeedChange = (speed?: number) => {
|
||||
if (!typingEmulation) return
|
||||
onUpdate({ ...typingEmulation, speed: speed ?? 0 })
|
||||
}
|
||||
|
||||
const handleMaxDelayChange = (maxDelay?: number) => {
|
||||
if (!typingEmulation) return
|
||||
onUpdate({ ...typingEmulation, maxDelay: maxDelay ?? 0 })
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Flex justifyContent="space-between" align="center">
|
||||
<Text>Typing emulation</Text>
|
||||
<Switch
|
||||
isChecked={typingEmulation?.enabled}
|
||||
onChange={handleSwitchChange}
|
||||
/>
|
||||
</Flex>
|
||||
{typingEmulation?.enabled && (
|
||||
<Stack pl={10}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Words per minutes:</Text>
|
||||
<SmartNumberInput
|
||||
initialValue={typingEmulation.speed}
|
||||
onValueChange={handleSpeedChange}
|
||||
maxW="100px"
|
||||
step={30}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Max delay (in seconds):</Text>
|
||||
<SmartNumberInput
|
||||
initialValue={typingEmulation.maxDelay}
|
||||
onValueChange={handleMaxDelayChange}
|
||||
maxW="100px"
|
||||
step={0.1}
|
||||
/>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
69
apps/builder/components/settings/TypingEmulationForm.tsx
Normal file
69
apps/builder/components/settings/TypingEmulationForm.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { Flex, FormLabel, Stack, Switch, Text } from '@chakra-ui/react'
|
||||
import { TypingEmulation } from 'models'
|
||||
import React from 'react'
|
||||
import { isDefined } from 'utils'
|
||||
import { SmartNumberInput } from '../shared/SmartNumberInput'
|
||||
|
||||
type Props = {
|
||||
typingEmulation: TypingEmulation
|
||||
onUpdate: (typingEmulation: TypingEmulation) => void
|
||||
}
|
||||
|
||||
export const TypingEmulationForm = ({ typingEmulation, onUpdate }: Props) => {
|
||||
const handleSwitchChange = () =>
|
||||
onUpdate({
|
||||
...typingEmulation,
|
||||
enabled: !typingEmulation.enabled,
|
||||
})
|
||||
|
||||
const handleSpeedChange = (speed?: number) =>
|
||||
isDefined(speed) && onUpdate({ ...typingEmulation, speed })
|
||||
|
||||
const handleMaxDelayChange = (maxDelay?: number) =>
|
||||
isDefined(maxDelay) && onUpdate({ ...typingEmulation, maxDelay })
|
||||
|
||||
return (
|
||||
<Stack spacing={6}>
|
||||
<Flex justifyContent="space-between" align="center">
|
||||
<FormLabel htmlFor="typing-emulation" mb="0">
|
||||
Typing emulation
|
||||
</FormLabel>
|
||||
<Switch
|
||||
id="typing-emulation"
|
||||
isChecked={typingEmulation.enabled}
|
||||
onChange={handleSwitchChange}
|
||||
/>
|
||||
</Flex>
|
||||
{typingEmulation.enabled && (
|
||||
<Stack pl={10}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<FormLabel htmlFor="speed" mb="0">
|
||||
Words per minutes:
|
||||
</FormLabel>
|
||||
<SmartNumberInput
|
||||
id="speed"
|
||||
data-testid="speed"
|
||||
value={typingEmulation.speed}
|
||||
onValueChange={handleSpeedChange}
|
||||
maxW="100px"
|
||||
step={30}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<FormLabel htmlFor="max-delay" mb="0">
|
||||
Max delay (in seconds):
|
||||
</FormLabel>
|
||||
<SmartNumberInput
|
||||
id="max-delay"
|
||||
data-testid="max-delay"
|
||||
value={typingEmulation.maxDelay}
|
||||
onValueChange={handleMaxDelayChange}
|
||||
maxW="100px"
|
||||
step={0.1}
|
||||
/>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user