2
0

🚸 Add a better select input

Also improves other inputs behavior
This commit is contained in:
Baptiste Arnaud
2023-03-03 09:01:11 +01:00
parent a66bfca1ec
commit cc7d7285e5
94 changed files with 1251 additions and 1109 deletions

View File

@@ -4,7 +4,7 @@ import { Grid } from '@giphy/react-components'
import { GiphyLogo } from './GiphyLogo' import { GiphyLogo } from './GiphyLogo'
import React, { useState } from 'react' import React, { useState } from 'react'
import { env, isEmpty } from 'utils' import { env, isEmpty } from 'utils'
import { Input } from '../inputs' import { TextInput } from '../inputs'
type GiphySearchFormProps = { type GiphySearchFormProps = {
onSubmit: (url: string) => void onSubmit: (url: string) => void
@@ -26,8 +26,7 @@ export const GiphySearchForm = ({ onSubmit }: GiphySearchFormProps) => {
) : ( ) : (
<Stack> <Stack>
<Flex align="center"> <Flex align="center">
<Input <TextInput
flex="1"
autoFocus autoFocus
placeholder="Search..." placeholder="Search..."
onChange={setInputValue} onChange={setInputValue}

View File

@@ -2,7 +2,7 @@ import { useState } from 'react'
import { Button, Flex, HStack, Stack } from '@chakra-ui/react' import { Button, Flex, HStack, Stack } from '@chakra-ui/react'
import { UploadButton } from './UploadButton' import { UploadButton } from './UploadButton'
import { GiphySearchForm } from './GiphySearchForm' import { GiphySearchForm } from './GiphySearchForm'
import { Input } from '../inputs/Input' import { TextInput } from '../inputs/TextInput'
import { EmojiSearchableList } from './emoji/EmojiSearchableList' import { EmojiSearchableList } from './emoji/EmojiSearchableList'
type Props = { type Props = {
@@ -137,7 +137,7 @@ const EmbedLinkContent = ({
onNewUrl, onNewUrl,
}: ContentProps & { defaultUrl?: string }) => ( }: ContentProps & { defaultUrl?: string }) => (
<Stack py="2"> <Stack py="2">
<Input <TextInput
placeholder={'Paste the image link...'} placeholder={'Paste the image link...'}
onChange={onNewUrl} onChange={onNewUrl}
defaultValue={defaultUrl ?? ''} defaultValue={defaultUrl ?? ''}

View File

@@ -1,241 +0,0 @@
import {
useDisclosure,
Flex,
Popover,
Input,
PopoverContent,
Button,
InputProps,
HStack,
useColorModeValue,
PopoverAnchor,
Portal,
} from '@chakra-ui/react'
import { Variable } from 'models'
import { useState, useRef, useEffect, ChangeEvent, ReactNode } from 'react'
import { useDebouncedCallback } from 'use-debounce'
import { env, isDefined } from 'utils'
import { VariablesButton } from '@/features/variables'
import { useOutsideClick } from '@/hooks/useOutsideClick'
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
type Props = {
selectedItem?: string
items: (string | { label: ReactNode; value: string })[]
debounceTimeout?: number
withVariableButton?: boolean
onValueChange?: (value: string) => void
} & InputProps
export const SearchableDropdown = ({
selectedItem,
items,
withVariableButton = false,
debounceTimeout = 1000,
onValueChange,
...inputProps
}: Props) => {
const bg = useColorModeValue('gray.200', 'gray.700')
const [carretPosition, setCarretPosition] = useState<number>(0)
const { onOpen, onClose, isOpen } = useDisclosure()
const [inputValue, setInputValue] = useState(selectedItem ?? '')
const debounced = useDebouncedCallback(
// eslint-disable-next-line @typescript-eslint/no-empty-function
onValueChange ? onValueChange : () => {},
env('E2E_TEST') === 'true' ? 0 : debounceTimeout
)
const [filteredItems, setFilteredItems] = useState([
...items
.filter((item) =>
(typeof item === 'string' ? item : item.value)
.toLowerCase()
.includes((selectedItem ?? '').toLowerCase())
)
.slice(0, 50),
])
const [keyboardFocusIndex, setKeyboardFocusIndex] = useState<
number | undefined
>()
const dropdownRef = useRef(null)
const itemsRef = useRef<(HTMLButtonElement | null)[]>([])
const inputRef = useRef<HTMLInputElement>(null)
const { ref: parentModalRef } = useParentModal()
useEffect(
() => () => {
debounced.flush()
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
)
useEffect(() => {
if (filteredItems.length > 0) return
setFilteredItems([
...items
.filter((item) =>
(typeof item === 'string' ? item : item.value)
.toLowerCase()
.includes((selectedItem ?? '').toLowerCase())
)
.slice(0, 50),
])
if (inputRef.current === document.activeElement) onOpen()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [items])
useOutsideClick({
ref: dropdownRef,
handler: onClose,
})
const onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
if (!isOpen) onOpen()
setInputValue(e.target.value)
debounced(e.target.value)
if (e.target.value === '') {
setFilteredItems([...items.slice(0, 50)])
return
}
setFilteredItems([
...items
.filter((item) =>
(typeof item === 'string' ? item : item.value)
.toLowerCase()
.includes((inputValue ?? '').toLowerCase())
)
.slice(0, 50),
])
}
const handleItemClick = (item: string) => () => {
setInputValue(item)
debounced(item)
setKeyboardFocusIndex(undefined)
onClose()
}
const handleVariableSelected = (variable?: Variable) => {
if (!inputRef.current || !variable) return
const cursorPosition = carretPosition
const textBeforeCursorPosition = inputRef.current.value.substring(
0,
cursorPosition
)
const textAfterCursorPosition = inputRef.current.value.substring(
cursorPosition,
inputRef.current.value.length
)
const newValue =
textBeforeCursorPosition +
`{{${variable.name}}}` +
textAfterCursorPosition
setInputValue(newValue)
debounced(newValue)
inputRef.current.focus()
setTimeout(() => {
if (!inputRef.current) return
inputRef.current.selectionStart = inputRef.current.selectionEnd =
carretPosition + `{{${variable.name}}}`.length
setCarretPosition(inputRef.current.selectionStart)
}, 100)
}
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (inputRef.current?.selectionStart)
setCarretPosition(inputRef.current.selectionStart)
if (e.key === 'Enter' && isDefined(keyboardFocusIndex)) {
const item = filteredItems[keyboardFocusIndex]
handleItemClick(typeof item === 'string' ? item : item.value)()
return setKeyboardFocusIndex(undefined)
}
if (e.key === 'ArrowDown') {
if (keyboardFocusIndex === undefined) return setKeyboardFocusIndex(0)
if (keyboardFocusIndex === filteredItems.length - 1) return
itemsRef.current[keyboardFocusIndex + 1]?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
})
return setKeyboardFocusIndex(keyboardFocusIndex + 1)
}
if (e.key === 'ArrowUp') {
if (keyboardFocusIndex === undefined) return
if (keyboardFocusIndex === 0) return setKeyboardFocusIndex(undefined)
itemsRef.current[keyboardFocusIndex - 1]?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
})
setKeyboardFocusIndex(keyboardFocusIndex - 1)
}
}
return (
<Flex ref={dropdownRef} w="full">
<Popover
isOpen={isOpen}
initialFocusRef={inputRef}
matchWidth
offset={[0, 0]}
isLazy
>
<PopoverAnchor>
<HStack spacing={0} align={'flex-end'} w="full">
<Input
ref={inputRef}
value={inputValue}
onChange={onInputChange}
onFocus={onOpen}
type="text"
onKeyUp={handleKeyUp}
{...inputProps}
/>
{withVariableButton && (
<VariablesButton
onSelectVariable={handleVariableSelected}
onClick={onClose}
/>
)}
</HStack>
</PopoverAnchor>
<Portal containerRef={parentModalRef}>
<PopoverContent
maxH="35vh"
overflowY="scroll"
role="menu"
w="inherit"
shadow="lg"
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
{filteredItems.length > 0 && (
<>
{filteredItems.map((item, idx) => {
return (
<Button
ref={(el) => (itemsRef.current[idx] = el)}
minH="40px"
key={idx}
onClick={handleItemClick(
typeof item === 'string' ? item : item.value
)}
fontSize="16px"
fontWeight="normal"
rounded="none"
colorScheme="gray"
role="menuitem"
variant="ghost"
bg={keyboardFocusIndex === idx ? bg : 'transparent'}
justifyContent="flex-start"
>
{typeof item === 'string' ? item : item.label}
</Button>
)
})}
</>
)}
</PopoverContent>
</Portal>
</Popover>
</Flex>
)
}

View File

@@ -0,0 +1,230 @@
import {
useDisclosure,
Popover,
PopoverContent,
Button,
useColorModeValue,
PopoverAnchor,
Portal,
Input,
HStack,
FormControl,
FormLabel,
} from '@chakra-ui/react'
import { useState, useRef, useEffect, ReactNode } from 'react'
import { useDebouncedCallback } from 'use-debounce'
import { env, isDefined } from 'utils'
import { useOutsideClick } from '@/hooks/useOutsideClick'
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
import { VariablesButton } from '@/features/variables'
import { Variable } from 'models'
import { injectVariableInText } from '@/features/variables/utils/injectVariableInTextInput'
import { focusInput } from '@/utils/focusInput'
import { MoreInfoTooltip } from '../MoreInfoTooltip'
type Props = {
items: string[]
defaultValue?: string
debounceTimeout?: number
placeholder?: string
withVariableButton?: boolean
label?: ReactNode
moreInfoTooltip?: string
isRequired?: boolean
onChange: (value: string) => void
}
export const AutocompleteInput = ({
items,
onChange: _onChange,
debounceTimeout,
placeholder,
withVariableButton = true,
defaultValue,
label,
moreInfoTooltip,
isRequired,
}: Props) => {
const bg = useColorModeValue('gray.200', 'gray.700')
const { onOpen, onClose, isOpen } = useDisclosure()
const [isTouched, setIsTouched] = useState(false)
const [inputValue, setInputValue] = useState(defaultValue ?? '')
const [carretPosition, setCarretPosition] = useState<number>(
inputValue.length ?? 0
)
const onChange = useDebouncedCallback(
_onChange,
env('E2E_TEST') === 'true' ? 0 : debounceTimeout
)
useEffect(() => {
if (isTouched || inputValue !== '' || !defaultValue || defaultValue === '')
return
setInputValue(defaultValue ?? '')
}, [defaultValue, inputValue, isTouched])
const [keyboardFocusIndex, setKeyboardFocusIndex] = useState<
number | undefined
>()
const dropdownRef = useRef(null)
const itemsRef = useRef<(HTMLButtonElement | null)[]>([])
const inputRef = useRef<HTMLInputElement & HTMLTextAreaElement>(null)
const { ref: parentModalRef } = useParentModal()
const filteredItems = (
inputValue === ''
? items
: [
...items.filter(
(item) =>
item.toLowerCase().startsWith((inputValue ?? '').toLowerCase()) &&
item.toLowerCase() !== inputValue.toLowerCase()
),
]
).slice(0, 50)
useOutsideClick({
ref: dropdownRef,
handler: onClose,
})
useEffect(
() => () => {
onChange.flush()
},
[onChange]
)
const changeValue = (value: string) => {
if (!isTouched) setIsTouched(true)
if (!isOpen) onOpen()
setInputValue(value)
onChange(value)
}
const handleItemClick = (value: string) => () => {
setInputValue(value)
onChange(value)
setKeyboardFocusIndex(undefined)
inputRef.current?.focus()
}
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && isDefined(keyboardFocusIndex)) {
handleItemClick(filteredItems[keyboardFocusIndex])()
return setKeyboardFocusIndex(undefined)
}
if (e.key === 'ArrowDown') {
if (keyboardFocusIndex === undefined) return setKeyboardFocusIndex(0)
if (keyboardFocusIndex === filteredItems.length - 1)
return setKeyboardFocusIndex(0)
itemsRef.current[keyboardFocusIndex + 1]?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
})
return setKeyboardFocusIndex(keyboardFocusIndex + 1)
}
if (e.key === 'ArrowUp') {
if (keyboardFocusIndex === 0 || keyboardFocusIndex === undefined)
return setKeyboardFocusIndex(filteredItems.length - 1)
itemsRef.current[keyboardFocusIndex - 1]?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
})
setKeyboardFocusIndex(keyboardFocusIndex - 1)
}
}
const handleVariableSelected = (variable?: Variable) => {
if (!variable) return
const { text, carretPosition: newCarretPosition } = injectVariableInText({
variable,
text: inputValue,
at: carretPosition,
})
changeValue(text)
focusInput({ at: newCarretPosition, input: inputRef.current })
}
const updateCarretPosition = (e: React.FocusEvent<HTMLInputElement>) => {
const carretPosition = e.target.selectionStart
if (!carretPosition) return
setCarretPosition(carretPosition)
}
return (
<FormControl isRequired={isRequired}>
{label && (
<FormLabel>
{label}{' '}
{moreInfoTooltip && (
<MoreInfoTooltip>{moreInfoTooltip}</MoreInfoTooltip>
)}
</FormLabel>
)}
<HStack ref={dropdownRef} spacing={0} w="full">
<Popover
isOpen={isOpen}
initialFocusRef={inputRef}
matchWidth
offset={[0, 1]}
isLazy
>
<PopoverAnchor>
<Input
autoComplete="off"
ref={inputRef}
value={inputValue}
onChange={(e) => changeValue(e.target.value)}
onFocus={onOpen}
onBlur={updateCarretPosition}
onKeyDown={handleKeyUp}
placeholder={placeholder}
/>
</PopoverAnchor>
{filteredItems.length > 0 && (
<Portal containerRef={parentModalRef}>
<PopoverContent
maxH="35vh"
overflowY="scroll"
role="menu"
w="inherit"
shadow="lg"
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
<>
{filteredItems.map((item, idx) => {
return (
<Button
ref={(el) => (itemsRef.current[idx] = el)}
minH="40px"
key={idx}
onClick={handleItemClick(item)}
fontSize="16px"
fontWeight="normal"
rounded="none"
colorScheme="gray"
role="menuitem"
variant="ghost"
bg={keyboardFocusIndex === idx ? bg : 'transparent'}
justifyContent="flex-start"
transition="none"
>
{item}
</Button>
)
})}
</>
</PopoverContent>
</Portal>
)}
</Popover>
{withVariableButton && (
<VariablesButton onSelectVariable={handleVariableSelected} />
)}
</HStack>
</FormControl>
)
}

View File

