refactor(♻️ Add defaults everywhere (+ settings page)):
This commit is contained in:
@ -8,10 +8,11 @@ import { ImageUploadContent } from 'components/shared/ImageUploadContent'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import {
|
||||
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<ImageBubbleStep>)
|
||||
const handleContentChange = (url: string) =>
|
||||
updateStep(step.id, { content: { url } } as Partial<ImageBubbleStep>)
|
||||
|
||||
const handleVideoContentChange = (content: VideoBubbleContent) =>
|
||||
updateStep(step.id, { content } as Partial<VideoBubbleStep>)
|
||||
|
||||
switch (step.type) {
|
||||
case BubbleStepType.IMAGE: {
|
||||
return (
|
||||
<ImageUploadContent
|
||||
content={step.content}
|
||||
url={step.content?.url}
|
||||
onSubmit={handleContentChange}
|
||||
/>
|
||||
)
|
||||
@ -55,7 +59,7 @@ export const StepContent = ({ step }: Props) => {
|
||||
return (
|
||||
<VideoUploadContent
|
||||
content={step.content}
|
||||
onSubmit={handleContentChange}
|
||||
onSubmit={handleVideoContentChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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 = ({
|
||||
<SwitchWithLabel
|
||||
id="is-range"
|
||||
label={'Is range?'}
|
||||
initialValue={options?.isRange ?? false}
|
||||
initialValue={options.isRange}
|
||||
onCheckChange={handleIsRangeChange}
|
||||
/>
|
||||
<SwitchWithLabel
|
||||
id="with-time"
|
||||
label={'With time?'}
|
||||
initialValue={options?.isRange ?? false}
|
||||
initialValue={options.isRange}
|
||||
onCheckChange={handleHasTimeChange}
|
||||
/>
|
||||
{options?.isRange && (
|
||||
{options.isRange && (
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="from">
|
||||
From label:
|
||||
</FormLabel>
|
||||
<DebouncedInput
|
||||
id="from"
|
||||
initialValue={options?.labels?.from ?? 'From:'}
|
||||
initialValue={options.labels.from}
|
||||
delay={100}
|
||||
onChange={handleFromChange}
|
||||
/>
|
||||
@ -61,7 +61,7 @@ export const DateInputSettingsBody = ({
|
||||
</FormLabel>
|
||||
<DebouncedInput
|
||||
id="to"
|
||||
initialValue={options?.labels?.to ?? 'To:'}
|
||||
initialValue={options.labels.to}
|
||||
delay={100}
|
||||
onChange={handleToChange}
|
||||
/>
|
||||
@ -73,7 +73,7 @@ export const DateInputSettingsBody = ({
|
||||
</FormLabel>
|
||||
<DebouncedInput
|
||||
id="button"
|
||||
initialValue={options?.labels?.button ?? 'Send'}
|
||||
initialValue={options.labels.button}
|
||||
delay={100}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
@ -83,7 +83,7 @@ export const DateInputSettingsBody = ({
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options?.variableId}
|
||||
initialVariableId={options.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -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 = ({
|
||||
</FormLabel>
|
||||
<DebouncedInput
|
||||
id="placeholder"
|
||||
initialValue={options?.labels?.placeholder ?? 'Type your email...'}
|
||||
initialValue={options.labels.placeholder}
|
||||
delay={100}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
@ -39,7 +39,7 @@ export const EmailInputSettingsBody = ({
|
||||
</FormLabel>
|
||||
<DebouncedInput
|
||||
id="button"
|
||||
initialValue={options?.labels?.button ?? 'Send'}
|
||||
initialValue={options.labels.button}
|
||||
delay={100}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
@ -49,7 +49,7 @@ export const EmailInputSettingsBody = ({
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options?.variableId}
|
||||
initialVariableId={options.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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 = ({
|
||||
</FormLabel>
|
||||
<DebouncedInput
|
||||
id="placeholder"
|
||||
initialValue={options?.labels?.placeholder ?? 'Type your answer...'}
|
||||
initialValue={options.labels.placeholder}
|
||||
delay={100}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
@ -58,7 +58,7 @@ export const NumberInputSettingsBody = ({
|
||||
</FormLabel>
|
||||
<SmartNumberInput
|
||||
id="min"
|
||||
initialValue={options?.min}
|
||||
value={options.min}
|
||||
onValueChange={handleMinChange}
|
||||
/>
|
||||
</HStack>
|
||||
@ -68,7 +68,7 @@ export const NumberInputSettingsBody = ({
|
||||
</FormLabel>
|
||||
<SmartNumberInput
|
||||
id="max"
|
||||
initialValue={options?.max}
|
||||
value={options.max}
|
||||
onValueChange={handleMaxChange}
|
||||
/>
|
||||
</HStack>
|
||||
@ -78,7 +78,7 @@ export const NumberInputSettingsBody = ({
|
||||
</FormLabel>
|
||||
<SmartNumberInput
|
||||
id="step"
|
||||
initialValue={options?.step}
|
||||
value={options.step}
|
||||
onValueChange={handleStepChange}
|
||||
/>
|
||||
</HStack>
|
||||
@ -87,7 +87,7 @@ export const NumberInputSettingsBody = ({
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options?.variableId}
|
||||
initialVariableId={options.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -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 = ({
|
||||
</FormLabel>
|
||||
<DebouncedInput
|
||||
id="placeholder"
|
||||
initialValue={options?.labels?.placeholder ?? 'Your phone number...'}
|
||||
initialValue={options.labels.placeholder}
|
||||
delay={100}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
@ -39,7 +39,7 @@ export const PhoneNumberSettingsBody = ({
|
||||
</FormLabel>
|
||||
<DebouncedInput
|
||||
id="button"
|
||||
initialValue={options?.labels?.button ?? 'Send'}
|
||||
initialValue={options.labels.button}
|
||||
delay={100}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
@ -49,7 +49,7 @@ export const PhoneNumberSettingsBody = ({
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options?.variableId}
|
||||
initialVariableId={options.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -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) => {
|
||||
</FormLabel>
|
||||
<DebouncedInput
|
||||
id="tracking-id"
|
||||
initialValue={options?.url ?? ''}
|
||||
initialValue={options.url ?? ''}
|
||||
placeholder="Type a URL..."
|
||||
delay={100}
|
||||
onChange={handleUrlChange}
|
||||
@ -32,7 +32,7 @@ export const RedirectSettings = ({ options, onOptionsChange }: Props) => {
|
||||
<SwitchWithLabel
|
||||
id="new-tab"
|
||||
label="Open in new tab?"
|
||||
initialValue={options?.isNewTab ?? false}
|
||||
initialValue={options.isNewTab}
|
||||
onCheckChange={handleIsNewTabChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -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 = ({
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
onSelectVariable={handleVariableChange}
|
||||
initialVariableId={options?.variableId}
|
||||
initialVariableId={options.variableId}
|
||||
id="variable-search"
|
||||
/>
|
||||
</Stack>
|
||||
@ -36,7 +36,7 @@ export const SetVariableSettingsBody = ({
|
||||
</FormLabel>
|
||||
<DebouncedTextarea
|
||||
id="expression"
|
||||
initialValue={options?.expressionToEvaluate ?? ''}
|
||||
initialValue={options.expressionToEvaluate ?? ''}
|
||||
delay={100}
|
||||
onChange={handleExpressionChange}
|
||||
/>
|
||||
|
@ -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 = ({
|
||||
</FormLabel>
|
||||
<DebouncedInput
|
||||
id="placeholder"
|
||||
initialValue={options?.labels?.placeholder ?? 'Type your answer...'}
|
||||
initialValue={options.labels.placeholder}
|
||||
delay={100}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
@ -48,7 +48,7 @@ export const TextInputSettingsBody = ({
|
||||
</FormLabel>
|
||||
<DebouncedInput
|
||||
id="button"
|
||||
initialValue={options?.labels?.button ?? 'Send'}
|
||||
initialValue={options.labels.button}
|
||||
delay={100}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
@ -58,7 +58,7 @@ export const TextInputSettingsBody = ({
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options?.variableId}
|
||||
initialVariableId={options.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -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 = ({
|
||||
</FormLabel>
|
||||
<DebouncedInput
|
||||
id="placeholder"
|
||||
initialValue={options?.labels?.placeholder ?? 'Type your URL...'}
|
||||
initialValue={options.labels.placeholder}
|
||||
delay={100}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
@ -39,7 +39,7 @@ export const UrlInputSettingsBody = ({
|
||||
</FormLabel>
|
||||
<DebouncedInput
|
||||
id="button"
|
||||
initialValue={options?.labels?.button ?? 'Send'}
|
||||
initialValue={options.labels.button}
|
||||
delay={100}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
@ -49,7 +49,7 @@ export const UrlInputSettingsBody = ({
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options?.variableId}
|
||||
initialVariableId={options.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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,
|
||||
|
@ -103,7 +103,7 @@ export const StepNodeContent = ({ step }: Props) => {
|
||||
return <ConditionNodeContent step={step} />
|
||||
}
|
||||
case LogicStepType.REDIRECT: {
|
||||
if (!step.options) return <Text color={'gray.500'}>Configure...</Text>
|
||||
if (!step.options.url) return <Text color={'gray.500'}>Configure...</Text>
|
||||
return <Text isTruncated>Redirect to {step.options?.url}</Text>
|
||||
}
|
||||
case IntegrationStepType.GOOGLE_SHEETS: {
|
||||
|
33
apps/builder/components/settings/GeneralSettingsForm.tsx
Normal file
33
apps/builder/components/settings/GeneralSettingsForm.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { Flex, FormLabel, Stack, Switch, Text } from '@chakra-ui/react'
|
||||
import { GeneralSettings } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
generalSettings: GeneralSettings
|
||||
onGeneralSettingsChange: (generalSettings: GeneralSettings) => void
|
||||
}
|
||||
|
||||
export const GeneralSettingsForm = ({
|
||||
generalSettings,
|
||||
onGeneralSettingsChange,
|
||||
}: Props) => {
|
||||
const handleSwitchChange = () =>
|
||||
onGeneralSettingsChange({
|
||||
isBrandingEnabled: !generalSettings?.isBrandingEnabled,
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack spacing={6}>
|
||||
<Flex justifyContent="space-between" align="center">
|
||||
<FormLabel htmlFor="branding" mb="0">
|
||||
Typebot.io branding
|
||||
</FormLabel>
|
||||
<Switch
|
||||
id="branding"
|
||||
isChecked={generalSettings.isBrandingEnabled}
|
||||
onChange={handleSwitchChange}
|
||||
/>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)
|
||||
}
|
112
apps/builder/components/settings/MetadataForm.tsx
Normal file
112
apps/builder/components/settings/MetadataForm.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import React from 'react'
|
||||
import { Metadata } from 'models'
|
||||
import {
|
||||
FormLabel,
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
Stack,
|
||||
Image,
|
||||
PopoverContent,
|
||||
} from '@chakra-ui/react'
|
||||
import { ImageUploadContent } from 'components/shared/ImageUploadContent'
|
||||
import {
|
||||
InputWithVariableButton,
|
||||
TextareaWithVariableButton,
|
||||
} from 'components/shared/TextboxWithVariableButton'
|
||||
|
||||
type Props = {
|
||||
typebotName: string
|
||||
metadata: Metadata
|
||||
onMetadataChange: (metadata: Metadata) => void
|
||||
}
|
||||
|
||||
export const MetadataForm = ({
|
||||
typebotName,
|
||||
metadata,
|
||||
onMetadataChange,
|
||||
}: Props) => {
|
||||
const handleTitleChange = (title: string) =>
|
||||
onMetadataChange({ ...metadata, title })
|
||||
const handleDescriptionChange = (description: string) =>
|
||||
onMetadataChange({ ...metadata, description })
|
||||
const handleFavIconSubmit = (favIconUrl: string) =>
|
||||
onMetadataChange({ ...metadata, favIconUrl })
|
||||
const handleImageSubmit = (imageUrl: string) =>
|
||||
onMetadataChange({ ...metadata, imageUrl })
|
||||
|
||||
return (
|
||||
<Stack spacing="6">
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="icon">
|
||||
Icon:
|
||||
</FormLabel>
|
||||
<Popover isLazy>
|
||||
<PopoverTrigger>
|
||||
<Image
|
||||
src={metadata.favIconUrl ?? '/favicon.png'}
|
||||
w="20px"
|
||||
alt="Fav icon"
|
||||
cursor="pointer"
|
||||
_hover={{ filter: 'brightness(.9)' }}
|
||||
transition="filter 200ms"
|
||||
rounded="md"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent p="4">
|
||||
<ImageUploadContent
|
||||
url={metadata.favIconUrl ?? ''}
|
||||
onSubmit={handleFavIconSubmit}
|
||||
isGiphyEnabled={false}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="image">
|
||||
Image:
|
||||
</FormLabel>
|
||||
<Popover isLazy>
|
||||
<PopoverTrigger>
|
||||
<Image
|
||||
src={metadata.imageUrl ?? '/viewer-preview.png'}
|
||||
alt="Website image"
|
||||
cursor="pointer"
|
||||
_hover={{ filter: 'brightness(.9)' }}
|
||||
transition="filter 200ms"
|
||||
rounded="md"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent p="4">
|
||||
<ImageUploadContent
|
||||
url={metadata.imageUrl}
|
||||
onSubmit={handleImageSubmit}
|
||||
isGiphyEnabled={false}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="title">
|
||||
Title:
|
||||
</FormLabel>
|
||||
<InputWithVariableButton
|
||||
id="title"
|
||||
initialValue={metadata.title ?? typebotName}
|
||||
delay={100}
|
||||
onChange={handleTitleChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="description">
|
||||
Description:
|
||||
</FormLabel>
|
||||
<TextareaWithVariableButton
|
||||
id="description"
|
||||
initialValue={metadata.description}
|
||||
delay={100}
|
||||
onChange={handleDescriptionChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import { Flex, Stack } from '@chakra-ui/react'
|
||||
import { TypingEmulationSettings } from 'models'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import React from 'react'
|
||||
import { TypingEmulation } from './TypingEmulation'
|
||||
|
||||
export const SettingsContent = () => {
|
||||
const { typebot, updateTypebot } = useTypebot()
|
||||
|
||||
const handleTypingEmulationUpdate = (
|
||||
typingEmulation: TypingEmulationSettings
|
||||
) => {
|
||||
if (!typebot) return
|
||||
updateTypebot({ settings: { ...typebot.settings, typingEmulation } })
|
||||
}
|
||||
return (
|
||||
<Flex h="full" w="full" justifyContent="center" align="flex-start">
|
||||
<Stack p="6" rounded="md" borderWidth={1} w="600px" minH="500px" mt={10}>
|
||||
<TypingEmulation
|
||||
typingEmulation={typebot?.settings?.typingEmulation}
|
||||
onUpdate={handleTypingEmulationUpdate}
|
||||
/>
|
||||
</Stack>
|
||||
</Flex>
|
||||
)
|
||||
}
|
103
apps/builder/components/settings/SettingsSideMenu.tsx
Normal file
103
apps/builder/components/settings/SettingsSideMenu.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Heading,
|
||||
HStack,
|
||||
Stack,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChatIcon, CodeIcon, MoreVerticalIcon } from 'assets/icons'
|
||||
import { headerHeight } from 'components/shared/TypebotHeader'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { GeneralSettings, Metadata, TypingEmulation } from 'models'
|
||||
import React from 'react'
|
||||
import { GeneralSettingsForm } from './GeneralSettingsForm'
|
||||
import { MetadataForm } from './MetadataForm'
|
||||
import { TypingEmulationForm } from './TypingEmulationForm'
|
||||
|
||||
export const SettingsSideMenu = () => {
|
||||
const { typebot, updateTypebot } = useTypebot()
|
||||
|
||||
const handleTypingEmulationChange = (typingEmulation: TypingEmulation) =>
|
||||
typebot &&
|
||||
updateTypebot({ settings: { ...typebot.settings, typingEmulation } })
|
||||
|
||||
const handleGeneralSettingsChange = (general: GeneralSettings) =>
|
||||
typebot && updateTypebot({ settings: { ...typebot.settings, general } })
|
||||
|
||||
const handleMetadataChange = (metadata: Metadata) =>
|
||||
typebot && updateTypebot({ settings: { ...typebot.settings, metadata } })
|
||||
|
||||
return (
|
||||
<Stack
|
||||
flex="1"
|
||||
maxW="400px"
|
||||
height={`calc(100vh - ${headerHeight}px)`}
|
||||
borderRightWidth={1}
|
||||
pt={10}
|
||||
spacing={10}
|
||||
overflowY="scroll"
|
||||
pb="20"
|
||||
>
|
||||
<Heading fontSize="xl" textAlign="center">
|
||||
Settings
|
||||
</Heading>
|
||||
<Accordion allowMultiple allowToggle>
|
||||
<AccordionItem>
|
||||
<AccordionButton py={6}>
|
||||
<HStack flex="1" pl={2}>
|
||||
<MoreVerticalIcon transform={'rotate(90deg)'} />
|
||||
<Heading fontSize="lg">General</Heading>
|
||||
</HStack>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} px="6">
|
||||
{typebot && (
|
||||
<GeneralSettingsForm
|
||||
generalSettings={typebot.settings.general}
|
||||
onGeneralSettingsChange={handleGeneralSettingsChange}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton py={6}>
|
||||
<HStack flex="1" pl={2}>
|
||||
<ChatIcon />
|
||||
<Heading fontSize="lg">Typing emulation</Heading>
|
||||
</HStack>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} px="6">
|
||||
{typebot && (
|
||||
<TypingEmulationForm
|
||||
typingEmulation={typebot.settings.typingEmulation}
|
||||
onUpdate={handleTypingEmulationChange}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton py={6}>
|
||||
<HStack flex="1" pl={2}>
|
||||
<CodeIcon />
|
||||
<Heading fontSize="lg">Metadata</Heading>
|
||||
</HStack>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} px="6">
|
||||
{typebot && (
|
||||
<MetadataForm
|
||||
typebotName={typebot.name}
|
||||
metadata={typebot.settings.metadata}
|
||||
onMetadataChange={handleMetadataChange}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
import { Flex, Stack, Switch, Text } from '@chakra-ui/react'
|
||||
import { TypingEmulationSettings } from 'models'
|
||||
import React from 'react'
|
||||
import { SmartNumberInput } from './SmartNumberInput'
|
||||
|
||||
type TypingEmulationProps = {
|
||||
typingEmulation?: TypingEmulationSettings
|
||||
onUpdate: (typingEmulation: TypingEmulationSettings) => void
|
||||
}
|
||||
|
||||
export const TypingEmulation = ({
|
||||
typingEmulation,
|
||||
onUpdate,
|
||||
}: TypingEmulationProps) => {
|
||||
const handleSwitchChange = () => {
|
||||
if (!typingEmulation) return
|
||||
onUpdate({ ...typingEmulation, enabled: !typingEmulation.enabled })
|
||||
}
|
||||
|
||||
const handleSpeedChange = (speed?: number) => {
|
||||
if (!typingEmulation) return
|
||||
onUpdate({ ...typingEmulation, speed: speed ?? 0 })
|
||||
}
|
||||
|
||||
const handleMaxDelayChange = (maxDelay?: number) => {
|
||||
if (!typingEmulation) return
|
||||
onUpdate({ ...typingEmulation, maxDelay: maxDelay ?? 0 })
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Flex justifyContent="space-between" align="center">
|
||||
<Text>Typing emulation</Text>
|
||||
<Switch
|
||||
isChecked={typingEmulation?.enabled}
|
||||
onChange={handleSwitchChange}
|
||||
/>
|
||||
</Flex>
|
||||
{typingEmulation?.enabled && (
|
||||
<Stack pl={10}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Words per minutes:</Text>
|
||||
<SmartNumberInput
|
||||
initialValue={typingEmulation.speed}
|
||||
onValueChange={handleSpeedChange}
|
||||
maxW="100px"
|
||||
step={30}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Max delay (in seconds):</Text>
|
||||
<SmartNumberInput
|
||||
initialValue={typingEmulation.maxDelay}
|
||||
onValueChange={handleMaxDelayChange}
|
||||
maxW="100px"
|
||||
step={0.1}
|
||||
/>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
69
apps/builder/components/settings/TypingEmulationForm.tsx
Normal file
69
apps/builder/components/settings/TypingEmulationForm.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { Flex, FormLabel, Stack, Switch, Text } from '@chakra-ui/react'
|
||||
import { TypingEmulation } from 'models'
|
||||
import React from 'react'
|
||||
import { isDefined } from 'utils'
|
||||
import { SmartNumberInput } from '../shared/SmartNumberInput'
|
||||
|
||||
type Props = {
|
||||
typingEmulation: TypingEmulation
|
||||
onUpdate: (typingEmulation: TypingEmulation) => void
|
||||
}
|
||||
|
||||
export const TypingEmulationForm = ({ typingEmulation, onUpdate }: Props) => {
|
||||
const handleSwitchChange = () =>
|
||||
onUpdate({
|
||||
...typingEmulation,
|
||||
enabled: !typingEmulation.enabled,
|
||||
})
|
||||
|
||||
const handleSpeedChange = (speed?: number) =>
|
||||
isDefined(speed) && onUpdate({ ...typingEmulation, speed })
|
||||
|
||||
const handleMaxDelayChange = (maxDelay?: number) =>
|
||||
isDefined(maxDelay) && onUpdate({ ...typingEmulation, maxDelay })
|
||||
|
||||
return (
|
||||
<Stack spacing={6}>
|
||||
<Flex justifyContent="space-between" align="center">
|
||||
<FormLabel htmlFor="typing-emulation" mb="0">
|
||||
Typing emulation
|
||||
</FormLabel>
|
||||
<Switch
|
||||
id="typing-emulation"
|
||||
isChecked={typingEmulation.enabled}
|
||||
onChange={handleSwitchChange}
|
||||
/>
|
||||
</Flex>
|
||||
{typingEmulation.enabled && (
|
||||
<Stack pl={10}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<FormLabel htmlFor="speed" mb="0">
|
||||
Words per minutes:
|
||||
</FormLabel>
|
||||
<SmartNumberInput
|
||||
id="speed"
|
||||
data-testid="speed"
|
||||
value={typingEmulation.speed}
|
||||
onValueChange={handleSpeedChange}
|
||||
maxW="100px"
|
||||
step={30}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<FormLabel htmlFor="max-delay" mb="0">
|
||||
Max delay (in seconds):
|
||||
</FormLabel>
|
||||
<SmartNumberInput
|
||||
id="max-delay"
|
||||
data-testid="max-delay"
|
||||
value={typingEmulation.maxDelay}
|
||||
onValueChange={handleMaxDelayChange}
|
||||
maxW="100px"
|
||||
step={0.1}
|
||||
/>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -1,22 +1,27 @@
|
||||
import { ChangeEvent, FormEvent, useState } from 'react'
|
||||
import { ChangeEvent, FormEvent, useEffect, useState } from 'react'
|
||||
import { Button, HStack, Input, Stack } from '@chakra-ui/react'
|
||||
import { 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 (
|
||||
<Stack>
|
||||
<HStack>
|
||||
@ -34,7 +39,7 @@ export const ImageUploadContent = ({ content, onSubmit }: Props) => {
|
||||
>
|
||||
Embed link
|
||||
</Button>
|
||||
{process.env.NEXT_PUBLIC_GIPHY_API_KEY && (
|
||||
{process.env.NEXT_PUBLIC_GIPHY_API_KEY && isGiphyEnabled && (
|
||||
<Button
|
||||
variant={currentTab === 'giphy' ? 'solid' : 'ghost'}
|
||||
onClick={() => setCurrentTab('giphy')}
|
||||
@ -45,11 +50,7 @@ export const ImageUploadContent = ({ content, onSubmit }: Props) => {
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<BodyContent
|
||||
tab={currentTab}
|
||||
onSubmit={handleSubmit}
|
||||
url={content?.url}
|
||||
/>
|
||||
<BodyContent tab={currentTab} onSubmit={handleSubmit} url={url} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@ -93,26 +94,23 @@ const UploadFileContent = ({ onNewUrl }: ContentProps) => {
|
||||
|
||||
const EmbedLinkContent = ({ initialUrl, onNewUrl }: ContentProps) => {
|
||||
const [imageUrl, setImageUrl] = useState(initialUrl ?? '')
|
||||
const [debouncedImageUrl] = useDebounce(imageUrl, 100)
|
||||
|
||||
useEffect(() => {
|
||||
if (initialUrl === debouncedImageUrl) return
|
||||
onNewUrl(imageUrl)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedImageUrl])
|
||||
|
||||
const handleImageUrlChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
setImageUrl(e.target.value)
|
||||
|
||||
const handleUrlSubmit = (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
onNewUrl(imageUrl)
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack as="form" onSubmit={handleUrlSubmit}>
|
||||
<Input
|
||||
placeholder={'Paste the image link...'}
|
||||
onChange={handleImageUrlChange}
|
||||
value={imageUrl}
|
||||
/>
|
||||
<Button type="submit" disabled={imageUrl === ''} colorScheme="blue">
|
||||
Embed image
|
||||
</Button>
|
||||
</Stack>
|
||||
<Input
|
||||
placeholder={'Paste the image link...'}
|
||||
onChange={handleImageUrlChange}
|
||||
value={imageUrl}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
9
apps/builder/components/shared/Info.tsx
Normal file
9
apps/builder/components/shared/Info.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { Alert, AlertIcon, AlertProps } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
|
||||
export const Info = (props: AlertProps) => (
|
||||
<Alert status="info" bgColor={'blue.50'} rounded="md" {...props}>
|
||||
<AlertIcon />
|
||||
{props.children}
|
||||
</Alert>
|
||||
)
|
@ -6,29 +6,29 @@ import {
|
||||
NumberIncrementStepper,
|
||||
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 (
|
||||
<NumberInput onChange={setValue} value={value} {...props}>
|
||||
<NumberInput onChange={handleValueChange} value={currentValue} {...props}>
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
@ -0,0 +1,10 @@
|
||||
import { Input } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
import {
|
||||
TextBoxWithVariableButton,
|
||||
TextBoxWithVariableButtonProps,
|
||||
} from './TextboxWithVariableButton'
|
||||
|
||||
export const InputWithVariableButton = (
|
||||
props: Omit<TextBoxWithVariableButtonProps, 'TextBox'>
|
||||
) => <TextBoxWithVariableButton TextBox={Input} {...props} />
|
@ -0,0 +1,10 @@
|
||||
import { Textarea } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
import {
|
||||
TextBoxWithVariableButton,
|
||||
TextBoxWithVariableButtonProps,
|
||||
} from './TextboxWithVariableButton'
|
||||
|
||||
export const TextareaWithVariableButton = (
|
||||
props: Omit<TextBoxWithVariableButtonProps, 'TextBox'>
|
||||
) => <TextBoxWithVariableButton TextBox={Textarea} {...props} />
|
@ -1,31 +1,40 @@
|
||||
import {
|
||||
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<InputProps, 'onChange'>) => {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||
TextBox:
|
||||
| ComponentWithAs<'textarea', TextareaProps>
|
||||
| ComponentWithAs<'input', InputProps>
|
||||
} & Omit<InputProps & TextareaProps, 'onChange'>
|
||||
|
||||
export const TextBoxWithVariableButton = ({
|
||||
initialValue,
|
||||
onChange,
|
||||
delay,
|
||||
TextBox,
|
||||
...props
|
||||
}: TextBoxWithVariableButtonProps) => {
|
||||
const textBoxRef = useRef<(HTMLInputElement & HTMLTextAreaElement) | null>(
|
||||
null
|
||||
)
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [debouncedValue] = useDebounce(value, delay ?? 100)
|
||||
const [carretPosition, setCarretPosition] = useState<number>(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<HTMLInputElement>) =>
|
||||
setValue(e.target.value)
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => setValue(e.target.value)
|
||||
|
||||
return (
|
||||
<HStack spacing={0}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
<HStack spacing={0} align={'flex-end'}>
|
||||
<TextBox
|
||||
ref={textBoxRef}
|
||||
onKeyUp={handleKeyUp}
|
||||
onClick={handleKeyUp}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
{...props}
|
||||
bgColor={'white'}
|
||||
{...props}
|
||||
/>
|
||||
<Popover matchWidth isLazy>
|
||||
<PopoverTrigger>
|
@ -0,0 +1,2 @@
|
||||
export { InputWithVariableButton } from './InputWithVariableButton'
|
||||
export { TextareaWithVariableButton } from './TextareaWithVariableButton'
|
@ -4,13 +4,10 @@ import React from 'react'
|
||||
import { ColorPicker } from '../GeneralSettings/ColorPicker'
|
||||
|
||||
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) => {
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Background:</Text>
|
||||
<ColorPicker
|
||||
initialColor={buttons?.backgroundColor ?? defaultBackgroundColor}
|
||||
initialColor={buttons.backgroundColor}
|
||||
onColorChange={handleBackgroundChange}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Text:</Text>
|
||||
<ColorPicker
|
||||
initialColor={buttons?.color ?? defaultTextColor}
|
||||
initialColor={buttons.color}
|
||||
onColorChange={handleTextChange}
|
||||
/>
|
||||
</Flex>
|
||||
|
@ -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) => {
|
||||
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
|
||||
<Heading fontSize="lg">Bot bubbles</Heading>
|
||||
<HostBubbles
|
||||
hostBubbles={chatTheme?.hostBubbles}
|
||||
hostBubbles={chatTheme.hostBubbles}
|
||||
onHostBubblesChange={handleHostBubblesChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
|
||||
<Heading fontSize="lg">User bubbles</Heading>
|
||||
<GuestBubbles
|
||||
guestBubbles={chatTheme?.guestBubbles}
|
||||
guestBubbles={chatTheme.guestBubbles}
|
||||
onGuestBubblesChange={handleGuestBubblesChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
|
||||
<Heading fontSize="lg">Buttons</Heading>
|
||||
<ButtonsTheme
|
||||
buttons={chatTheme?.buttons}
|
||||
buttons={chatTheme.buttons}
|
||||
onButtonsChange={handleButtonsChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
|
||||
<Heading fontSize="lg">Inputs</Heading>
|
||||
<InputsTheme
|
||||
inputs={chatTheme?.inputs}
|
||||
inputs={chatTheme.inputs}
|
||||
onInputsChange={handleInputsChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -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) => {
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Background:</Text>
|
||||
<ColorPicker
|
||||
initialColor={guestBubbles?.backgroundColor ?? defaultBackgroundColor}
|
||||
initialColor={guestBubbles.backgroundColor}
|
||||
onColorChange={handleBackgroundChange}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Text:</Text>
|
||||
<ColorPicker
|
||||
initialColor={guestBubbles?.color ?? defaultTextColor}
|
||||
initialColor={guestBubbles.color}
|
||||
onColorChange={handleTextChange}
|
||||
/>
|
||||
</Flex>
|
||||
|
@ -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) => {
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Background:</Text>
|
||||
<ColorPicker
|
||||
initialColor={hostBubbles?.backgroundColor ?? defaultBackgroundColor}
|
||||
initialColor={hostBubbles.backgroundColor}
|
||||
onColorChange={handleBackgroundChange}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Text:</Text>
|
||||
<ColorPicker
|
||||
initialColor={hostBubbles?.color ?? defaultTextColor}
|
||||
initialColor={hostBubbles.color}
|
||||
onColorChange={handleTextChange}
|
||||
/>
|
||||
</Flex>
|
||||
|
@ -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) => {
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Background:</Text>
|
||||
<ColorPicker
|
||||
initialColor={inputs?.backgroundColor ?? defaultBackgroundColor}
|
||||
initialColor={inputs.backgroundColor}
|
||||
onColorChange={handleBackgroundChange}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Text:</Text>
|
||||
<ColorPicker
|
||||
initialColor={inputs?.color ?? defaultTextColor}
|
||||
initialColor={inputs.color}
|
||||
onColorChange={handleTextChange}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Placeholder text:</Text>
|
||||
<ColorPicker
|
||||
initialColor={inputs?.placeholderColor ?? defaultPlaceholderColor}
|
||||
initialColor={inputs.placeholderColor}
|
||||
onColorChange={handlePlaceholderChange}
|
||||
/>
|
||||
</Flex>
|
||||
|
@ -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<HTMLInputElement>) =>
|
||||
setColor(e.target.value)
|
||||
|
||||
return (
|
||||
<Popover variant="picker">
|
||||
<PopoverTrigger>
|
||||
@ -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}
|
||||
/>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
|
@ -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 (
|
||||
<Stack spacing={6}>
|
||||
<FontSelector
|
||||
activeFont={generalTheme?.font ?? defaultFont}
|
||||
activeFont={generalTheme.font}
|
||||
onSelectFont={handleSelectFont}
|
||||
/>
|
||||
<BackgroundSelector
|
||||
background={generalTheme?.background ?? { type: BackgroundType.NONE }}
|
||||
background={generalTheme.background}
|
||||
onBackgroundChange={handleBackgroundChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -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 (
|
||||
<Stack
|
||||
@ -53,10 +53,12 @@ export const SideMenu = () => {
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4}>
|
||||
<GeneralSettings
|
||||
generalTheme={typebot?.theme?.general}
|
||||
onGeneralThemeChange={handleGeneralThemeChange}
|
||||
/>
|
||||
{typebot && (
|
||||
<GeneralSettings
|
||||
generalTheme={typebot.theme.general}
|
||||
onGeneralThemeChange={handleGeneralThemeChange}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
@ -68,10 +70,12 @@ export const SideMenu = () => {
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4}>
|
||||
<ChatThemeSettings
|
||||
chatTheme={typebot?.theme?.chat}
|
||||
onChatThemeChange={handleChatThemeChange}
|
||||
/>
|
||||
{typebot && (
|
||||
<ChatThemeSettings
|
||||
chatTheme={typebot.theme.chat}
|
||||
onChatThemeChange={handleChatThemeChange}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
@ -83,10 +87,12 @@ export const SideMenu = () => {
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4}>
|
||||
<CustomCssSettings
|
||||
customCss={typebot?.theme?.customCss}
|
||||
onCustomCssChange={handleCustomCssChange}
|
||||
/>
|
||||
{typebot && (
|
||||
<CustomCssSettings
|
||||
customCss={typebot.theme.customCss}
|
||||
onCustomCssChange={handleCustomCssChange}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
Reference in New Issue
Block a user