feat(inputs): ✨ Add Set variable step
This commit is contained in:
38
apps/builder/components/shared/DebouncedTextarea.tsx
Normal file
38
apps/builder/components/shared/DebouncedTextarea.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Textarea, TextareaProps } from '@chakra-ui/react'
|
||||
import { ChangeEvent, useEffect, useState } from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
|
||||
type Props = Omit<TextareaProps, 'onChange' | 'value'> & {
|
||||
delay: number
|
||||
initialValue: string
|
||||
onChange: (debouncedValue: string) => void
|
||||
}
|
||||
|
||||
export const DebouncedTextarea = ({
|
||||
delay,
|
||||
onChange,
|
||||
initialValue,
|
||||
...props
|
||||
}: Props) => {
|
||||
const [currentValue, setCurrentValue] = useState(initialValue)
|
||||
const [currentValueDebounced] = useDebounce(currentValue, delay)
|
||||
|
||||
useEffect(() => {
|
||||
if (currentValueDebounced === initialValue) return
|
||||
onChange(currentValueDebounced)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentValueDebounced])
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setCurrentValue(e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
{...props}
|
||||
value={currentValue}
|
||||
onChange={handleChange}
|
||||
resize={'vertical'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
111
apps/builder/components/shared/SearchableDropdown.tsx
Normal file
111
apps/builder/components/shared/SearchableDropdown.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
useDisclosure,
|
||||
useOutsideClick,
|
||||
Flex,
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
Input,
|
||||
PopoverContent,
|
||||
Button,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { useState, useRef, useEffect, ChangeEvent } from 'react'
|
||||
|
||||
export const SearchableDropdown = ({
|
||||
selectedItem,
|
||||
items,
|
||||
onSelectItem,
|
||||
}: {
|
||||
selectedItem?: string
|
||||
items: string[]
|
||||
onSelectItem: (value: string) => void
|
||||
}) => {
|
||||
const { onOpen, onClose, isOpen } = useDisclosure()
|
||||
const [inputValue, setInputValue] = useState(selectedItem)
|
||||
const [filteredItems, setFilteredItems] = useState([
|
||||
...items
|
||||
.filter((item) =>
|
||||
item.toLowerCase().includes((selectedItem ?? '').toLowerCase())
|
||||
)
|
||||
.slice(0, 50),
|
||||
])
|
||||
const dropdownRef = useRef(null)
|
||||
const inputRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (filteredItems.length > 0) return
|
||||
setFilteredItems([
|
||||
...items
|
||||
.filter((item) =>
|
||||
item.toLowerCase().includes((selectedItem ?? '').toLowerCase())
|
||||
)
|
||||
.slice(0, 50),
|
||||
])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [items])
|
||||
|
||||
useOutsideClick({
|
||||
ref: dropdownRef,
|
||||
handler: onClose,
|
||||
})
|
||||
|
||||
const onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value)
|
||||
if (e.target.value === '') {
|
||||
setFilteredItems([...items.slice(0, 50)])
|
||||
return
|
||||
}
|
||||
setFilteredItems([
|
||||
...items
|
||||
.filter((item) =>
|
||||
item.toLowerCase().includes((inputValue ?? '').toLowerCase())
|
||||
)
|
||||
.slice(0, 50),
|
||||
])
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex ref={dropdownRef}>
|
||||
<Popover isOpen={isOpen} initialFocusRef={inputRef}>
|
||||
<PopoverTrigger>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={onInputChange}
|
||||
onClick={onOpen}
|
||||
w="300px"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent maxH="35vh" overflowY="scroll" spacing="0" w="300px">
|
||||
{filteredItems.length > 0 ? (
|
||||
<>
|
||||
{filteredItems.map((item, idx) => {
|
||||
return (
|
||||
<Button
|
||||
minH="40px"
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
setInputValue(item)
|
||||
onSelectItem(item)
|
||||
onClose()
|
||||
}}
|
||||
fontSize="16px"
|
||||
fontWeight="normal"
|
||||
rounded="none"
|
||||
colorScheme="gray"
|
||||
variant="ghost"
|
||||
justifyContent="flex-start"
|
||||
>
|
||||
{item}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<Text p={4}>Not found.</Text>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
170
apps/builder/components/shared/VariableSearchInput.tsx
Normal file
170
apps/builder/components/shared/VariableSearchInput.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import {
|
||||
useDisclosure,
|
||||
useOutsideClick,
|
||||
Flex,
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
Input,
|
||||
PopoverContent,
|
||||
Button,
|
||||
InputProps,
|
||||
IconButton,
|
||||
} from '@chakra-ui/react'
|
||||
import { PlusIcon, TrashIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { Variable } from 'models'
|
||||
import React, { useState, useRef, ChangeEvent, useMemo, useEffect } from 'react'
|
||||
import { generate } from 'short-uuid'
|
||||
import { isDefined } from 'utils'
|
||||
|
||||
type Props = {
|
||||
initialVariableId?: string
|
||||
onSelectVariable: (variable: Pick<Variable, 'id' | 'name'>) => void
|
||||
isDefaultOpen?: boolean
|
||||
} & InputProps
|
||||
|
||||
export const VariableSearchInput = ({
|
||||
initialVariableId,
|
||||
onSelectVariable,
|
||||
isDefaultOpen,
|
||||
...inputProps
|
||||
}: Props) => {
|
||||
const { onOpen, onClose, isOpen } = useDisclosure()
|
||||
const { typebot, createVariable, deleteVariable } = useTypebot()
|
||||
const variables = useMemo(
|
||||
() =>
|
||||
typebot?.variables.allIds.map((id) => typebot.variables.byId[id]) ?? [],
|
||||
[typebot?.variables]
|
||||
)
|
||||
const [inputValue, setInputValue] = useState(
|
||||
typebot?.variables.byId[initialVariableId ?? '']?.name ?? ''
|
||||
)
|
||||
const [filteredItems, setFilteredItems] = useState<Variable[]>(variables)
|
||||
const dropdownRef = useRef(null)
|
||||
const inputRef = useRef(null)
|
||||
|
||||
useOutsideClick({
|
||||
ref: dropdownRef,
|
||||
handler: onClose,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefaultOpen) onOpen()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value)
|
||||
onOpen()
|
||||
if (e.target.value === '') {
|
||||
setFilteredItems([...variables.slice(0, 50)])
|
||||
return
|
||||
}
|
||||
setFilteredItems([
|
||||
...variables
|
||||
.filter((item) =>
|
||||
item.name.toLowerCase().includes((e.target.value ?? '').toLowerCase())
|
||||
)
|
||||
.slice(0, 50),
|
||||
])
|
||||
}
|
||||
|
||||
const handleVariableNameClick = (variable: Variable) => () => {
|
||||
setInputValue(variable.name)
|
||||
onSelectVariable(variable)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleCreateNewVariableClick = () => {
|
||||
if (!inputValue || inputValue === '') return
|
||||
const id = generate()
|
||||
createVariable({ id, name: inputValue })
|
||||
onSelectVariable({ id, name: inputValue })
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleDeleteVariableClick =
|
||||
(variable: Variable) => (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
deleteVariable(variable.id)
|
||||
setFilteredItems(filteredItems.filter((item) => item.id !== variable.id))
|
||||
if (variable.name === inputValue) setInputValue('')
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex ref={dropdownRef} w="full">
|
||||
<Popover
|
||||
isOpen={isOpen}
|
||||
initialFocusRef={inputRef}
|
||||
matchWidth
|
||||
offset={[0, 0]}
|
||||
isLazy
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<Input
|
||||
data-testid="variables-input"
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={onInputChange}
|
||||
onClick={onOpen}
|
||||
{...inputProps}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
maxH="35vh"
|
||||
overflowY="scroll"
|
||||
spacing="0"
|
||||
role="menu"
|
||||
w="inherit"
|
||||
shadow="lg"
|
||||
>
|
||||
{(inputValue?.length ?? 0) > 0 &&
|
||||
!isDefined(variables.find((v) => v.name === inputValue)) && (
|
||||
<Button
|
||||
role="menuitem"
|
||||
minH="40px"
|
||||
onClick={handleCreateNewVariableClick}
|
||||
fontSize="16px"
|
||||
fontWeight="normal"
|
||||
rounded="none"
|
||||
colorScheme="gray"
|
||||
variant="ghost"
|
||||
justifyContent="flex-start"
|
||||
leftIcon={<PlusIcon />}
|
||||
>
|
||||
Create "{inputValue}"
|
||||
</Button>
|
||||
)}
|
||||
{filteredItems.length > 0 && (
|
||||
<>
|
||||
{filteredItems.map((item, idx) => {
|
||||
return (
|
||||
<Button
|
||||
role="menuitem"
|
||||
minH="40px"
|
||||
key={idx}
|
||||
onClick={handleVariableNameClick(item)}
|
||||
fontSize="16px"
|
||||
fontWeight="normal"
|
||||
rounded="none"
|
||||
colorScheme="gray"
|
||||
variant="ghost"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
{item.name}
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove variable"
|
||||
size="xs"
|
||||
onClick={handleDeleteVariableClick(item)}
|
||||
/>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user