@@ -15,7 +15,7 @@ import { tokyoNight } from '@uiw/codemirror-theme-tokyo-night'
import { githubLight } from '@uiw/codemirror-theme-github' import { githubLight } from '@uiw/codemirror-theme-github'
import { LanguageName, loadLanguage } from '@uiw/codemirror-extensions-langs' import { LanguageName, loadLanguage } from '@uiw/codemirror-extensions-langs'
import { isDefined } from '@udecode/plate-common' import { isDefined } from '@udecode/plate-common'
import { CopyButton } from './CopyButton' import { CopyButton } from '../CopyButton'
type Props = { type Props = {
value?: string value?: string

View File

@@ -1,7 +0,0 @@
import { Input as ChakraInput } from '@chakra-ui/react'
import React from 'react'
import { TextBox, TextBoxProps } from './TextBox'
export const Input = (props: Omit<TextBoxProps, 'TextBox'>) => (
<TextBox TextBox={ChakraInput} {...props} />
)

View File

@@ -1,7 +1,7 @@
import { VariablesButton } from '@/features/variables' import { VariablesButton } from '@/features/variables'
import { import {
NumberInputProps, NumberInputProps,
NumberInput, NumberInput as ChakraNumberInput,
NumberInputField, NumberInputField,
NumberInputStepper, NumberInputStepper,
NumberIncrementStepper, NumberIncrementStepper,
@@ -30,7 +30,7 @@ type Props<HasVariable extends boolean> = {
onValueChange: (value?: Value<HasVariable>) => void onValueChange: (value?: Value<HasVariable>) => void
} & Omit<NumberInputProps, 'defaultValue' | 'value' | 'onChange' | 'isRequired'> } & Omit<NumberInputProps, 'defaultValue' | 'value' | 'onChange' | 'isRequired'>
export const SmartNumberInput = <HasVariable extends boolean>({ export const NumberInput = <HasVariable extends boolean>({
defaultValue, defaultValue,
onValueChange, onValueChange,
withVariableButton, withVariableButton,
@@ -79,13 +79,13 @@ export const SmartNumberInput = <HasVariable extends boolean>({
} }
const Input = ( const Input = (
<NumberInput onChange={handleValueChange} value={value} {...props}> <ChakraNumberInput onChange={handleValueChange} value={value} {...props}>
<NumberInputField placeholder={props.placeholder} /> <NumberInputField placeholder={props.placeholder} />
<NumberInputStepper> <NumberInputStepper>
<NumberIncrementStepper /> <NumberIncrementStepper />
<NumberDecrementStepper /> <NumberDecrementStepper />
</NumberInputStepper> </NumberInputStepper>
</NumberInput> </ChakraNumberInput>
) )
return ( return (

View File

@@ -0,0 +1,227 @@
import {
useDisclosure,
Flex,
Popover,
Input,
PopoverContent,
Button,
useColorModeValue,
PopoverAnchor,
Portal,
InputGroup,
InputRightElement,
Text,
Box,
} from '@chakra-ui/react'
import { useState, useRef, ChangeEvent } from 'react'
import { isDefined } from 'utils'
import { useOutsideClick } from '@/hooks/useOutsideClick'
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
import { ChevronDownIcon } from '../icons'
const dropdownCloseAnimationDuration = 200
type Item = string | { icon?: JSX.Element; label: string; value: string }
type Props = {
selectedItem?: string
items: Item[]
placeholder?: string
debounceTimeout?: number
onSelect?: (value: string) => void
}
export const Select = ({
selectedItem,
placeholder,
items,
onSelect,
}: Props) => {
const focusedItemBgColor = useColorModeValue('gray.200', 'gray.700')
const selectedItemBgColor = useColorModeValue('blue.50', 'blue.400')
const [isTouched, setIsTouched] = useState(false)
const { onOpen, onClose, isOpen } = useDisclosure()
const [inputValue, setInputValue] = useState(
getItemLabel(
items.find((item) =>
typeof item === 'string'
? selectedItem === item
: selectedItem === item.value
)
)
)
const closeDropwdown = () => {
onClose()
}
const [keyboardFocusIndex, setKeyboardFocusIndex] = useState<
number | undefined
>()
const dropdownRef = useRef(null)
const itemsRef = useRef<(HTMLButtonElement | null)[]>([])
const inputRef = useRef<HTMLInputElement>(null)
const { ref: parentModalRef } = useParentModal()
const filteredItems = (
isTouched
? [
...items.filter((item) =>
getItemLabel(item)
.toLowerCase()
.includes((inputValue ?? '').toLowerCase())
),
]
: items
).slice(0, 50)
useOutsideClick({
ref: dropdownRef,
handler: closeDropwdown,
})
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
if (!isOpen) onOpen()
if (!isTouched) setIsTouched(true)
setInputValue(e.target.value)
}
const handleItemClick = (item: Item) => () => {
if (!isTouched) setIsTouched(true)
setInputValue(getItemLabel(item))
onSelect?.(getItemValue(item))
setKeyboardFocusIndex(undefined)
closeDropwdown()
}
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && isDefined(keyboardFocusIndex)) {
handleItemClick(filteredItems[keyboardFocusIndex])()
return setKeyboardFocusIndex(undefined)
}
if (e.key === 'ArrowDown') {
if (keyboardFocusIndex === undefined) return setKeyboardFocusIndex(0)
if (keyboardFocusIndex === filteredItems.length - 1)
return setKeyboardFocusIndex(0)
itemsRef.current[keyboardFocusIndex + 1]?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
})
return setKeyboardFocusIndex(keyboardFocusIndex + 1)
}
if (e.key === 'ArrowUp') {
if (keyboardFocusIndex === 0 || keyboardFocusIndex === undefined)
return setKeyboardFocusIndex(filteredItems.length - 1)
itemsRef.current[keyboardFocusIndex - 1]?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
})
setKeyboardFocusIndex(keyboardFocusIndex - 1)
}
}
const resetIsTouched = () => {
setTimeout(() => {
setIsTouched(false)
}, dropdownCloseAnimationDuration)
}
return (
<Flex ref={dropdownRef} w="full">
<Popover
isOpen={isOpen}
initialFocusRef={inputRef}
matchWidth
offset={[0, 1]}
isLazy
>
<PopoverAnchor>
<InputGroup>
<Box pos="absolute" py={2} pl={4} pr={6}>
{!isTouched && (
<Text noOfLines={1} data-testid="selected-item-label">
{inputValue}
</Text>
)}
</Box>
<Input
type="text"
autoComplete="off"
ref={inputRef}
className="select-input"
value={isTouched ? inputValue : ''}
placeholder={
!isTouched && inputValue !== '' ? undefined : placeholder
}
onBlur={resetIsTouched}
onChange={handleInputChange}
onFocus={onOpen}
onKeyDown={handleKeyUp}
/>
<InputRightElement pointerEvents="none" cursor="pointer">
<ChevronDownIcon />
</InputRightElement>
</InputGroup>
</PopoverAnchor>
<Portal containerRef={parentModalRef}>
<PopoverContent
maxH="35vh"
overflowY="scroll"
role="menu"
w="inherit"
shadow="lg"
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
{filteredItems.length > 0 && (
<>
{filteredItems.map((item, idx) => {
return (
<Button
ref={(el) => (itemsRef.current[idx] = el)}
minH="40px"
key={idx}
onClick={handleItemClick(item)}
fontSize="16px"
fontWeight="normal"
rounded="none"
colorScheme="gray"
role="menuitem"
variant="ghost"
bg={
keyboardFocusIndex === idx
? focusedItemBgColor
: selectedItem === getItemValue(item)
? selectedItemBgColor
: 'transparent'
}
justifyContent="flex-start"
transition="none"
leftIcon={
typeof item === 'object' ? item.icon : undefined
}
>
{getItemLabel(item)}
</Button>
)
})}
</>
)}
</PopoverContent>
</Portal>
</Popover>
</Flex>
)
}
const getItemLabel = (item?: Item) => {
if (!item) return ''
if (typeof item === 'object') return item.label
return item
}
const getItemValue = (item: Item) => {
if (typeof item === 'object') return item.value
return item
}

View File

@@ -6,7 +6,7 @@ import {
SwitchProps, SwitchProps,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import React, { useState } from 'react' import React, { useState } from 'react'
import { MoreInfoTooltip } from './MoreInfoTooltip' import { MoreInfoTooltip } from '../MoreInfoTooltip'
type SwitchWithLabelProps = { type SwitchWithLabelProps = {
label: string label: string

View File

@@ -1,134 +0,0 @@
import {
ComponentWithAs,
FormControl,
FormLabel,
HStack,
InputProps,
TextareaProps,
} from '@chakra-ui/react'
import { Variable } from 'models'
import React, { ChangeEvent, useEffect, useRef, useState } from 'react'
import { useDebouncedCallback } from 'use-debounce'
import { env } from 'utils'
import { VariablesButton } from '@/features/variables'
import { MoreInfoTooltip } from '../MoreInfoTooltip'
export type TextBoxProps = {
defaultValue?: string
onChange: (value: string) => void
TextBox:
| ComponentWithAs<'textarea', TextareaProps>
| ComponentWithAs<'input', InputProps>
withVariableButton?: boolean
debounceTimeout?: number
label?: string
moreInfoTooltip?: string
} & Omit<InputProps & TextareaProps, 'onChange' | 'defaultValue'>
export const TextBox = ({
onChange,
TextBox,
withVariableButton = true,
debounceTimeout = 1000,
label,
moreInfoTooltip,
defaultValue,
isRequired,
...props
}: TextBoxProps) => {
const textBoxRef = useRef<(HTMLInputElement & HTMLTextAreaElement) | null>(
null
)
const [value, setValue] = useState<string>(defaultValue ?? '')
const [carretPosition, setCarretPosition] = useState<number>(
defaultValue?.length ?? 0
)
const [isTouched, setIsTouched] = useState(false)
const debounced = useDebouncedCallback(
(value) => {
onChange(value)
},
env('E2E_TEST') === 'true' ? 0 : debounceTimeout
)
useEffect(() => {
if (isTouched || defaultValue === value) return
setValue(defaultValue ?? '')
}, [defaultValue, isTouched, value])
useEffect(
() => () => {
debounced.flush()
},
[debounced]
)
const handleChange = (
e: ChangeEvent<HTMLInputElement & HTMLTextAreaElement>
) => {
setIsTouched(true)
setValue(e.target.value)
debounced(e.target.value)
}
const handleVariableSelected = (variable?: Variable) => {
if (!variable) return
setIsTouched(true)
const textBeforeCursorPosition = value.substring(0, carretPosition)
const textAfterCursorPosition = value.substring(
carretPosition,
value.length
)
const newValue =
textBeforeCursorPosition +
`{{${variable.name}}}` +
textAfterCursorPosition
setValue(newValue)
debounced(newValue)
const newCarretPosition = carretPosition + `{{${variable.name}}}`.length
setCarretPosition(newCarretPosition)
textBoxRef.current?.focus()
setTimeout(() => {
if (!textBoxRef.current) return
textBoxRef.current.selectionStart = textBoxRef.current.selectionEnd =
newCarretPosition
}, 100)
}
const updateCarretPosition = () => {
if (textBoxRef.current?.selectionStart === undefined) return
setCarretPosition(textBoxRef.current.selectionStart)
}
const Input = (
<TextBox
ref={textBoxRef}
value={value}
onKeyUp={updateCarretPosition}
onClick={updateCarretPosition}
onChange={handleChange}
{...props}
/>
)
return (
<FormControl isRequired={isRequired}>
{label && (
<FormLabel>
{label}{' '}
{moreInfoTooltip && (
<MoreInfoTooltip>{moreInfoTooltip}</MoreInfoTooltip>
)}
</FormLabel>
)}
{withVariableButton ? (
<HStack spacing={0} align={'flex-end'}>
{Input}
<VariablesButton onSelectVariable={handleVariableSelected} />
</HStack>
) : (
Input
)}
</FormControl>
)
}

View File

@@ -0,0 +1,131 @@
import { VariablesButton } from '@/features/variables'
import { injectVariableInText } from '@/features/variables/utils/injectVariableInTextInput'
import { focusInput } from '@/utils/focusInput'
import {
FormControl,
FormLabel,
HStack,
Input as ChakraInput,
InputProps,
} from '@chakra-ui/react'
import { Variable } from 'models'
import React, { ReactNode, useEffect, useRef, useState } from 'react'
import { useDebouncedCallback } from 'use-debounce'
import { env } from 'utils'
import { MoreInfoTooltip } from '../MoreInfoTooltip'
export type TextInputProps = {
defaultValue?: string
onChange: (value: string) => void
debounceTimeout?: number
label?: ReactNode
moreInfoTooltip?: string
withVariableButton?: boolean
isRequired?: boolean
placeholder?: string
isDisabled?: boolean
} & Pick<
InputProps,
'autoComplete' | 'onFocus' | 'onKeyUp' | 'type' | 'autoFocus'
>
export const TextInput = ({
type,
defaultValue,
debounceTimeout = 1000,
label,
moreInfoTooltip,
withVariableButton = true,
isRequired,
placeholder,
autoComplete,
isDisabled,
autoFocus,
onChange: _onChange,
onFocus,
onKeyUp,
}: TextInputProps) => {
const inputRef = useRef<HTMLInputElement | null>(null)
const [isTouched, setIsTouched] = useState(false)
const [localValue, setLocalValue] = useState<string>(defaultValue ?? '')
const [carretPosition, setCarretPosition] = useState<number>(
localValue.length ?? 0
)
const onChange = useDebouncedCallback(
_onChange,
env('E2E_TEST') === 'true' ? 0 : debounceTimeout
)
useEffect(() => {
if (isTouched || localValue !== '' || !defaultValue || defaultValue === '')
return
setLocalValue(defaultValue ?? '')
}, [defaultValue, isTouched, localValue])
useEffect(
() => () => {
onChange.flush()
},
[onChange]
)
const changeValue = (value: string) => {
if (!isTouched) setIsTouched(true)
setLocalValue(value)
onChange(value)
}
const handleVariableSelected = (variable?: Variable) => {
if (!variable) return
const { text, carretPosition: newCarretPosition } = injectVariableInText({
variable,
text: localValue,
at: carretPosition,
})
changeValue(text)
focusInput({ at: newCarretPosition, input: inputRef.current })
}
const updateCarretPosition = (e: React.FocusEvent<HTMLInputElement>) => {
const carretPosition = e.target.selectionStart
if (!carretPosition) return
setCarretPosition(carretPosition)
}
const Input = (
<ChakraInput
type={type}
ref={inputRef}
value={localValue}
autoComplete={autoComplete}
placeholder={placeholder}
isDisabled={isDisabled}
autoFocus={autoFocus}
onFocus={onFocus}
onKeyUp={onKeyUp}
onBlur={updateCarretPosition}
onChange={(e) => changeValue(e.target.value)}
/>
)
return (
<FormControl isRequired={isRequired}>
{label && (
<FormLabel>
{label}{' '}
{moreInfoTooltip && (
<MoreInfoTooltip>{moreInfoTooltip}</MoreInfoTooltip>
)}
</FormLabel>
)}
{withVariableButton ? (
<HStack spacing={0} align={'flex-end'}>
{Input}
<VariablesButton onSelectVariable={handleVariableSelected} />
</HStack>
) : (
Input
)}
</FormControl>
)
}

View File

@@ -1,7 +1,115 @@
import { Textarea as ChakraTextarea } from '@chakra-ui/react' import { VariablesButton } from '@/features/variables'
import React from 'react' import { injectVariableInText } from '@/features/variables/utils/injectVariableInTextInput'
import { TextBox, TextBoxProps } from './TextBox' import { focusInput } from '@/utils/focusInput'
import {
FormControl,
FormLabel,
HStack,
Textarea as ChakraTextarea,
TextareaProps,
} from '@chakra-ui/react'
import { Variable } from 'models'
import React, { useEffect, useRef, useState } from 'react'
import { useDebouncedCallback } from 'use-debounce'
import { env } from 'utils'
import { MoreInfoTooltip } from '../MoreInfoTooltip'
export const Textarea = (props: Omit<TextBoxProps, 'TextBox'>) => ( type Props = {
<TextBox TextBox={ChakraTextarea} {...props} /> id?: string
) defaultValue?: string
debounceTimeout?: number
label?: string
moreInfoTooltip?: string
withVariableButton?: boolean
isRequired?: boolean
onChange: (value: string) => void
} & Pick<TextareaProps, 'minH'>
export const Textarea = ({
id,
defaultValue,
onChange: _onChange,
debounceTimeout = 1000,
label,
moreInfoTooltip,
withVariableButton = true,
isRequired,
}: Props) => {
const inputRef = useRef<HTMLTextAreaElement | null>(null)
const [isTouched, setIsTouched] = useState(false)
const [localValue, setLocalValue] = useState<string>(defaultValue ?? '')
const [carretPosition, setCarretPosition] = useState<number>(
localValue.length ?? 0
)
const onChange = useDebouncedCallback(
_onChange,
env('E2E_TEST') === 'true' ? 0 : debounceTimeout
)
useEffect(() => {
if (isTouched || localValue !== '' || !defaultValue || defaultValue === '')
return
setLocalValue(defaultValue ?? '')
}, [defaultValue, isTouched, localValue])
useEffect(
() => () => {
onChange.flush()
},
[onChange]
)
const changeValue = (value: string) => {
if (!isTouched) setIsTouched(true)
setLocalValue(value)
onChange(value)
}
const handleVariableSelected = (variable?: Variable) => {
if (!variable) return
const { text, carretPosition: newCarretPosition } = injectVariableInText({
variable,
text: localValue,
at: carretPosition,
})
changeValue(text)
focusInput({ at: newCarretPosition, input: inputRef.current })
}
const updateCarretPosition = (e: React.FocusEvent<HTMLTextAreaElement>) => {
const carretPosition = e.target.selectionStart
if (!carretPosition) return
setCarretPosition(carretPosition)
}
const Textarea = (
<ChakraTextarea
ref={inputRef}
id={id}
value={localValue}
onBlur={updateCarretPosition}
onChange={(e) => changeValue(e.target.value)}
/>
)
return (
<FormControl isRequired={isRequired}>
{label && (
<FormLabel>
{label}{' '}
{moreInfoTooltip && (
<MoreInfoTooltip>{moreInfoTooltip}</MoreInfoTooltip>
)}
</FormLabel>
)}
{withVariableButton ? (
<HStack spacing={0} align={'flex-end'}>
{Textarea}
<VariablesButton onSelectVariable={handleVariableSelected} />
</HStack>
) : (
Textarea
)}
</FormControl>
)
}

View File

@@ -36,7 +36,7 @@ export const VariableSearchInput = ({
autoFocus, autoFocus,
...inputProps ...inputProps
}: Props) => { }: Props) => {
const bg = useColorModeValue('gray.200', 'gray.700') const focusedItemBgColor = useColorModeValue('gray.200', 'gray.700')
const { onOpen, onClose, isOpen } = useDisclosure() const { onOpen, onClose, isOpen } = useDisclosure()
const { typebot, createVariable, deleteVariable, updateVariable } = const { typebot, createVariable, deleteVariable, updateVariable } =
useTypebot() useTypebot()
@@ -63,9 +63,7 @@ export const VariableSearchInput = ({
useEffect(() => { useEffect(() => {
if (autoFocus) onOpen() if (autoFocus) onOpen()
}, [autoFocus, onOpen])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const onInputChange = (e: ChangeEvent<HTMLInputElement>) => { const onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value) setInputValue(e.target.value)
@@ -178,7 +176,7 @@ export const VariableSearchInput = ({
value={inputValue} value={inputValue}
onChange={onInputChange} onChange={onInputChange}
onFocus={onOpen} onFocus={onOpen}
onKeyUp={handleKeyUp} onKeyDown={handleKeyUp}
placeholder={inputProps.placeholder ?? 'Select a variable'} placeholder={inputProps.placeholder ?? 'Select a variable'}
{...inputProps} {...inputProps}
/> />
@@ -206,7 +204,9 @@ export const VariableSearchInput = ({
variant="ghost" variant="ghost"
justifyContent="flex-start" justifyContent="flex-start"
leftIcon={<PlusIcon />} leftIcon={<PlusIcon />}
bgColor={keyboardFocusIndex === 0 ? bg : 'transparent'} bgColor={
keyboardFocusIndex === 0 ? focusedItemBgColor : 'transparent'
}
> >
Create Create
<Tag colorScheme="orange" ml="1"> <Tag colorScheme="orange" ml="1">
@@ -234,8 +234,11 @@ export const VariableSearchInput = ({
variant="ghost" variant="ghost"
justifyContent="space-between" justifyContent="space-between"
bgColor={ bgColor={
keyboardFocusIndex === indexInList ? bg : 'transparent' keyboardFocusIndex === indexInList
? focusedItemBgColor
: 'transparent'
} }
transition="none"
> >
{item.name} {item.name}
<HStack> <HStack>

View File

@@ -1,3 +1,3 @@
export { Input } from './Input' export { TextInput } from './TextInput'
export { Textarea } from './Textarea' export { Textarea } from './Textarea'
export { SmartNumberInput } from './SmartNumberInput' export { NumberInput } from './NumberInput'

View File

@@ -4,7 +4,7 @@ import React, { useState } from 'react'
import { ApiTokensList } from './ApiTokensList' import { ApiTokensList } from './ApiTokensList'
import { UploadButton } from '@/components/ImageUploadContent/UploadButton' import { UploadButton } from '@/components/ImageUploadContent/UploadButton'
import { useUser } from '@/features/account' import { useUser } from '@/features/account'
import { Input } from '@/components/inputs/Input' import { TextInput } from '@/components/inputs/TextInput'
export const MyAccountForm = () => { export const MyAccountForm = () => {
const { user, updateUser } = useUser() const { user, updateUser } = useUser()
@@ -49,8 +49,8 @@ export const MyAccountForm = () => {
</Stack> </Stack>
</HStack> </HStack>
<Input <TextInput
value={name} defaultValue={name}
onChange={handleNameChange} onChange={handleNameChange}
label="Name:" label="Name:"
withVariableButton={false} withVariableButton={false}
@@ -58,9 +58,9 @@ export const MyAccountForm = () => {
/> />
<Tooltip label="Updating email is not available. Contact the support if you want to change it."> <Tooltip label="Updating email is not available. Contact the support if you want to change it.">
<span> <span>
<Input <TextInput
type="email" type="email"
value={email} defaultValue={email}
onChange={handleEmailChange} onChange={handleEmailChange}
label="Email address:" label="Email address:"
withVariableButton={false} withVariableButton={false}

View File

@@ -1,6 +1,6 @@
import { Button, Flex, HStack, Stack, Text } from '@chakra-ui/react' import { Button, Flex, HStack, Stack, Text } from '@chakra-ui/react'
import { AudioBubbleContent } from 'models' import { AudioBubbleContent } from 'models'
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { useState } from 'react' import { useState } from 'react'
import { UploadButton } from '@/components/ImageUploadContent/UploadButton' import { UploadButton } from '@/components/ImageUploadContent/UploadButton'
@@ -52,7 +52,7 @@ export const AudioBubbleForm = ({
)} )}
{currentTab === 'link' && ( {currentTab === 'link' && (
<> <>
<Input <TextInput
placeholder="Paste the audio file link..." placeholder="Paste the audio file link..."
defaultValue={content.url ?? ''} defaultValue={content.url ?? ''}
onChange={submit} onChange={submit}

View File

@@ -1,4 +1,4 @@
import { Input, SmartNumberInput } from '@/components/inputs' import { TextInput, NumberInput } from '@/components/inputs'
import { HStack, Stack, Text } from '@chakra-ui/react' import { HStack, Stack, Text } from '@chakra-ui/react'
import { EmbedBubbleContent } from 'models' import { EmbedBubbleContent } from 'models'
import { sanitizeUrl } from 'utils' import { sanitizeUrl } from 'utils'
@@ -22,7 +22,7 @@ export const EmbedUploadContent = ({ content, onSubmit }: Props) => {
return ( return (
<Stack p="2" spacing={6}> <Stack p="2" spacing={6}>
<Stack> <Stack>
<Input <TextInput
placeholder="Paste the link or code..." placeholder="Paste the link or code..."
defaultValue={content?.url ?? ''} defaultValue={content?.url ?? ''}
onChange={handleUrlChange} onChange={handleUrlChange}
@@ -33,7 +33,7 @@ export const EmbedUploadContent = ({ content, onSubmit }: Props) => {
</Stack> </Stack>
<HStack> <HStack>
<SmartNumberInput <NumberInput
label="Height:" label="Height:"
defaultValue={content?.height} defaultValue={content?.height}
onValueChange={handleHeightChange} onValueChange={handleHeightChange}

View File

@@ -19,7 +19,7 @@ import { defaultTextBubbleContent, TextBubbleContent, Variable } from 'models'
import { ReactEditor } from 'slate-react' import { ReactEditor } from 'slate-react'
import { serializeHtml } from '@udecode/plate-serializer-html' import { serializeHtml } from '@udecode/plate-serializer-html'
import { parseHtmlStringToPlainText } from '../../utils' import { parseHtmlStringToPlainText } from '../../utils'
import { VariableSearchInput } from '@/components/VariableSearchInput' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { colors } from '@/lib/theme' import { colors } from '@/lib/theme'
import { useOutsideClick } from '@/hooks/useOutsideClick' import { useOutsideClick } from '@/hooks/useOutsideClick'

View File

@@ -4,7 +4,7 @@ import urlParser from 'js-video-url-parser/lib/base'
import 'js-video-url-parser/lib/provider/vimeo' import 'js-video-url-parser/lib/provider/vimeo'
import 'js-video-url-parser/lib/provider/youtube' import 'js-video-url-parser/lib/provider/youtube'
import { isDefined } from 'utils' import { isDefined } from 'utils'
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
type Props = { type Props = {
content?: VideoBubbleContent content?: VideoBubbleContent
@@ -24,7 +24,7 @@ export const VideoUploadContent = ({ content, onSubmit }: Props) => {
} }
return ( return (
<Stack p="2"> <Stack p="2">
<Input <TextInput
placeholder="Paste the video link..." placeholder="Paste the video link..."
defaultValue={content?.url ?? ''} defaultValue={content?.url ?? ''}
onChange={handleUrlChange} onChange={handleUrlChange}

View File

@@ -49,7 +49,7 @@ test.describe.parallel('Buttons input block', () => {
await page.click('[data-testid="block2-icon"]') await page.click('[data-testid="block2-icon"]')
await page.click('text=Multiple choice?') await page.click('text=Multiple choice?')
await page.fill('#button', 'Go') await page.getByLabel('Button label:').fill('Go')
await page.getByPlaceholder('Select a variable').nth(1).click() await page.getByPlaceholder('Select a variable').nth(1).click()
await page.getByText('var1').click() await page.getByText('var1').click()
await expect(page.getByText('Collectsvar1')).toBeVisible() await expect(page.getByText('Collectsvar1')).toBeVisible()

View File

@@ -1,7 +1,7 @@
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip' import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { SwitchWithLabel } from '@/components/SwitchWithLabel' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { VariableSearchInput } from '@/components/VariableSearchInput' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { FormControl, FormLabel, Stack } from '@chakra-ui/react' import { FormControl, FormLabel, Stack } from '@chakra-ui/react'
import { ChoiceInputOptions, Variable } from 'models' import { ChoiceInputOptions, Variable } from 'models'
import React from 'react' import React from 'react'
@@ -29,16 +29,11 @@ export const ButtonsBlockSettings = ({ options, onOptionsChange }: Props) => {
onCheckChange={handleIsMultipleChange} onCheckChange={handleIsMultipleChange}
/> />
{options?.isMultipleChoice && ( {options?.isMultipleChoice && (
<Stack> <TextInput
<FormLabel mb="0" htmlFor="button"> label="Button label:"
Button label: defaultValue={options?.buttonLabel ?? 'Send'}
</FormLabel> onChange={handleButtonLabelChange}
<Input />
id="button"
defaultValue={options?.buttonLabel ?? 'Send'}
onChange={handleButtonLabelChange}
/>
</Stack>
)} )}
<FormControl> <FormControl>
<FormLabel> <FormLabel>

View File

@@ -1,6 +1,6 @@
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { SwitchWithLabel } from '@/components/SwitchWithLabel' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { VariableSearchInput } from '@/components/VariableSearchInput' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { FormLabel, Stack } from '@chakra-ui/react' import { FormLabel, Stack } from '@chakra-ui/react'
import { DateInputOptions, Variable } from 'models' import { DateInputOptions, Variable } from 'models'
import React from 'react' import React from 'react'
@@ -40,39 +40,24 @@ export const DateInputSettingsBody = ({
onCheckChange={handleHasTimeChange} onCheckChange={handleHasTimeChange}
/> />
{options.isRange && ( {options.isRange && (
<Stack> <>
<FormLabel mb="0" htmlFor="from"> <TextInput
From label: label="From label:"
</FormLabel>
<Input
id="from"
defaultValue={options.labels.from} defaultValue={options.labels.from}
onChange={handleFromChange} onChange={handleFromChange}
/> />
</Stack> <TextInput
)} label="To label:"
{options?.isRange && (
<Stack>
<FormLabel mb="0" htmlFor="to">
To label:
</FormLabel>
<Input
id="to"
defaultValue={options.labels.to} defaultValue={options.labels.to}
onChange={handleToChange} onChange={handleToChange}
/> />
</Stack> </>
)} )}
<Stack> <TextInput
<FormLabel mb="0" htmlFor="button"> label="Button label:"
Button label: defaultValue={options.labels.button}
</FormLabel> onChange={handleButtonLabelChange}
<Input />
id="button"
defaultValue={options.labels.button}
onChange={handleButtonLabelChange}
/>
</Stack>
<Stack> <Stack>
<FormLabel mb="0" htmlFor="variable"> <FormLabel mb="0" htmlFor="variable">
Save answer in a variable: Save answer in a variable:

View File

@@ -32,9 +32,9 @@ test.describe('Date input block', () => {
await page.click(`text=Pick a date...`) await page.click(`text=Pick a date...`)
await page.click('text=Is range?') await page.click('text=Is range?')
await page.click('text=With time?') await page.click('text=With time?')
await page.fill('#from', 'Previous:') await page.getByLabel('From label:').fill('Previous:')
await page.fill('#to', 'After:') await page.getByLabel('To label:').fill('After:')
await page.fill('#button', 'Go') await page.getByLabel('Button label:').fill('Go')
await page.click('text=Restart') await page.click('text=Restart')
await expect(page.locator(`[data-testid="from-date"]`)).toHaveAttribute( await expect(page.locator(`[data-testid="from-date"]`)).toHaveAttribute(

View File

@@ -1,5 +1,5 @@
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { VariableSearchInput } from '@/components/VariableSearchInput' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { FormLabel, Stack } from '@chakra-ui/react' import { FormLabel, Stack } from '@chakra-ui/react'
import { EmailInputOptions, Variable } from 'models' import { EmailInputOptions, Variable } from 'models'
import React from 'react' import React from 'react'
@@ -24,36 +24,21 @@ export const EmailInputSettingsBody = ({
return ( return (
<Stack spacing={4}> <Stack spacing={4}>
<Stack> <TextInput
<FormLabel mb="0" htmlFor="placeholder"> label="Placeholder:"
Placeholder: defaultValue={options.labels.placeholder}
</FormLabel> onChange={handlePlaceholderChange}
<Input />
id="placeholder" <TextInput
defaultValue={options.labels.placeholder} label="Button label:"
onChange={handlePlaceholderChange} defaultValue={options.labels.button}
/> onChange={handleButtonLabelChange}
</Stack> />
<Stack> <TextInput
<FormLabel mb="0" htmlFor="button"> label="Retry message:"
Button label: defaultValue={options.retryMessageContent}
</FormLabel> onChange={handleRetryMessageChange}
<Input />
id="button"
defaultValue={options.labels.button}
onChange={handleButtonLabelChange}
/>
</Stack>
<Stack>
<FormLabel mb="0" htmlFor="retry">
Retry message:
</FormLabel>
<Input
id="retry"
defaultValue={options.retryMessageContent}
onChange={handleRetryMessageChange}
/>
</Stack>
<Stack> <Stack>
<FormLabel mb="0" htmlFor="variable"> <FormLabel mb="0" htmlFor="variable">
Save answer in a variable: Save answer in a variable:

View File

@@ -33,7 +33,7 @@ test.describe('Email input block', () => {
'Your email...' 'Your email...'
) )
await expect(page.locator('text=Your email...')).toBeVisible() await expect(page.locator('text=Your email...')).toBeVisible()
await page.fill('#button', 'Go') await page.getByLabel('Button label:').fill('Go')
await page.fill( await page.fill(
`input[value="${defaultEmailInputOptions.retryMessageContent}"]`, `input[value="${defaultEmailInputOptions.retryMessageContent}"]`,
'Try again bro' 'Try again bro'

View File

@@ -1,10 +1,10 @@
import { FormLabel, HStack, Stack, Text } from '@chakra-ui/react' import { FormLabel, HStack, Stack, Text } from '@chakra-ui/react'
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { FileInputOptions, Variable } from 'models' import { FileInputOptions, Variable } from 'models'
import React from 'react' import React from 'react'
import { Input, SmartNumberInput } from '@/components/inputs' import { TextInput, NumberInput } from '@/components/inputs'
import { SwitchWithLabel } from '@/components/SwitchWithLabel' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { VariableSearchInput } from '@/components/VariableSearchInput' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
type Props = { type Props = {
options: FileInputOptions options: FileInputOptions
@@ -49,7 +49,7 @@ export const FileInputSettings = ({ options, onOptionsChange }: Props) => {
onCheckChange={handleMultipleFilesChange} onCheckChange={handleMultipleFilesChange}
/> />
<HStack> <HStack>
<SmartNumberInput <NumberInput
label={'Size limit:'} label={'Size limit:'}
defaultValue={options.sizeLimit ?? 10} defaultValue={options.sizeLimit ?? 10}
onValueChange={handleSizeLimitChange} onValueChange={handleSizeLimitChange}
@@ -68,21 +68,21 @@ export const FileInputSettings = ({ options, onOptionsChange }: Props) => {
withVariableButton={false} withVariableButton={false}
/> />
</Stack> </Stack>
<Input <TextInput
label="Button label:" label="Button label:"
defaultValue={options.labels.button} defaultValue={options.labels.button}
onChange={handleButtonLabelChange} onChange={handleButtonLabelChange}
withVariableButton={false} withVariableButton={false}
/> />
<Input <TextInput
label="Clear button label:" label="Clear button label:"
defaultValue={options.labels.clear} defaultValue={options.labels.clear ?? ''}
onChange={updateClearButtonLabel} onChange={updateClearButtonLabel}
withVariableButton={false} withVariableButton={false}
/> />
<Input <TextInput
label="Skip button label:" label="Skip button label:"
defaultValue={options.labels.skip} defaultValue={options.labels.skip ?? ''}
onChange={updateSkipButtonLabel} onChange={updateSkipButtonLabel}
withVariableButton={false} withVariableButton={false}
/> />

View File

@@ -1,5 +1,5 @@
import { Input, SmartNumberInput } from '@/components/inputs' import { TextInput, NumberInput } from '@/components/inputs'
import { VariableSearchInput } from '@/components/VariableSearchInput' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { removeUndefinedFields } from '@/utils/helpers' import { removeUndefinedFields } from '@/utils/helpers'
import { FormLabel, Stack } from '@chakra-ui/react' import { FormLabel, Stack } from '@chakra-ui/react'
import { NumberInputOptions, Variable } from 'models' import { NumberInputOptions, Variable } from 'models'
@@ -30,39 +30,29 @@ export const NumberInputSettingsBody = ({
return ( return (
<Stack spacing={4}> <Stack spacing={4}>
<Stack> <TextInput
<FormLabel mb="0" htmlFor="placeholder"> label="Placeholder:"
Placeholder: defaultValue={options.labels.placeholder}
</FormLabel> onChange={handlePlaceholderChange}
<Input />
id="placeholder" <TextInput
defaultValue={options.labels.placeholder} label="Button label:"
onChange={handlePlaceholderChange} defaultValue={options?.labels?.button ?? 'Send'}
/> onChange={handleButtonLabelChange}
</Stack> />
<Stack> <NumberInput
<FormLabel mb="0" htmlFor="button">
Button label:
</FormLabel>
<Input
id="button"
defaultValue={options?.labels?.button ?? 'Send'}
onChange={handleButtonLabelChange}
/>
</Stack>
<SmartNumberInput
label="Min:" label="Min:"
defaultValue={options.min} defaultValue={options.min}
onValueChange={handleMinChange} onValueChange={handleMinChange}
withVariableButton={false} withVariableButton={false}
/> />
<SmartNumberInput <NumberInput
label="Max:" label="Max:"
defaultValue={options.max} defaultValue={options.max}
onValueChange={handleMaxChange} onValueChange={handleMaxChange}
withVariableButton={false} withVariableButton={false}
/> />
<SmartNumberInput <NumberInput
label="Step:" label="Step:"
defaultValue={options.step} defaultValue={options.step}
onValueChange={handleBlockChange} onValueChange={handleBlockChange}

View File

@@ -28,9 +28,9 @@ test.describe('Number input block', () => {
await expect(page.getByRole('button', { name: 'Send' })).toBeDisabled() await expect(page.getByRole('button', { name: 'Send' })).toBeDisabled()
await page.click(`text=${defaultNumberInputOptions.labels.placeholder}`) await page.click(`text=${defaultNumberInputOptions.labels.placeholder}`)
await page.fill('#placeholder', 'Your number...') await page.getByLabel('Placeholder:').fill('Your number...')
await expect(page.locator('text=Your number...')).toBeVisible() await expect(page.locator('text=Your number...')).toBeVisible()
await page.fill('#button', 'Go') await page.getByLabel('Button label:').fill('Go')
await page.fill('[role="spinbutton"] >> nth=0', '0') await page.fill('[role="spinbutton"] >> nth=0', '0')
await page.fill('[role="spinbutton"] >> nth=1', '100') await page.fill('[role="spinbutton"] >> nth=1', '100')
await page.fill('[role="spinbutton"] >> nth=2', '10') await page.fill('[role="spinbutton"] >> nth=2', '10')

View File

@@ -16,7 +16,7 @@ import React, { ChangeEvent, useState } from 'react'
import { currencies } from './currencies' import { currencies } from './currencies'
import { StripeConfigModal } from './StripeConfigModal' import { StripeConfigModal } from './StripeConfigModal'
import { CredentialsDropdown } from '@/features/credentials' import { CredentialsDropdown } from '@/features/credentials'
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
type Props = { type Props = {
options: PaymentInputOptions options: PaymentInputOptions
@@ -105,14 +105,12 @@ export const PaymentSettings = ({ options, onOptionsChange }: Props) => {
/> />
</Stack> </Stack>
<HStack> <HStack>
<Stack> <TextInput
<Text>Price amount:</Text> label="Price amount:"
<Input onChange={handleAmountChange}
onChange={handleAmountChange} defaultValue={options.amount ?? ''}
defaultValue={options.amount} placeholder="30.00"
placeholder="30.00" />
/>
</Stack>
<Stack> <Stack>
<Text>Currency:</Text> <Text>Currency:</Text>
<Select <Select
@@ -128,22 +126,18 @@ export const PaymentSettings = ({ options, onOptionsChange }: Props) => {
</Select> </Select>
</Stack> </Stack>
</HStack> </HStack>
<Stack> <TextInput
<Text>Button label:</Text> label="Button label:"
<Input onChange={handleButtonLabelChange}
onChange={handleButtonLabelChange} defaultValue={options.labels.button}
defaultValue={options.labels.button} placeholder="Pay"
placeholder="Pay" />
/> <TextInput
</Stack> label="Success message:"
<Stack> onChange={handleSuccessLabelChange}
<Text>Success message:</Text> defaultValue={options.labels.success ?? 'Success'}
<Input placeholder="Success"
onChange={handleSuccessLabelChange} />
defaultValue={options.labels.success ?? 'Success'}
placeholder="Success"
/>
</Stack>
<Accordion allowToggle> <Accordion allowToggle>
<AccordionItem> <AccordionItem>
<AccordionButton justifyContent="space-between"> <AccordionButton justifyContent="space-between">
@@ -151,30 +145,24 @@ export const PaymentSettings = ({ options, onOptionsChange }: Props) => {
<AccordionIcon /> <AccordionIcon />
</AccordionButton> </AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="6"> <AccordionPanel pb={4} as={Stack} spacing="6">
<Stack> <TextInput
<Text>Name:</Text> label="Name:"
<Input defaultValue={options.additionalInformation?.name ?? ''}
defaultValue={options.additionalInformation?.name ?? ''} onChange={handleNameChange}
onChange={handleNameChange} placeholder="John Smith"
placeholder="John Smith" />
/> <TextInput
</Stack> label="Email:"
<Stack> defaultValue={options.additionalInformation?.email ?? ''}
<Text>Email:</Text> onChange={handleEmailChange}
<Input placeholder="john@gmail.com"
defaultValue={options.additionalInformation?.email ?? ''} />
onChange={handleEmailChange} <TextInput
placeholder="john@gmail.com" label="Phone number:"
/> defaultValue={options.additionalInformation?.phoneNumber ?? ''}
</Stack> onChange={handlePhoneNumberChange}
<Stack> placeholder="+33XXXXXXXXX"
<Text>Phone number:</Text> />
<Input
defaultValue={options.additionalInformation?.phoneNumber ?? ''}
onChange={handlePhoneNumberChange}
placeholder="+33XXXXXXXXX"
/>
</Stack>
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>

View File

@@ -19,7 +19,7 @@ import React, { useState } from 'react'
import { useWorkspace } from '@/features/workspace' import { useWorkspace } from '@/features/workspace'
import { omit } from 'utils' import { omit } from 'utils'
import { useToast } from '@/hooks/useToast' import { useToast } from '@/hooks/useToast'
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip' import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { TextLink } from '@/components/TextLink' import { TextLink } from '@/components/TextLink'
import { createCredentialsQuery } from '@/features/credentials' import { createCredentialsQuery } from '@/features/credentials'
@@ -102,14 +102,13 @@ export const StripeConfigModal = ({
<ModalCloseButton /> <ModalCloseButton />
<ModalBody> <ModalBody>
<Stack as="form" spacing={4}> <Stack as="form" spacing={4}>
<FormControl isRequired> <TextInput
<FormLabel>Account name:</FormLabel> isRequired
<Input label="Account name:"
onChange={handleNameChange} onChange={handleNameChange}
placeholder="Typebot" placeholder="Typebot"
withVariableButton={false} withVariableButton={false}
/> />
</FormControl>
<Stack> <Stack>
<FormLabel> <FormLabel>
Test keys:{' '} Test keys:{' '}
@@ -118,34 +117,30 @@ export const StripeConfigModal = ({
</MoreInfoTooltip> </MoreInfoTooltip>
</FormLabel> </FormLabel>
<HStack> <HStack>
<FormControl> <TextInput
<Input onChange={handleTestPublicKeyChange}
onChange={handleTestPublicKeyChange} placeholder="pk_test_..."
placeholder="pk_test_..." withVariableButton={false}
withVariableButton={false} />
/> <TextInput
</FormControl> onChange={handleTestSecretKeyChange}
<FormControl> placeholder="sk_test_..."
<Input withVariableButton={false}
onChange={handleTestSecretKeyChange} />
placeholder="sk_test_..."
withVariableButton={false}
/>
</FormControl>
</HStack> </HStack>
</Stack> </Stack>
<Stack> <Stack>
<FormLabel>Live keys:</FormLabel> <FormLabel>Live keys:</FormLabel>
<HStack> <HStack>
<FormControl> <FormControl>
<Input <TextInput
onChange={handlePublicKeyChange} onChange={handlePublicKeyChange}
placeholder="pk_live_..." placeholder="pk_live_..."
withVariableButton={false} withVariableButton={false}
/> />
</FormControl> </FormControl>
<FormControl> <FormControl>
<Input <TextInput
onChange={handleSecretKeyChange} onChange={handleSecretKeyChange}
placeholder="sk_live_..." placeholder="sk_live_..."
withVariableButton={false} withVariableButton={false}

View File

@@ -1,5 +1,5 @@
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { VariableSearchInput } from '@/components/VariableSearchInput' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { FormLabel, Stack } from '@chakra-ui/react' import { FormLabel, Stack } from '@chakra-ui/react'
import { PhoneNumberInputOptions, Variable } from 'models' import { PhoneNumberInputOptions, Variable } from 'models'
import React from 'react' import React from 'react'
@@ -27,26 +27,16 @@ export const PhoneNumberSettingsBody = ({
return ( return (
<Stack spacing={4}> <Stack spacing={4}>
<Stack> <TextInput
<FormLabel mb="0" htmlFor="placeholder"> label="Placeholder:"
Placeholder: defaultValue={options.labels.placeholder}
</FormLabel> onChange={handlePlaceholderChange}
<Input />
id="placeholder" <TextInput
defaultValue={options.labels.placeholder} label="Button label:"
onChange={handlePlaceholderChange} defaultValue={options.labels.button}
/> onChange={handleButtonLabelChange}
</Stack> />
<Stack>
<FormLabel mb="0" htmlFor="button">
Button label:
</FormLabel>
<Input
id="button"
defaultValue={options.labels.button}
onChange={handleButtonLabelChange}
/>
</Stack>
<Stack> <Stack>
<FormLabel mb="0" htmlFor="button"> <FormLabel mb="0" htmlFor="button">
Default country: Default country:
@@ -56,16 +46,11 @@ export const PhoneNumberSettingsBody = ({
countryCode={options.defaultCountryCode} countryCode={options.defaultCountryCode}
/> />
</Stack> </Stack>
<Stack> <TextInput
<FormLabel mb="0" htmlFor="retry"> label="Retry message:"
Retry message: defaultValue={options.retryMessageContent}
</FormLabel> onChange={handleRetryMessageChange}
<Input />
id="retry"
defaultValue={options.retryMessageContent}
onChange={handleRetryMessageChange}
/>
</Stack>
<Stack> <Stack>
<FormLabel mb="0" htmlFor="variable"> <FormLabel mb="0" htmlFor="variable">
Save answer in a variable: Save answer in a variable:

View File

@@ -28,8 +28,8 @@ test.describe('Phone input block', () => {
await expect(page.getByRole('button', { name: 'Send' })).toBeDisabled() await expect(page.getByRole('button', { name: 'Send' })).toBeDisabled()
await page.click(`text=${defaultPhoneInputOptions.labels.placeholder}`) await page.click(`text=${defaultPhoneInputOptions.labels.placeholder}`)
await page.fill('#placeholder', '+33 XX XX XX XX') await page.getByLabel('Placeholder:').fill('+33 XX XX XX XX')
await page.fill('#button', 'Go') await page.getByLabel('Button label:').fill('Go')
await page.fill( await page.fill(
`input[value="${defaultPhoneInputOptions.retryMessageContent}"]`, `input[value="${defaultPhoneInputOptions.retryMessageContent}"]`,
'Try again bro' 'Try again bro'

View File

@@ -2,9 +2,9 @@ import { FormLabel, Stack } from '@chakra-ui/react'
import { DropdownList } from '@/components/DropdownList' import { DropdownList } from '@/components/DropdownList'
import { RatingInputOptions, Variable } from 'models' import { RatingInputOptions, Variable } from 'models'
import React from 'react' import React from 'react'
import { SwitchWithLabel } from '@/components/SwitchWithLabel' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { VariableSearchInput } from '@/components/VariableSearchInput' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
type RatingInputSettingsProps = { type RatingInputSettingsProps = {
options: RatingInputOptions options: RatingInputOptions
@@ -77,56 +77,36 @@ export const RatingInputSettings = ({
/> />
)} )}
{options.buttonType === 'Icons' && options.customIcon.isEnabled && ( {options.buttonType === 'Icons' && options.customIcon.isEnabled && (
<Stack> <TextInput
<FormLabel mb="0" htmlFor="svg"> label="Icon SVG:"
Icon SVG: defaultValue={options.customIcon.svg}
</FormLabel> onChange={handleIconSvgChange}
<Input placeholder="<svg>...</svg>"
id="svg" />
defaultValue={options.customIcon.svg}
onChange={handleIconSvgChange}
placeholder="<svg>...</svg>"
/>
</Stack>
)} )}
<Stack> <TextInput
<FormLabel mb="0" htmlFor="button"> label={`${options.buttonType === 'Icons' ? '1' : '0'} label:`}
{options.buttonType === 'Icons' ? '1' : '0'} label: defaultValue={options.labels.left}
</FormLabel> onChange={handleLeftLabelChange}
<Input placeholder="Not likely at all"
id="button" />
defaultValue={options.labels.left} <TextInput
onChange={handleLeftLabelChange} label={`${options.length} label:`}
placeholder="Not likely at all" defaultValue={options.labels.right}
/> onChange={handleRightLabelChange}
</Stack> placeholder="Extremely likely"
<Stack> />
<FormLabel mb="0" htmlFor="button">
{options.length} label:
</FormLabel>
<Input
id="button"
defaultValue={options.labels.right}
onChange={handleRightLabelChange}
placeholder="Extremely likely"
/>
</Stack>
<SwitchWithLabel <SwitchWithLabel
label="One click submit" label="One click submit"
moreInfoContent='If enabled, the answer will be submitted as soon as the user clicks on a rating instead of showing the "Send" button.' moreInfoContent='If enabled, the answer will be submitted as soon as the user clicks on a rating instead of showing the "Send" button.'
initialValue={options.isOneClickSubmitEnabled ?? false} initialValue={options.isOneClickSubmitEnabled ?? false}
onCheckChange={handleOneClickSubmitChange} onCheckChange={handleOneClickSubmitChange}
/> />
<Stack> <TextInput
<FormLabel mb="0" htmlFor="button"> label="Button label:"
Button label: defaultValue={options.labels.button}
</FormLabel> onChange={handleButtonLabelChange}
<Input />
id="button"
defaultValue={options.labels.button}
onChange={handleButtonLabelChange}
/>
</Stack>
<Stack> <Stack>
<FormLabel mb="0" htmlFor="variable"> <FormLabel mb="0" htmlFor="variable">
Save answer in a variable: Save answer in a variable:

View File

@@ -1,6 +1,6 @@
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { SwitchWithLabel } from '@/components/SwitchWithLabel' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { VariableSearchInput } from '@/components/VariableSearchInput' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { FormLabel, Stack } from '@chakra-ui/react' import { FormLabel, Stack } from '@chakra-ui/react'
import { TextInputOptions, Variable } from 'models' import { TextInputOptions, Variable } from 'models'
import React from 'react' import React from 'react'
@@ -30,26 +30,16 @@ export const TextInputSettingsBody = ({
initialValue={options?.isLong ?? false} initialValue={options?.isLong ?? false}
onCheckChange={handleLongChange} onCheckChange={handleLongChange}
/> />
<Stack> <TextInput
<FormLabel mb="0" htmlFor="placeholder"> label="Placeholder:"
Placeholder: defaultValue={options.labels.placeholder}
</FormLabel> onChange={handlePlaceholderChange}
<Input />
id="placeholder" <TextInput
defaultValue={options.labels.placeholder} label="Button label:"
onChange={handlePlaceholderChange} defaultValue={options.labels.button}
/> onChange={handleButtonLabelChange}
</Stack> />
<Stack>
<FormLabel mb="0" htmlFor="button">
Button label:
</FormLabel>
<Input
id="button"
defaultValue={options.labels.button}
onChange={handleButtonLabelChange}
/>
</Stack>
<Stack> <Stack>
<FormLabel mb="0" htmlFor="variable"> <FormLabel mb="0" htmlFor="variable">
Save answer in a variable: Save answer in a variable:

View File

@@ -28,8 +28,8 @@ test.describe.parallel('Text input block', () => {
await expect(page.getByRole('button', { name: 'Send' })).toBeDisabled() await expect(page.getByRole('button', { name: 'Send' })).toBeDisabled()
await page.click(`text=${defaultTextInputOptions.labels.placeholder}`) await page.click(`text=${defaultTextInputOptions.labels.placeholder}`)
await page.fill('#placeholder', 'Your name...') await page.getByLabel('Placeholder:').fill('Your name...')
await page.fill('#button', 'Go') await page.getByLabel('Button label:').fill('Go')
await page.click('text=Long text?') await page.click('text=Long text?')
await page.click('text=Restart') await page.click('text=Restart')

View File

@@ -1,5 +1,5 @@
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { VariableSearchInput } from '@/components/VariableSearchInput' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { FormLabel, Stack } from '@chakra-ui/react' import { FormLabel, Stack } from '@chakra-ui/react'
import { UrlInputOptions, Variable } from 'models' import { UrlInputOptions, Variable } from 'models'
import React from 'react' import React from 'react'
@@ -24,36 +24,21 @@ export const UrlInputSettingsBody = ({
return ( return (
<Stack spacing={4}> <Stack spacing={4}>
<Stack> <TextInput
<FormLabel mb="0" htmlFor="placeholder"> label="Placeholder:"
Placeholder: defaultValue={options.labels.placeholder}
</FormLabel> onChange={handlePlaceholderChange}
<Input />
id="placeholder" <TextInput
defaultValue={options.labels.placeholder} label="Button label:"
onChange={handlePlaceholderChange} defaultValue={options.labels.button}
/> onChange={handleButtonLabelChange}
</Stack> />
<Stack> <TextInput
<FormLabel mb="0" htmlFor="button"> label="Retry message:"
Button label: defaultValue={options.retryMessageContent}
</FormLabel> onChange={handleRetryMessageChange}
<Input />
id="button"
defaultValue={options.labels.button}
onChange={handleButtonLabelChange}
/>
</Stack>
<Stack>
<FormLabel mb="0" htmlFor="retry">
Retry message:
</FormLabel>
<Input
id="retry"
defaultValue={options.retryMessageContent}
onChange={handleRetryMessageChange}
/>
</Stack>
<Stack> <Stack>
<FormLabel mb="0" htmlFor="variable"> <FormLabel mb="0" htmlFor="variable">
Save answer in a variable: Save answer in a variable:

View File

@@ -30,9 +30,9 @@ test.describe('Url input block', () => {
).toBeDisabled() ).toBeDisabled()
await page.click(`text=${defaultUrlInputOptions.labels.placeholder}`) await page.click(`text=${defaultUrlInputOptions.labels.placeholder}`)
await page.fill('#placeholder', 'Your URL...') await page.getByLabel('Placeholder:').fill('Your URL...')
await expect(page.locator('text=Your URL...')).toBeVisible() await expect(page.locator('text=Your URL...')).toBeVisible()
await page.fill('#button', 'Go') await page.getByLabel('Button label:').fill('Go')
await page.fill( await page.fill(
`input[value="${defaultUrlInputOptions.retryMessageContent}"]`, `input[value="${defaultUrlInputOptions.retryMessageContent}"]`,
'Try again bro' 'Try again bro'

View File

@@ -1,4 +1,4 @@
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { import {
Accordion, Accordion,
AccordionButton, AccordionButton,
@@ -18,7 +18,7 @@ type Props = {
export const ChatwootSettingsForm = ({ options, onOptionsChange }: Props) => { export const ChatwootSettingsForm = ({ options, onOptionsChange }: Props) => {
return ( return (
<Stack spacing={4}> <Stack spacing={4}>
<Input <TextInput
isRequired isRequired
label="Base URL" label="Base URL"
defaultValue={options.baseUrl} defaultValue={options.baseUrl}
@@ -27,7 +27,7 @@ export const ChatwootSettingsForm = ({ options, onOptionsChange }: Props) => {
}} }}
withVariableButton={false} withVariableButton={false}
/> />
<Input <TextInput
isRequired isRequired
label="Website token" label="Website token"
defaultValue={options.websiteToken} defaultValue={options.websiteToken}
@@ -43,14 +43,14 @@ export const ChatwootSettingsForm = ({ options, onOptionsChange }: Props) => {
<AccordionIcon /> <AccordionIcon />
</AccordionButton> </AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="4"> <AccordionPanel pb={4} as={Stack} spacing="4">
<Input <TextInput
label="ID" label="ID"
defaultValue={options.user?.id} defaultValue={options.user?.id}
onChange={(id: string) => { onChange={(id: string) => {
onOptionsChange({ ...options, user: { ...options.user, id } }) onOptionsChange({ ...options, user: { ...options.user, id } })
}} }}
/> />
<Input <TextInput
label="Name" label="Name"
defaultValue={options.user?.name} defaultValue={options.user?.name}
onChange={(name: string) => { onChange={(name: string) => {
@@ -60,7 +60,7 @@ export const ChatwootSettingsForm = ({ options, onOptionsChange }: Props) => {
}) })
}} }}
/> />
<Input <TextInput
label="Email" label="Email"
defaultValue={options.user?.email} defaultValue={options.user?.email}
onChange={(email: string) => { onChange={(email: string) => {
@@ -70,7 +70,7 @@ export const ChatwootSettingsForm = ({ options, onOptionsChange }: Props) => {
}) })
}} }}
/> />
<Input <TextInput
label="Avatar URL" label="Avatar URL"
defaultValue={options.user?.avatarUrl} defaultValue={options.user?.avatarUrl}
onChange={(avatarUrl: string) => { onChange={(avatarUrl: string) => {
@@ -80,7 +80,7 @@ export const ChatwootSettingsForm = ({ options, onOptionsChange }: Props) => {
}) })
}} }}
/> />
<Input <TextInput
label="Phone number" label="Phone number"
defaultValue={options.user?.phoneNumber} defaultValue={options.user?.phoneNumber}
onChange={(phoneNumber: string) => { onChange={(phoneNumber: string) => {

View File

@@ -1,4 +1,4 @@
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { import {
Accordion, Accordion,
AccordionButton, AccordionButton,
@@ -42,39 +42,24 @@ export const GoogleAnalyticsSettings = ({
return ( return (
<Stack spacing={4}> <Stack spacing={4}>
<Stack> <TextInput
<FormLabel mb="0" htmlFor="tracking-id"> label="Tracking ID:"
Tracking ID: defaultValue={options?.trackingId ?? ''}
</FormLabel> placeholder="G-123456..."
<Input onChange={handleTrackingIdChange}
id="tracking-id" />
defaultValue={options?.trackingId ?? ''} <TextInput
placeholder="G-123456..." label="Event category:"
onChange={handleTrackingIdChange} defaultValue={options?.category ?? ''}
/> placeholder="Example: Typebot"
</Stack> onChange={handleCategoryChange}
<Stack> />
<FormLabel mb="0" htmlFor="category"> <TextInput
Event category: label="Event action:"
</FormLabel> defaultValue={options?.action ?? ''}
<Input placeholder="Example: Submit email"
id="category" onChange={handleActionChange}
defaultValue={options?.category ?? ''} />
placeholder="Example: Typebot"
onChange={handleCategoryChange}
/>
</Stack>
<Stack>
<FormLabel mb="0" htmlFor="action">
Event action:
</FormLabel>
<Input
id="action"
defaultValue={options?.action ?? ''}
placeholder="Example: Submit email"
onChange={handleActionChange}
/>
</Stack>
<Accordion allowToggle> <Accordion allowToggle>
<AccordionItem> <AccordionItem>
<h2> <h2>
@@ -86,28 +71,28 @@ export const GoogleAnalyticsSettings = ({
</AccordionButton> </AccordionButton>
</h2> </h2>
<AccordionPanel pb={4} as={Stack} spacing="6"> <AccordionPanel pb={4} as={Stack} spacing="6">
<Stack> <TextInput
<FormLabel mb="0" htmlFor="label"> label={
Event label <Tag>Optional</Tag>: <>
</FormLabel> Event label <Tag>Optional</Tag>:
<Input </>
id="label" }
defaultValue={options?.label ?? ''} defaultValue={options?.label ?? ''}
placeholder="Example: Campaign Z" placeholder="Example: Campaign Z"
onChange={handleLabelChange} onChange={handleLabelChange}
/> />
</Stack> <TextInput
<Stack> label={
<FormLabel mb="0" htmlFor="value"> <>
Event value <Tag>Optional</Tag>: <FormLabel mb="0" htmlFor="value">
</FormLabel> Event value <Tag>Optional</Tag>:
<Input </FormLabel>
id="value" </>
defaultValue={options?.value?.toString() ?? ''} }
placeholder="Example: 0" defaultValue={options?.value?.toString() ?? ''}
onChange={handleValueChange} placeholder="Example: 0"
/> onChange={handleValueChange}
</Stack> />
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>

View File

@@ -2,7 +2,7 @@ import { Stack } from '@chakra-ui/react'
import { DropdownList } from '@/components/DropdownList' import { DropdownList } from '@/components/DropdownList'
import { Cell } from 'models' import { Cell } from 'models'
import { TableListItemProps } from '@/components/TableList' import { TableListItemProps } from '@/components/TableList'
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
export const CellWithValueStack = ({ export const CellWithValueStack = ({
item, item,
@@ -25,7 +25,7 @@ export const CellWithValueStack = ({
items={columns} items={columns}
placeholder="Select a column" placeholder="Select a column"
/> />
<Input <TextInput
defaultValue={item.value ?? ''} defaultValue={item.value ?? ''}
onChange={handleValueChange} onChange={handleValueChange}
placeholder="Type a value..." placeholder="Type a value..."

View File

@@ -2,7 +2,7 @@ import { Stack } from '@chakra-ui/react'
import { DropdownList } from '@/components/DropdownList' import { DropdownList } from '@/components/DropdownList'
import { ExtractingCell, Variable } from 'models' import { ExtractingCell, Variable } from 'models'
import { TableListItemProps } from '@/components/TableList' import { TableListItemProps } from '@/components/TableList'
import { VariableSearchInput } from '@/components/VariableSearchInput' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
export const CellWithVariableIdStack = ({ export const CellWithVariableIdStack = ({
item, item,

View File

@@ -1,5 +1,5 @@
import { DropdownList } from '@/components/DropdownList' import { DropdownList } from '@/components/DropdownList'
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { TableListItemProps } from '@/components/TableList' import { TableListItemProps } from '@/components/TableList'
import { Stack } from '@chakra-ui/react' import { Stack } from '@chakra-ui/react'
import { ComparisonOperators, RowsFilterComparison } from 'models' import { ComparisonOperators, RowsFilterComparison } from 'models'
@@ -42,7 +42,7 @@ export const RowsFilterComparisonItem = ({
placeholder="Select an operator" placeholder="Select an operator"
/> />
{item.comparisonOperator !== ComparisonOperators.IS_SET && ( {item.comparisonOperator !== ComparisonOperators.IS_SET && (
<Input <TextInput
defaultValue={item.value ?? ''} defaultValue={item.value ?? ''}
onChange={handleChangeValue} onChange={handleChangeValue}
placeholder="Type a value..." placeholder="Type a value..."

View File

@@ -1,8 +1,6 @@
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip' import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { SearchableDropdown } from '@/components/SearchableDropdown' import { Select } from '@/components/inputs/Select'
import { HStack, Input } from '@chakra-ui/react' import { HStack, Input } from '@chakra-ui/react'
import { useMemo } from 'react'
import { isDefined } from 'utils'
import { Sheet } from '../../types' import { Sheet } from '../../types'
type Props = { type Props = {
@@ -18,16 +16,6 @@ export const SheetsDropdown = ({
sheetId, sheetId,
onSelectSheetId, onSelectSheetId,
}: Props) => { }: Props) => {
const currentSheet = useMemo(
() => sheets?.find((s) => s.id === sheetId),
[sheetId, sheets]
)
const handleSpreadsheetSelect = (name: string) => {
const id = sheets?.find((s) => s.name === name)?.id
if (isDefined(id)) onSelectSheetId(id)
}
if (isLoading) return <Input value="Loading..." isDisabled /> if (isLoading) return <Input value="Loading..." isDisabled />
if (!sheets || sheets.length === 0) if (!sheets || sheets.length === 0)
return ( return (
@@ -40,10 +28,10 @@ export const SheetsDropdown = ({
</HStack> </HStack>
) )
return ( return (
<SearchableDropdown <Select
selectedItem={currentSheet?.name} selectedItem={sheetId}
items={(sheets ?? []).map((s) => s.name)} items={(sheets ?? []).map((s) => ({ label: s.name, value: s.id }))}
onValueChange={handleSpreadsheetSelect} onSelect={onSelectSheetId}
placeholder={'Select the sheet'} placeholder={'Select the sheet'}
/> />
) )

View File

@@ -1,6 +1,5 @@
import { SearchableDropdown } from '@/components/SearchableDropdown' import { Select } from '@/components/inputs/Select'
import { Input, Tooltip } from '@chakra-ui/react' import { Input, Tooltip } from '@chakra-ui/react'
import { useMemo } from 'react'
import { useSpreadsheets } from '../../hooks/useSpreadsheets' import { useSpreadsheets } from '../../hooks/useSpreadsheets'
type Props = { type Props = {
@@ -17,15 +16,7 @@ export const SpreadsheetsDropdown = ({
const { spreadsheets, isLoading } = useSpreadsheets({ const { spreadsheets, isLoading } = useSpreadsheets({
credentialsId, credentialsId,
}) })
const currentSpreadsheet = useMemo(
() => spreadsheets?.find((s) => s.id === spreadsheetId),
[spreadsheetId, spreadsheets]
)
const handleSpreadsheetSelect = (name: string) => {
const id = spreadsheets?.find((s) => s.name === name)?.id
if (id) onSelectSpreadsheetId(id)
}
if (isLoading) return <Input value="Loading..." isDisabled /> if (isLoading) return <Input value="Loading..." isDisabled />
if (!spreadsheets || spreadsheets.length === 0) if (!spreadsheets || spreadsheets.length === 0)
return ( return (
@@ -36,10 +27,13 @@ export const SpreadsheetsDropdown = ({
</Tooltip> </Tooltip>
) )
return ( return (
<SearchableDropdown <Select
selectedItem={currentSpreadsheet?.name} selectedItem={spreadsheetId}
items={(spreadsheets ?? []).map((s) => s.name)} items={(spreadsheets ?? []).map((spreadsheet) => ({
onValueChange={handleSpreadsheetSelect} label: spreadsheet.name,
value: spreadsheet.id,
}))}
onSelect={onSelectSpreadsheetId}
placeholder={'Search for spreadsheet'} placeholder={'Search for spreadsheet'}
/> />
) )

View File

@@ -7,15 +7,15 @@ import {
Switch, Switch,
FormLabel, FormLabel,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { CredentialsType, SendEmailOptions, Variable } from 'models' import { CredentialsType, SendEmailOptions, Variable } from 'models'
import React, { useState } from 'react' import React, { useState } from 'react'
import { env, isNotEmpty } from 'utils' import { env, isNotEmpty } from 'utils'
import { SmtpConfigModal } from './SmtpConfigModal' import { SmtpConfigModal } from './SmtpConfigModal'
import { SwitchWithLabel } from '@/components/SwitchWithLabel' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { VariableSearchInput } from '@/components/VariableSearchInput' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { CredentialsDropdown } from '@/features/credentials' import { CredentialsDropdown } from '@/features/credentials'
import { Input, Textarea } from '@/components/inputs' import { TextInput, Textarea } from '@/components/inputs'
type Props = { type Props = {
options: SendEmailOptions options: SendEmailOptions
@@ -120,46 +120,35 @@ export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
refreshDropdownKey={refreshCredentialsKey} refreshDropdownKey={refreshCredentialsKey}
/> />
</Stack> </Stack>
<Stack> <TextInput
<Text>Reply to: </Text> label="Reply to:"
<Input onChange={handleReplyToChange}
onChange={handleReplyToChange} defaultValue={options.replyTo}
defaultValue={options.replyTo} placeholder={'email@gmail.com'}
placeholder={'email@gmail.com'} />
/> <TextInput
</Stack> label="To:"
<Stack> onChange={handleToChange}
<Text>To: </Text> defaultValue={options.recipients.join(', ')}
<Input placeholder="email1@gmail.com, email2@gmail.com"
onChange={handleToChange} />
defaultValue={options.recipients.join(', ')} <TextInput
placeholder="email1@gmail.com, email2@gmail.com" label="Cc:"
/> onChange={handleCcChange}
</Stack> defaultValue={options.cc?.join(', ') ?? ''}
<Stack> placeholder="email1@gmail.com, email2@gmail.com"
<Text>Cc: </Text> />
<Input <TextInput
onChange={handleCcChange} label="Bcc:"
defaultValue={options.cc?.join(', ') ?? ''} onChange={handleBccChange}
placeholder="email1@gmail.com, email2@gmail.com" defaultValue={options.bcc?.join(', ') ?? ''}
/> placeholder="email1@gmail.com, email2@gmail.com"
</Stack> />
<Stack> <TextInput
<Text>Bcc: </Text> label="Subject:"
<Input onChange={handleSubjectChange}
onChange={handleBccChange} defaultValue={options.subject ?? ''}
defaultValue={options.bcc?.join(', ') ?? ''} />
placeholder="email1@gmail.com, email2@gmail.com"
/>
</Stack>
<Stack>
<Text>Subject: </Text>
<Input
data-testid="subject-input"
onChange={handleSubjectChange}
defaultValue={options.subject ?? ''}
/>
</Stack>
<SwitchWithLabel <SwitchWithLabel
label={'Custom content?'} label={'Custom content?'}
moreInfoContent="By default, the email body will be a recap of what has been collected so far. You can override it with this option." moreInfoContent="By default, the email body will be a recap of what has been collected so far. You can override it with this option."

View File

@@ -1,5 +1,5 @@
import { Input, SmartNumberInput } from '@/components/inputs' import { TextInput, NumberInput } from '@/components/inputs'
import { SwitchWithLabel } from '@/components/SwitchWithLabel' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { Stack } from '@chakra-ui/react' import { Stack } from '@chakra-ui/react'
import { isDefined } from '@udecode/plate-common' import { isDefined } from '@udecode/plate-common'
import { SmtpCredentialsData } from 'models' import { SmtpCredentialsData } from 'models'
@@ -27,7 +27,7 @@ export const SmtpConfigForm = ({ config, onConfigChange }: Props) => {
return ( return (
<Stack as="form" spacing={4}> <Stack as="form" spacing={4}>
<Input <TextInput
isRequired isRequired
label="From email" label="From email"
defaultValue={config.from.email ?? ''} defaultValue={config.from.email ?? ''}
@@ -35,14 +35,14 @@ export const SmtpConfigForm = ({ config, onConfigChange }: Props) => {
placeholder="notifications@provider.com" placeholder="notifications@provider.com"
withVariableButton={false} withVariableButton={false}
/> />
<Input <TextInput
label="From name" label="From name"
defaultValue={config.from.name ?? ''} defaultValue={config.from.name ?? ''}
onChange={handleFromNameChange} onChange={handleFromNameChange}
placeholder="John Smith" placeholder="John Smith"
withVariableButton={false} withVariableButton={false}
/> />
<Input <TextInput
isRequired isRequired
label="Host" label="Host"
defaultValue={config.host ?? ''} defaultValue={config.host ?? ''}
@@ -50,7 +50,7 @@ export const SmtpConfigForm = ({ config, onConfigChange }: Props) => {
placeholder="mail.provider.com" placeholder="mail.provider.com"
withVariableButton={false} withVariableButton={false}
/> />
<Input <TextInput
isRequired isRequired
label="Username / Email" label="Username / Email"
type="email" type="email"
@@ -59,7 +59,7 @@ export const SmtpConfigForm = ({ config, onConfigChange }: Props) => {
placeholder="user@provider.com" placeholder="user@provider.com"
withVariableButton={false} withVariableButton={false}
/> />
<Input <TextInput
isRequired isRequired
label="Password" label="Password"
type="password" type="password"
@@ -73,7 +73,7 @@ export const SmtpConfigForm = ({ config, onConfigChange }: Props) => {
onCheckChange={handleTlsCheck} onCheckChange={handleTlsCheck}
moreInfoContent="If enabled, the connection will use TLS when connecting to server. If disabled then TLS is used if server supports the STARTTLS extension. In most cases enable it if you are connecting to port 465. For port 587 or 25 keep it disabled." moreInfoContent="If enabled, the connection will use TLS when connecting to server. If disabled then TLS is used if server supports the STARTTLS extension. In most cases enable it if you are connecting to port 465. For port 587 or 25 keep it disabled."
/> />
<SmartNumberInput <NumberInput
isRequired isRequired
label="Port number:" label="Port number:"
placeholder="25" placeholder="25"

View File

@@ -58,9 +58,9 @@ test.describe('Send email block', () => {
'[placeholder="email1@gmail.com, email2@gmail.com"]', '[placeholder="email1@gmail.com, email2@gmail.com"]',
'email1@gmail.com, email2@gmail.com' 'email1@gmail.com, email2@gmail.com'
) )
await page.fill('[data-testid="subject-input"]', 'Email subject') await page.getByLabel('Subject:').fill('Email subject')
await page.click('text="Custom content?"') await page.click('text="Custom content?"')
await page.fill('[data-testid="body-input"]', 'Here is my email') await page.locator('textarea').fill('Here is my email')
await page.click('text=Preview') await page.click('text=Preview')
await page.locator('typebot-standard').locator('text=Go').click() await page.locator('typebot-standard').locator('text=Go').click()

View File

@@ -1,6 +1,6 @@
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { TableListItemProps } from '@/components/TableList' import { TableListItemProps } from '@/components/TableList'
import { Stack, FormControl, FormLabel } from '@chakra-ui/react' import { Stack } from '@chakra-ui/react'
import { KeyValue } from 'models' import { KeyValue } from 'models'
export const QueryParamsInputs = (props: TableListItemProps<KeyValue>) => ( export const QueryParamsInputs = (props: TableListItemProps<KeyValue>) => (
@@ -39,26 +39,20 @@ export const KeyValueInputs = ({
} }
return ( return (
<Stack p="4" rounded="md" flex="1" borderWidth="1px"> <Stack p="4" rounded="md" flex="1" borderWidth="1px">
<FormControl> <TextInput
<FormLabel htmlFor={'key' + item.id}>Key:</FormLabel> label="Key:"
<Input defaultValue={item.key ?? ''}
id={'key' + item.id} onChange={handleKeyChange}
defaultValue={item.key ?? ''} placeholder={keyPlaceholder}
onChange={handleKeyChange} debounceTimeout={debounceTimeout}
placeholder={keyPlaceholder} />
debounceTimeout={debounceTimeout} <TextInput
/> label="Value:"
</FormControl> defaultValue={item.value ?? ''}
<FormControl> onChange={handleValueChange}
<FormLabel htmlFor={'value' + item.id}>Value:</FormLabel> placeholder={valuePlaceholder}
<Input debounceTimeout={debounceTimeout}
id={'value' + item.id} />
defaultValue={item.value ?? ''}
onChange={handleValueChange}
placeholder={valuePlaceholder}
debounceTimeout={debounceTimeout}
/>
</FormControl>
</Stack> </Stack>
) )
} }

View File

@@ -1,6 +1,6 @@
import { SearchableDropdown } from '@/components/SearchableDropdown' import { AutocompleteInput } from '@/components/inputs/AutocompleteInput'
import { TableListItemProps } from '@/components/TableList' import { TableListItemProps } from '@/components/TableList'
import { VariableSearchInput } from '@/components/VariableSearchInput' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { Stack, FormControl, FormLabel } from '@chakra-ui/react' import { Stack, FormControl, FormLabel } from '@chakra-ui/react'
import { Variable, ResponseVariableMapping } from 'models' import { Variable, ResponseVariableMapping } from 'models'
@@ -18,10 +18,10 @@ export const DataVariableInputs = ({
<Stack p="4" rounded="md" flex="1" borderWidth="1px"> <Stack p="4" rounded="md" flex="1" borderWidth="1px">
<FormControl> <FormControl>
<FormLabel htmlFor="name">Data:</FormLabel> <FormLabel htmlFor="name">Data:</FormLabel>
<SearchableDropdown <AutocompleteInput
items={dataItems} items={dataItems}
value={item.bodyPath} defaultValue={item.bodyPath}
onValueChange={handleBodyPathChange} onChange={handleBodyPathChange}
placeholder="Select the data" placeholder="Select the data"
withVariableButton withVariableButton
/> />

View File

@@ -1,6 +1,6 @@
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { TableListItemProps } from '@/components/TableList' import { TableListItemProps } from '@/components/TableList'
import { VariableSearchInput } from '@/components/VariableSearchInput' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { Stack, FormControl, FormLabel } from '@chakra-ui/react' import { Stack, FormControl, FormLabel } from '@chakra-ui/react'
import { VariableForTest, Variable } from 'models' import { VariableForTest, Variable } from 'models'
@@ -25,15 +25,12 @@ export const VariableForTestInputs = ({
onSelectVariable={handleVariableSelect} onSelectVariable={handleVariableSelect}
/> />
</FormControl> </FormControl>
<FormControl> <TextInput
<FormLabel htmlFor={'value' + item.id}>Test value:</FormLabel> label="Test value:"
<Input defaultValue={item.value ?? ''}
id={'value' + item.id} onChange={handleValueChange}
defaultValue={item.value ?? ''} debounceTimeout={debounceTimeout}
onChange={handleValueChange} />
debounceTimeout={debounceTimeout}
/>
</FormControl>
</Stack> </Stack>
) )
} }

View File

@@ -27,18 +27,18 @@ import {
Webhook, Webhook,
} from 'models' } from 'models'
import { DropdownList } from '@/components/DropdownList' import { DropdownList } from '@/components/DropdownList'
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { HeadersInputs, QueryParamsInputs } from './KeyValueInputs' import { HeadersInputs, QueryParamsInputs } from './KeyValueInputs'
import { VariableForTestInputs } from './VariableForTestInputs' import { VariableForTestInputs } from './VariableForTestInputs'
import { DataVariableInputs } from './ResponseMappingInputs' import { DataVariableInputs } from './ResponseMappingInputs'
import { byId, env } from 'utils' import { byId, env } from 'utils'
import { ExternalLinkIcon } from '@/components/icons' import { ExternalLinkIcon } from '@/components/icons'
import { useToast } from '@/hooks/useToast' import { useToast } from '@/hooks/useToast'
import { SwitchWithLabel } from '@/components/SwitchWithLabel' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { TableListItemProps, TableList } from '@/components/TableList' import { TableListItemProps, TableList } from '@/components/TableList'
import { executeWebhook } from '../../queries/executeWebhookQuery' import { executeWebhook } from '../../queries/executeWebhookQuery'
import { getDeepKeys } from '../../utils/getDeepKeys' import { getDeepKeys } from '../../utils/getDeepKeys'
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { convertVariablesForTestToVariables } from '../../utils/convertVariablesForTestToVariables' import { convertVariablesForTestToVariables } from '../../utils/convertVariablesForTestToVariables'
import { useDebouncedCallback } from 'use-debounce' import { useDebouncedCallback } from 'use-debounce'
@@ -157,7 +157,7 @@ export const WebhookSettings = ({
</Stack> </Stack>
</Alert> </Alert>
)} )}
<Input <TextInput
placeholder="Paste webhook URL..." placeholder="Paste webhook URL..."
defaultValue={localWebhook.url ?? ''} defaultValue={localWebhook.url ?? ''}
onChange={handleUrlChange} onChange={handleUrlChange}

View File

@@ -2,8 +2,8 @@ import { Stack } from '@chakra-ui/react'
import { DropdownList } from '@/components/DropdownList' import { DropdownList } from '@/components/DropdownList'
import { Comparison, Variable, ComparisonOperators } from 'models' import { Comparison, Variable, ComparisonOperators } from 'models'
import { TableListItemProps } from '@/components/TableList' import { TableListItemProps } from '@/components/TableList'
import { VariableSearchInput } from '@/components/VariableSearchInput' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
export const ComparisonItem = ({ export const ComparisonItem = ({
item, item,
@@ -39,7 +39,7 @@ export const ComparisonItem = ({
placeholder="Select an operator" placeholder="Select an operator"
/> />
{item.comparisonOperator !== ComparisonOperators.IS_SET && ( {item.comparisonOperator !== ComparisonOperators.IS_SET && (
<Input <TextInput
defaultValue={item.value ?? ''} defaultValue={item.value ?? ''}
onChange={handleChangeValue} onChange={handleChangeValue}
placeholder="Type a value..." placeholder="Type a value..."

View File

@@ -1,6 +1,6 @@
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { SwitchWithLabel } from '@/components/SwitchWithLabel' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { FormLabel, Stack } from '@chakra-ui/react' import { Stack } from '@chakra-ui/react'
import { RedirectOptions } from 'models' import { RedirectOptions } from 'models'
import React from 'react' import React from 'react'
@@ -17,17 +17,12 @@ export const RedirectSettings = ({ options, onOptionsChange }: Props) => {
return ( return (
<Stack spacing={4}> <Stack spacing={4}>
<Stack> <TextInput
<FormLabel mb="0" htmlFor="tracking-id"> label="Url:"
Url: defaultValue={options.url ?? ''}
</FormLabel> placeholder="Type a URL..."
<Input onChange={handleUrlChange}
id="tracking-id" />
defaultValue={options.url ?? ''}
placeholder="Type a URL..."
onChange={handleUrlChange}
/>
</Stack>
<SwitchWithLabel <SwitchWithLabel
label="Open in new tab?" label="Open in new tab?"
initialValue={options.isNewTab} initialValue={options.isNewTab}

View File

@@ -1,7 +1,7 @@
import { FormLabel, Stack, Text } from '@chakra-ui/react' import { Stack, Text } from '@chakra-ui/react'
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import React from 'react' import React from 'react'
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { ScriptOptions } from 'models' import { ScriptOptions } from 'models'
type Props = { type Props = {
@@ -17,17 +17,12 @@ export const ScriptSettings = ({ options, onOptionsChange }: Props) => {
return ( return (
<Stack spacing={4}> <Stack spacing={4}>
<Stack> <TextInput
<FormLabel mb="0" htmlFor="name"> label="Name:"
Name: defaultValue={options.name}
</FormLabel> onChange={handleNameChange}
<Input withVariableButton={false}
id="name" />
defaultValue={options.name}
onChange={handleNameChange}
withVariableButton={false}
/>
</Stack>
<Stack> <Stack>
<Text>Code:</Text> <Text>Code:</Text>
<CodeEditor <CodeEditor

View File

@@ -1,8 +1,8 @@
import { FormLabel, HStack, Stack, Switch, Text } from '@chakra-ui/react' import { FormLabel, HStack, Stack, Switch, Text } from '@chakra-ui/react'
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { SetVariableOptions, Variable } from 'models' import { SetVariableOptions, Variable } from 'models'
import React from 'react' import React from 'react'
import { VariableSearchInput } from '@/components/VariableSearchInput' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { Textarea } from '@/components/inputs' import { Textarea } from '@/components/inputs'
type Props = { type Props = {

View File

@@ -1,8 +1,6 @@
import { SearchableDropdown } from '@/components/SearchableDropdown' import { Select } from '@/components/inputs/Select'
import { Input } from '@chakra-ui/react' import { Input } from '@chakra-ui/react'
import { Group } from 'models' import { Group } from 'models'
import { useMemo } from 'react'
import { byId } from 'utils'
type Props = { type Props = {
groups: Group[] groups: Group[]
@@ -17,24 +15,17 @@ export const GroupsDropdown = ({
onGroupIdSelected, onGroupIdSelected,
isLoading, isLoading,
}: Props) => { }: Props) => {
const currentGroup = useMemo(
() => groups?.find(byId(groupId)),
[groupId, groups]
)
const handleGroupSelect = (title: string) => {
const id = groups?.find((b) => b.title === title)?.id
if (id) onGroupIdSelected(id)
}
if (isLoading) return <Input value="Loading..." isDisabled /> if (isLoading) return <Input value="Loading..." isDisabled />
if (!groups || groups.length === 0) if (!groups || groups.length === 0)
return <Input value="No groups found" isDisabled /> return <Input value="No groups found" isDisabled />
return ( return (
<SearchableDropdown <Select
selectedItem={currentGroup?.title} selectedItem={groupId}
items={(groups ?? []).map((b) => b.title)} items={(groups ?? []).map((group) => ({
onValueChange={handleGroupSelect} label: group.title,
value: group.id,
}))}
onSelect={onGroupIdSelected}
placeholder={'Select a block'} placeholder={'Select a block'}
/> />
) )

View File

@@ -24,7 +24,7 @@ export const TypebotLinkForm = ({ options, onOptionsChange }: Props) => {
<TypebotsDropdown <TypebotsDropdown
idsToExclude={[typebot.id]} idsToExclude={[typebot.id]}
typebotId={options.typebotId} typebotId={options.typebotId}
onSelectTypebotId={handleTypebotIdChange} onSelect={handleTypebotIdChange}
currentWorkspaceId={typebot.workspaceId as string} currentWorkspaceId={typebot.workspaceId as string}
/> />
)} )}

View File

@@ -1,25 +1,23 @@
import { HStack, IconButton, Input, Text } from '@chakra-ui/react' import { HStack, IconButton, Input } from '@chakra-ui/react'
import { ExternalLinkIcon } from '@/components/icons' import { ExternalLinkIcon } from '@/components/icons'
import { useToast } from '@/hooks/useToast' import { useToast } from '@/hooks/useToast'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useMemo } from 'react'
import { byId } from 'utils'
import { useTypebots } from '@/features/dashboard' import { useTypebots } from '@/features/dashboard'
import { SearchableDropdown } from '@/components/SearchableDropdown' import { Select } from '@/components/inputs/Select'
import { EmojiOrImageIcon } from '@/components/EmojiOrImageIcon' import { EmojiOrImageIcon } from '@/components/EmojiOrImageIcon'
type Props = { type Props = {
idsToExclude: string[] idsToExclude: string[]
typebotId?: string | 'current' typebotId?: string | 'current'
currentWorkspaceId: string currentWorkspaceId: string
onSelectTypebotId: (typebotId: string | 'current') => void onSelect: (typebotId: string | 'current') => void
} }
export const TypebotsDropdown = ({ export const TypebotsDropdown = ({
idsToExclude, idsToExclude,
typebotId, typebotId,
onSelectTypebotId, onSelect,
currentWorkspaceId, currentWorkspaceId,
}: Props) => { }: Props) => {
const { query } = useRouter() const { query } = useRouter()
@@ -28,56 +26,42 @@ export const TypebotsDropdown = ({
workspaceId: currentWorkspaceId, workspaceId: currentWorkspaceId,
onError: (e) => showToast({ title: e.name, description: e.message }), onError: (e) => showToast({ title: e.name, description: e.message }),
}) })
const currentTypebot = useMemo(
() => typebots?.find(byId(typebotId)),
[typebotId, typebots]
)
const handleTypebotSelect = (name: string) => {
if (name === 'Current typebot') return onSelectTypebotId('current')
const id = typebots?.find((s) => s.name === name)?.id
if (id) onSelectTypebotId(id)
}
if (isLoading) return <Input value="Loading..." isDisabled /> if (isLoading) return <Input value="Loading..." isDisabled />
if (!typebots || typebots.length === 0) if (!typebots || typebots.length === 0)
return <Input value="No typebots found" isDisabled /> return <Input value="No typebots found" isDisabled />
return ( return (
<HStack> <HStack>
<SearchableDropdown <Select
selectedItem={ selectedItem={typebotId}
typebotId === 'current' ? 'Current typebot' : currentTypebot?.name
}
items={[ items={[
{ {
label: 'Current typebot', label: 'Current typebot',
value: 'Current typebot', value: 'current',
}, },
...(typebots ?? []) ...(typebots ?? [])
.filter((typebot) => !idsToExclude.includes(typebot.id)) .filter((typebot) => !idsToExclude.includes(typebot.id))
.map((typebot) => ({ .map((typebot) => ({
value: typebot.name, icon: (
label: ( <EmojiOrImageIcon
<HStack as="span" spacing="2"> icon={typebot.icon}
<EmojiOrImageIcon boxSize="18px"
icon={typebot.icon} emojiFontSize="18px"
boxSize="18px" />
emojiFontSize="18px"
/>
<Text>{typebot.name}</Text>
</HStack>
), ),
label: typebot.name,
value: typebot.id,
})), })),
]} ]}
onValueChange={handleTypebotSelect} onSelect={onSelect}
placeholder={'Select a typebot'} placeholder={'Select a typebot'}
/> />
{currentTypebot?.id && ( {typebotId && typebotId !== 'current' && (
<IconButton <IconButton
aria-label="Navigate to typebot" aria-label="Navigate to typebot"
icon={<ExternalLinkIcon />} icon={<ExternalLinkIcon />}
as={Link} as={Link}
href={`/typebots/${currentTypebot?.id}/edit?parentId=${query.typebotId}`} href={`/typebots/${typebotId}/edit?parentId=${query.typebotId}`}
/> />
)} )}
</HStack> </HStack>

View File

@@ -29,7 +29,9 @@ test('should be configurable', async ({ page }) => {
await page.click('[aria-label="Navigate back"]') await page.click('[aria-label="Navigate back"]')
await expect(page).toHaveURL(`/typebots/${typebotId}/edit`) await expect(page).toHaveURL(`/typebots/${typebotId}/edit`)
await page.click('text=Jump in My link typebot 2') await page.click('text=Jump in My link typebot 2')
await expect(page.locator('input[value="My link typebot 2"]')).toBeVisible() await expect(page.getByTestId('selected-item-label').first()).toHaveText(
'My link typebot 2'
)
await page.click('input[placeholder="Select a block"]') await page.click('input[placeholder="Select a block"]')
await page.click('text=Group #2') await page.click('text=Group #2')
@@ -40,8 +42,7 @@ test('should be configurable', async ({ page }) => {
await page.click('[aria-label="Close"]') await page.click('[aria-label="Close"]')
await page.click('text=Jump to Group #2 in My link typebot 2') await page.click('text=Jump to Group #2 in My link typebot 2')
await page.click('input[value="Group #2"]', { clickCount: 3 }) await page.getByTestId('selected-item-label').nth(1).click({ force: true })
await page.press('input[value="Group #2"]', 'Backspace')
await page.click('button >> text=Start') await page.click('button >> text=Start')
await page.click('text=Preview') await page.click('text=Preview')
@@ -54,13 +55,9 @@ test('should be configurable', async ({ page }) => {
await page.click('[aria-label="Close"]') await page.click('[aria-label="Close"]')
await page.click('text=Jump to Start in My link typebot 2') await page.click('text=Jump to Start in My link typebot 2')
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
await page.click('input[value="My link typebot 2"]', { clickCount: 3 }) await page.getByTestId('selected-item-label').first().click({ force: true })
await page.press('input[value="My link typebot 2"]', 'Backspace')
await page.click('button >> text=Current typebot') await page.click('button >> text=Current typebot')
await page.click('input[placeholder="Select a block"]', { await page.getByPlaceholder('Select a block').click()
clickCount: 3,
})
await page.press('input[placeholder="Select a block"]', 'Backspace')
await page.click('button >> text=Hello') await page.click('button >> text=Hello')
await page.click('text=Preview') await page.click('text=Preview')

View File

@@ -1,7 +1,7 @@
import { Stack } from '@chakra-ui/react' import { Stack } from '@chakra-ui/react'
import { WaitOptions } from 'models' import { WaitOptions } from 'models'
import React from 'react' import React from 'react'
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
type Props = { type Props = {
options: WaitOptions options: WaitOptions
@@ -15,7 +15,7 @@ export const WaitSettings = ({ options, onOptionsChange }: Props) => {
return ( return (
<Stack spacing={4}> <Stack spacing={4}>
<Input <TextInput
label="Seconds to wait for:" label="Seconds to wait for:"
defaultValue={options.secondsToWaitFor} defaultValue={options.secondsToWaitFor}
onChange={handleSecondsChange} onChange={handleSecondsChange}

View File

@@ -36,8 +36,6 @@ export const EditableTypebotName = ({
cursor="pointer" cursor="pointer"
maxW="150px" maxW="150px"
overflow="hidden" overflow="hidden"
display="flex"
alignItems="center"
fontSize="14px" fontSize="14px"
minW="30px" minW="30px"
minH="20px" minH="20px"

View File

@@ -1,4 +1,4 @@
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { TextLink } from '@/components/TextLink' import { TextLink } from '@/components/TextLink'
import { useEditor } from '@/features/editor/providers/EditorProvider' import { useEditor } from '@/features/editor/providers/EditorProvider'
import { useTypebot } from '@/features/editor/providers/TypebotProvider' import { useTypebot } from '@/features/editor/providers/TypebotProvider'

View File

@@ -1,5 +1,5 @@
import { AlertInfo } from '@/components/AlertInfo' import { AlertInfo } from '@/components/AlertInfo'
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { TextLink } from '@/components/TextLink' import { TextLink } from '@/components/TextLink'
import { import {
Modal, Modal,

View File

@@ -1,4 +1,4 @@
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { OrderedList, ListItem, Code, Stack, Text } from '@chakra-ui/react' import { OrderedList, ListItem, Code, Stack, Text } from '@chakra-ui/react'
import { Typebot } from 'models' import { Typebot } from 'models'
import { useState } from 'react' import { useState } from 'react'

View File

@@ -1,7 +1,7 @@
import { FlexProps } from '@chakra-ui/react' import { FlexProps } from '@chakra-ui/react'
import { useTypebot } from '@/features/editor' import { useTypebot } from '@/features/editor'
import { env, getViewerUrl } from 'utils' import { env, getViewerUrl } from 'utils'
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import prettier from 'prettier/standalone' import prettier from 'prettier/standalone'
import parserHtml from 'prettier/parser-html' import parserHtml from 'prettier/parser-html'

View File

@@ -2,7 +2,7 @@ import prettier from 'prettier/standalone'
import parserHtml from 'prettier/parser-html' import parserHtml from 'prettier/parser-html'
import { parseInitBubbleCode, typebotImportCode } from '../../snippetParsers' import { parseInitBubbleCode, typebotImportCode } from '../../snippetParsers'
import { useTypebot } from '@/features/editor' import { useTypebot } from '@/features/editor'
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { BubbleProps } from '@typebot.io/js' import { BubbleProps } from '@typebot.io/js'
import { isCloudProdInstance } from '@/utils/helpers' import { isCloudProdInstance } from '@/utils/helpers'
import { env, getViewerUrl } from 'utils' import { env, getViewerUrl } from 'utils'

View File

@@ -2,7 +2,7 @@ import { useTypebot } from '@/features/editor'
import parserHtml from 'prettier/parser-html' import parserHtml from 'prettier/parser-html'
import prettier from 'prettier/standalone' import prettier from 'prettier/standalone'
import { parseInitPopupCode, typebotImportCode } from '../../snippetParsers' import { parseInitPopupCode, typebotImportCode } from '../../snippetParsers'
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { PopupProps } from '@typebot.io/js' import { PopupProps } from '@typebot.io/js'
import { isCloudProdInstance } from '@/utils/helpers' import { isCloudProdInstance } from '@/utils/helpers'
import { env, getViewerUrl } from 'utils' import { env, getViewerUrl } from 'utils'

View File

@@ -2,7 +2,7 @@ import parserHtml from 'prettier/parser-html'
import prettier from 'prettier/standalone' import prettier from 'prettier/standalone'
import { parseInitStandardCode, typebotImportCode } from '../../snippetParsers' import { parseInitStandardCode, typebotImportCode } from '../../snippetParsers'
import { useTypebot } from '@/features/editor' import { useTypebot } from '@/features/editor'
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { isCloudProdInstance } from '@/utils/helpers' import { isCloudProdInstance } from '@/utils/helpers'
import { env, getViewerUrl } from 'utils' import { env, getViewerUrl } from 'utils'

View File

@@ -1,4 +1,4 @@
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
export const InstallReactPackageSnippet = () => { export const InstallReactPackageSnippet = () => {
return ( return (

View File

@@ -1,4 +1,4 @@
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { useTypebot } from '@/features/editor' import { useTypebot } from '@/features/editor'
import { BubbleProps } from '@typebot.io/js' import { BubbleProps } from '@typebot.io/js'
import parserBabel from 'prettier/parser-babel' import parserBabel from 'prettier/parser-babel'

View File

@@ -1,4 +1,4 @@
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { useTypebot } from '@/features/editor' import { useTypebot } from '@/features/editor'
import { PopupProps } from '@typebot.io/js' import { PopupProps } from '@typebot.io/js'
import parserBabel from 'prettier/parser-babel' import parserBabel from 'prettier/parser-babel'

View File

@@ -1,4 +1,4 @@
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { useTypebot } from '@/features/editor' import { useTypebot } from '@/features/editor'
import parserBabel from 'prettier/parser-babel' import parserBabel from 'prettier/parser-babel'
import prettier from 'prettier/standalone' import prettier from 'prettier/standalone'

View File

@@ -1,4 +1,4 @@
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { useTypebot } from '@/features/editor' import { useTypebot } from '@/features/editor'
import { isCloudProdInstance } from '@/utils/helpers' import { isCloudProdInstance } from '@/utils/helpers'
import { Stack, Text } from '@chakra-ui/react' import { Stack, Text } from '@chakra-ui/react'

View File

@@ -1,4 +1,4 @@
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { useTypebot } from '@/features/editor' import { useTypebot } from '@/features/editor'
import { isCloudProdInstance } from '@/utils/helpers' import { isCloudProdInstance } from '@/utils/helpers'
import { Stack, Text } from '@chakra-ui/react' import { Stack, Text } from '@chakra-ui/react'

View File

@@ -1,4 +1,4 @@
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { useTypebot } from '@/features/editor' import { useTypebot } from '@/features/editor'
import { isCloudProdInstance } from '@/utils/helpers' import { isCloudProdInstance } from '@/utils/helpers'
import { Stack, Code, Text } from '@chakra-ui/react' import { Stack, Code, Text } from '@chakra-ui/react'

View File

@@ -1,4 +1,4 @@
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { OrderedList, ListItem, Stack, Text, Code } from '@chakra-ui/react' import { OrderedList, ListItem, Stack, Text, Code } from '@chakra-ui/react'
import { useState } from 'react' import { useState } from 'react'
import { StandardSettings } from '../../../settings/StandardSettings' import { StandardSettings } from '../../../settings/StandardSettings'

View File

@@ -1,4 +1,4 @@
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { ExternalLinkIcon } from '@/components/icons' import { ExternalLinkIcon } from '@/components/icons'
import { useTypebot } from '@/features/editor' import { useTypebot } from '@/features/editor'
import { import {

View File

@@ -1,4 +1,4 @@
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { ExternalLinkIcon } from '@/components/icons' import { ExternalLinkIcon } from '@/components/icons'
import { import {
OrderedList, OrderedList,

View File

@@ -1,4 +1,4 @@
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { ExternalLinkIcon } from '@/components/icons' import { ExternalLinkIcon } from '@/components/icons'
import { import {
OrderedList, OrderedList,

View File

@@ -1,4 +1,4 @@
import { SmartNumberInput } from '@/components/inputs' import { NumberInput } from '@/components/inputs'
import { FormLabel, HStack, Input, Stack, Switch, Text } from '@chakra-ui/react' import { FormLabel, HStack, Input, Stack, Switch, Text } from '@chakra-ui/react'
import { PreviewMessageParams } from '@typebot.io/js/dist/features/bubble/types' import { PreviewMessageParams } from '@typebot.io/js/dist/features/bubble/types'
import { useState } from 'react' import { useState } from 'react'
@@ -101,7 +101,7 @@ export const PreviewMessageSettings = ({ defaultAvatar, onChange }: Props) => {
{isAutoShowEnabled && ( {isAutoShowEnabled && (
<> <>
<Text>After</Text> <Text>After</Text>
<SmartNumberInput <NumberInput
size="sm" size="sm"
w="70px" w="70px"
defaultValue={autoShowDelay} defaultValue={autoShowDelay}

View File

@@ -1,4 +1,4 @@
import { SmartNumberInput } from '@/components/inputs' import { NumberInput } from '@/components/inputs'
import { import {
StackProps, StackProps,
Stack, Stack,
@@ -37,7 +37,7 @@ export const PopupSettings = ({ onUpdateSettings, ...props }: Props) => {
/> />
{isEnabled && ( {isEnabled && (
<> <>
<SmartNumberInput <NumberInput
label="After" label="After"
size="sm" size="sm"
w="70px" w="70px"

View File

@@ -9,7 +9,7 @@ import {
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { DropdownList } from '@/components/DropdownList' import { DropdownList } from '@/components/DropdownList'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { SwitchWithLabel } from '@/components/SwitchWithLabel' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
type Props = { type Props = {
onUpdateWindowSettings: (windowSettings: { onUpdateWindowSettings: (windowSettings: {

View File

@@ -1,6 +1,6 @@
import { AlertInfo } from '@/components/AlertInfo' import { AlertInfo } from '@/components/AlertInfo'
import { DownloadIcon } from '@/components/icons' import { DownloadIcon } from '@/components/icons'
import { SwitchWithLabel } from '@/components/SwitchWithLabel' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { useTypebot } from '@/features/editor' import { useTypebot } from '@/features/editor'
import { useToast } from '@/hooks/useToast' import { useToast } from '@/hooks/useToast'
import { trpc } from '@/lib/trpc' import { trpc } from '@/lib/trpc'

View File

@@ -5,7 +5,7 @@ import { GeneralSettings } from 'models'
import React from 'react' import React from 'react'
import { isDefined } from 'utils' import { isDefined } from 'utils'
import { ChangePlanModal, isFreePlan, LimitReached } from '@/features/billing' import { ChangePlanModal, isFreePlan, LimitReached } from '@/features/billing'
import { SwitchWithLabel } from '@/components/SwitchWithLabel' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { LockTag } from '@/features/billing' import { LockTag } from '@/features/billing'
type Props = { type Props = {

View File

@@ -10,10 +10,10 @@ import {
HStack, HStack,
Text, Text,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import { ImageUploadContent } from '@/components/ImageUploadContent' import { ImageUploadContent } from '@/components/ImageUploadContent'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip' import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { Input, Textarea } from '@/components/inputs' import { TextInput, Textarea } from '@/components/inputs'
type Props = { type Props = {
typebotId: string typebotId: string
@@ -94,7 +94,7 @@ export const MetadataForm = ({
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</Stack> </Stack>
<Input <TextInput
label="Title:" label="Title:"
defaultValue={metadata.title ?? typebotName} defaultValue={metadata.title ?? typebotName}
onChange={handleTitleChange} onChange={handleTitleChange}
@@ -104,7 +104,7 @@ export const MetadataForm = ({
onChange={handleDescriptionChange} onChange={handleDescriptionChange}
label="Description:" label="Description:"
/> />
<Input <TextInput
defaultValue={metadata.googleTagManagerId} defaultValue={metadata.googleTagManagerId}
placeholder="GTM-XXXXXX" placeholder="GTM-XXXXXX"
onChange={handleGoogleTagManagerIdChange} onChange={handleGoogleTagManagerIdChange}

View File

@@ -2,7 +2,7 @@ import { Flex, FormLabel, Stack, Switch } from '@chakra-ui/react'
import { TypingEmulation } from 'models' import { TypingEmulation } from 'models'
import React from 'react' import React from 'react'
import { isDefined } from 'utils' import { isDefined } from 'utils'
import { SmartNumberInput } from '@/components/inputs' import { NumberInput } from '@/components/inputs'
type Props = { type Props = {
typingEmulation: TypingEmulation typingEmulation: TypingEmulation
@@ -36,7 +36,7 @@ export const TypingEmulationForm = ({ typingEmulation, onUpdate }: Props) => {
</Flex> </Flex>
{typingEmulation.enabled && ( {typingEmulation.enabled && (
<Stack pl={10}> <Stack pl={10}>
<SmartNumberInput <NumberInput
label="Words per minutes:" label="Words per minutes:"
data-testid="speed" data-testid="speed"
defaultValue={typingEmulation.speed} defaultValue={typingEmulation.speed}
@@ -45,7 +45,7 @@ export const TypingEmulationForm = ({ typingEmulation, onUpdate }: Props) => {
maxW="100px" maxW="100px"
step={30} step={30}
/> />
<SmartNumberInput <NumberInput
label="Max delay (in seconds):" label="Max delay (in seconds):"
data-testid="max-delay" data-testid="max-delay"
defaultValue={typingEmulation.maxDelay} defaultValue={typingEmulation.maxDelay}

View File

@@ -88,11 +88,14 @@ test.describe.parallel('Settings page', () => {
favIconUrl favIconUrl
) )
await expect(favIconImg).toHaveAttribute('src', favIconUrl) await expect(favIconImg).toHaveAttribute('src', favIconUrl)
// Close popover
await page.getByText('Image:').click()
await page.waitForTimeout(1000)
// Website image // Website image
const websiteImg = page.locator('img >> nth=1') const websiteImg = page.locator('img >> nth=1')
await expect(websiteImg).toHaveAttribute('src', '/viewer-preview.png') await expect(websiteImg).toHaveAttribute('src', '/viewer-preview.png')
await websiteImg.click({ position: { x: 0, y: 160 }, force: true }) await websiteImg.click()
await expect(page.locator('text=Giphy')).toBeHidden() await expect(page.locator('text=Giphy')).toBeHidden()
await page.click('button >> text="Embed link"') await page.click('button >> text="Embed link"')
await page.fill('input[placeholder="Paste the image link..."]', imageUrl) await page.fill('input[placeholder="Paste the image link..."]', imageUrl)

View File

@@ -1,4 +1,4 @@
import { CodeEditor } from '@/components/CodeEditor' import { CodeEditor } from '@/components/inputs/CodeEditor'
import React from 'react' import React from 'react'
type Props = { type Props = {

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { Text, HStack } from '@chakra-ui/react' import { Text, HStack } from '@chakra-ui/react'
import { SearchableDropdown } from '@/components/SearchableDropdown'
import { env, isEmpty } from 'utils' import { env, isEmpty } from 'utils'
import { AutocompleteInput } from '@/components/inputs/AutocompleteInput'
type FontSelectorProps = { type FontSelectorProps = {
activeFont?: string activeFont?: string
@@ -32,7 +32,7 @@ export const FontSelector = ({
} }
const handleFontSelected = (nextFont: string) => { const handleFontSelected = (nextFont: string) => {
if (nextFont == currentFont) return if (nextFont === currentFont) return
setCurrentFont(nextFont) setCurrentFont(nextFont)
onSelectFont(nextFont) onSelectFont(nextFont)
} }
@@ -40,10 +40,11 @@ export const FontSelector = ({
return ( return (
<HStack justify="space-between" align="center"> <HStack justify="space-between" align="center">
<Text>Font</Text> <Text>Font</Text>
<SearchableDropdown <AutocompleteInput
selectedItem={activeFont} defaultValue={activeFont}
items={googleFonts} items={googleFonts}
onValueChange={handleFontSelected} onChange={handleFontSelected}
withVariableButton={false}
/> />
</HStack> </HStack>
) )

View File

@@ -19,7 +19,7 @@ test.describe.parallel('Theme page', () => {
await expect(page.locator('button >> text="Go"')).toBeVisible() await expect(page.locator('button >> text="Go"')).toBeVisible()
// Font // Font
await page.fill('input[type="text"]', 'Roboto Slab') await page.getByRole('textbox').fill('Roboto Slab')
await expect(page.locator('.typebot-container')).toHaveCSS( await expect(page.locator('.typebot-container')).toHaveCSS(
'font-family', 'font-family',
/"Roboto Slab"/ /"Roboto Slab"/

View File

@@ -7,12 +7,14 @@ import {
IconButtonProps, IconButtonProps,
useDisclosure, useDisclosure,
PopoverAnchor, PopoverAnchor,
Portal,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { UserIcon } from '@/components/icons' import { UserIcon } from '@/components/icons'
import { Variable } from 'models' import { Variable } from 'models'
import React, { useRef } from 'react' import React, { useRef } from 'react'
import { VariableSearchInput } from '@/components/VariableSearchInput' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { useOutsideClick } from '@/hooks/useOutsideClick' import { useOutsideClick } from '@/hooks/useOutsideClick'
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
type Props = { type Props = {
onSelectVariable: (variable: Pick<Variable, 'name' | 'id'>) => void onSelectVariable: (variable: Pick<Variable, 'name' | 'id'>) => void
@@ -21,6 +23,7 @@ type Props = {
export const VariablesButton = ({ onSelectVariable, ...props }: Props) => { export const VariablesButton = ({ onSelectVariable, ...props }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure() const { isOpen, onOpen, onClose } = useDisclosure()
const popoverRef = useRef<HTMLDivElement>(null) const popoverRef = useRef<HTMLDivElement>(null)
const { ref: parentModalRef } = useParentModal()
useOutsideClick({ useOutsideClick({
ref: popoverRef, ref: popoverRef,
@@ -28,7 +31,7 @@ export const VariablesButton = ({ onSelectVariable, ...props }: Props) => {
}) })
return ( return (
<Popover isLazy placement="bottom-end" gutter={0} isOpen={isOpen}> <Popover isLazy isOpen={isOpen}>
<PopoverAnchor> <PopoverAnchor>
<Flex> <Flex>
<Tooltip label="Insert a variable"> <Tooltip label="Insert a variable">
@@ -42,17 +45,19 @@ export const VariablesButton = ({ onSelectVariable, ...props }: Props) => {
</Tooltip> </Tooltip>
</Flex> </Flex>
</PopoverAnchor> </PopoverAnchor>
<PopoverContent w="full" ref={popoverRef}> <Portal containerRef={parentModalRef}>
<VariableSearchInput <PopoverContent w="full" ref={popoverRef}>
onSelectVariable={(variable) => { <VariableSearchInput
onClose() onSelectVariable={(variable) => {
if (variable) onSelectVariable(variable) onClose()
}} if (variable) onSelectVariable(variable)
placeholder="Search for a variable" }}
shadow="lg" placeholder="Search for a variable"
autoFocus shadow="lg"
/> autoFocus
</PopoverContent> />
</PopoverContent>
</Portal>
</Popover> </Popover>
) )
} }

View File

@@ -0,0 +1,23 @@
import { Variable } from 'models'
type Props = {
variable: Variable
text: string
at: number
}
export const injectVariableInText = ({
variable,
text,
at,
}: Props): { text: string; carretPosition: number } => {
const textBeforeCursorPosition = text.substring(0, at)
const textAfterCursorPosition = text.substring(at, text.length)
const newText =
textBeforeCursorPosition + `{{${variable.name}}}` + textAfterCursorPosition
const newCarretPosition = at + `{{${variable.name}}}`.length
return {
text: newText,
carretPosition: newCarretPosition,
}
}

View File

@@ -11,7 +11,7 @@ import { ConfirmModal } from '@/components/ConfirmModal'
import React from 'react' import React from 'react'
import { EditableEmojiOrImageIcon } from '@/components/EditableEmojiOrImageIcon' import { EditableEmojiOrImageIcon } from '@/components/EditableEmojiOrImageIcon'
import { useWorkspace } from '../WorkspaceProvider' import { useWorkspace } from '../WorkspaceProvider'
import { Input } from '@/components/inputs' import { TextInput } from '@/components/inputs'
export const WorkspaceSettingsForm = ({ onClose }: { onClose: () => void }) => { export const WorkspaceSettingsForm = ({ onClose }: { onClose: () => void }) => {
const { workspace, workspaces, updateWorkspace, deleteCurrentWorkspace } = const { workspace, workspaces, updateWorkspace, deleteCurrentWorkspace } =
@@ -46,17 +46,14 @@ export const WorkspaceSettingsForm = ({ onClose }: { onClose: () => void }) => {
)} )}
</Flex> </Flex>
</FormControl> </FormControl>
<FormControl> {workspace && (
<FormLabel htmlFor="name">Name</FormLabel> <TextInput
{workspace && ( label="Name:"
<Input withVariableButton={false}
id="name" defaultValue={workspace?.name}
withVariableButton={false} onChange={handleNameChange}
defaultValue={workspace?.name} />
onChange={handleNameChange} )}
/>
)}
</FormControl>
{workspace && workspaces && workspaces.length > 1 && ( {workspace && workspaces && workspaces.length > 1 && (
<DeleteWorkspaceButton <DeleteWorkspaceButton
onConfirm={handleDeleteClick} onConfirm={handleDeleteClick}

View File

@@ -0,0 +1,12 @@
type Props = {
at: number
input?: HTMLInputElement | HTMLTextAreaElement | null
}
export const focusInput = ({ at, input }: Props) => {
if (!input) return
input.focus()
setTimeout(() => {
input.selectionStart = input.selectionEnd = at
}, 100)
}