🚸 Add a better select input
Also improves other inputs behavior
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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 ?? ''}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
230
apps/builder/src/components/inputs/AutocompleteInput.tsx
Normal file
230
apps/builder/src/components/inputs/AutocompleteInput.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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} />
|
|
||||||
)
|
|
||||||
@@ -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 (
|
||||||
227
apps/builder/src/components/inputs/Select.tsx
Normal file
227
apps/builder/src/components/inputs/Select.tsx
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
131
apps/builder/src/components/inputs/TextInput.tsx
Normal file
131
apps/builder/src/components/inputs/TextInput.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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..."
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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..."
|
||||||
|
|||||||
@@ -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'}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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'}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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..."
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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'}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CodeEditor } from '@/components/CodeEditor'
|
import { CodeEditor } from '@/components/inputs/CodeEditor'
|
||||||
|
|
||||||
export const InstallReactPackageSnippet = () => {
|
export const InstallReactPackageSnippet = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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"/
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
|||||||
12
apps/builder/src/utils/focusInput.ts
Normal file
12
apps/builder/src/utils/focusInput.ts
Normal 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user