173
apps/builder/src/components/TagsInput.tsx
Normal file
173
apps/builder/src/components/TagsInput.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
import {
|
||||
HStack,
|
||||
IconButton,
|
||||
Wrap,
|
||||
Text,
|
||||
WrapItem,
|
||||
Input,
|
||||
} from '@chakra-ui/react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { CloseIcon } from './icons'
|
||||
import { colors } from '@/lib/theme'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { convertStrToList } from '@typebot.io/lib/convertStrToList'
|
||||
import { isEmpty } from '@typebot.io/lib/utils'
|
||||
|
||||
type Props = {
|
||||
items?: string[]
|
||||
placeholder?: string
|
||||
onChange: (value: string[]) => void
|
||||
}
|
||||
export const TagsInput = ({ items, placeholder, onChange }: Props) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [isFocused, setIsFocused] = useState(false)
|
||||
const [focusedTagIndex, setFocusedTagIndex] = useState<number>()
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
e.preventDefault()
|
||||
setFocusedTagIndex(undefined)
|
||||
setInputValue(e.target.value)
|
||||
if (e.target.value.length - inputValue.length > 0) {
|
||||
const values = convertStrToList(e.target.value)
|
||||
if (values.length > 1) {
|
||||
onChange([...(items ?? []), ...convertStrToList(e.target.value)])
|
||||
setInputValue('')
|
||||
}
|
||||
}
|
||||
}
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (!items) return
|
||||
|
||||
if (e.key === 'Backspace') {
|
||||
if (focusedTagIndex !== undefined) {
|
||||
if (focusedTagIndex === items.length - 1) {
|
||||
setFocusedTagIndex((idx) => idx! - 1)
|
||||
}
|
||||
removeItem(focusedTagIndex)
|
||||
return
|
||||
}
|
||||
if (inputValue === '' && focusedTagIndex === undefined) {
|
||||
setFocusedTagIndex(items?.length - 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowLeft') {
|
||||
if (focusedTagIndex !== undefined) {
|
||||
if (focusedTagIndex === 0) return
|
||||
setFocusedTagIndex(focusedTagIndex - 1)
|
||||
return
|
||||
}
|
||||
if (inputRef.current?.selectionStart === 0 && items) {
|
||||
setFocusedTagIndex(items.length - 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (e.key === 'ArrowRight' && focusedTagIndex !== undefined) {
|
||||
if (focusedTagIndex === items.length - 1) {
|
||||
setFocusedTagIndex(undefined)
|
||||
return
|
||||
}
|
||||
setFocusedTagIndex(focusedTagIndex + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const removeItem = (index: number) => {
|
||||
if (!items) return
|
||||
const newItems = [...items]
|
||||
newItems.splice(index, 1)
|
||||
onChange(newItems)
|
||||
}
|
||||
|
||||
const addItem = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
if (isEmpty(inputValue)) return
|
||||
setInputValue('')
|
||||
onChange(items ? [...items, inputValue.trim()] : [inputValue.trim()])
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrap
|
||||
spacing={1}
|
||||
borderWidth={1}
|
||||
boxShadow={isFocused ? `0 0 0 1px ${colors['blue'][500]}` : undefined}
|
||||
p="2"
|
||||
rounded="md"
|
||||
borderColor={isFocused ? 'blue.500' : 'gray.200'}
|
||||
transitionProperty="box-shadow, border-color"
|
||||
transitionDuration="150ms"
|
||||
transitionTimingFunction="ease-in-out"
|
||||
onClick={() => inputRef.current?.focus()}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{items?.map((item, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, transform: 'translateY(5px)' }}
|
||||
animate={{ opacity: 1, transform: 'translateY(0)' }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
>
|
||||
<WrapItem>
|
||||
<Tag
|
||||
content={item}
|
||||
onDeleteClick={() => removeItem(index)}
|
||||
isFocused={focusedTagIndex === index}
|
||||
/>
|
||||
</WrapItem>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
<WrapItem>
|
||||
<form onSubmit={addItem}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
h="24px"
|
||||
p="0"
|
||||
borderWidth={0}
|
||||
focusBorderColor="transparent"
|
||||
size="sm"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
placeholder={items && items.length === 0 ? placeholder : undefined}
|
||||
/>
|
||||
</form>
|
||||
</WrapItem>
|
||||
</Wrap>
|
||||
)
|
||||
}
|
||||
|
||||
const Tag = ({
|
||||
isFocused,
|
||||
content,
|
||||
onDeleteClick,
|
||||
}: {
|
||||
isFocused?: boolean
|
||||
content: string
|
||||
onDeleteClick: () => void
|
||||
}) => (
|
||||
<HStack
|
||||
spacing={0.5}
|
||||
borderWidth="1px"
|
||||
pl="1"
|
||||
rounded="sm"
|
||||
maxW="100%"
|
||||
borderColor={isFocused ? 'blue.500' : undefined}
|
||||
boxShadow={isFocused ? `0 0 0 1px ${colors['blue'][500]}` : undefined}
|
||||
>
|
||||
<Text fontSize="sm" noOfLines={1}>
|
||||
{content}
|
||||
</Text>
|
||||
<IconButton
|
||||
size="xs"
|
||||
icon={<CloseIcon />}
|
||||
aria-label="Remove tag"
|
||||
variant="ghost"
|
||||
onClick={onDeleteClick}
|
||||
/>
|
||||
</HStack>
|
||||
)
|
@ -22,6 +22,7 @@ import { isEmpty } from '@typebot.io/lib'
|
||||
import { useGraph } from '@/features/graph/providers/GraphProvider'
|
||||
import { ButtonsItemSettings } from './ButtonsItemSettings'
|
||||
import { useTranslate } from '@tolgee/react'
|
||||
import { convertStrToList } from '@typebot.io/lib/convertStrToList'
|
||||
|
||||
type Props = {
|
||||
item: ButtonItem
|
||||
@ -70,26 +71,18 @@ export const ButtonsItemNode = ({ item, indices, isMouseOver }: Props) => {
|
||||
|
||||
const handleEditableChange = (val: string) => {
|
||||
if (val.length - itemValue.length && val.endsWith('\n')) return
|
||||
const splittedBreakLines = val.split('\n')
|
||||
const splittedCommas = val.split(',')
|
||||
const isPastingMultipleItems =
|
||||
val.length - itemValue.length > 1 &&
|
||||
(splittedBreakLines.length > 2 || splittedCommas.length > 2)
|
||||
if (isPastingMultipleItems) {
|
||||
const values =
|
||||
splittedBreakLines.length > 2 ? splittedBreakLines : splittedCommas
|
||||
return values.forEach((v, i) => {
|
||||
if (i === 0) {
|
||||
setItemValue(v)
|
||||
} else {
|
||||
createItem(
|
||||
{ content: v.trim() },
|
||||
{ ...indices, itemIndex: indices.itemIndex + i }
|
||||
)
|
||||
}
|
||||
if (val.length - itemValue.length === 1) return setItemValue(val)
|
||||
const values = convertStrToList(val)
|
||||
if (values.length === 1) {
|
||||
setItemValue(values[0])
|
||||
} else {
|
||||
values.forEach((v, i) => {
|
||||
createItem(
|
||||
{ content: v },
|
||||
{ ...indices, itemIndex: indices.itemIndex + i }
|
||||
)
|
||||
})
|
||||
}
|
||||
setItemValue(val)
|
||||
}
|
||||
|
||||
const handlePlusClick = () => {
|
||||
|
@ -24,10 +24,11 @@ import {
|
||||
ForgedBlockDefinition,
|
||||
ForgedBlock,
|
||||
} from '@typebot.io/forge-repository/types'
|
||||
import { PrimitiveList } from '@/components/PrimitiveList'
|
||||
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
||||
import { CodeEditor } from '@/components/inputs/CodeEditor'
|
||||
import { getZodInnerSchema } from '../../helpers/getZodInnerSchema'
|
||||
import { TagsInput } from '@/components/TagsInput'
|
||||
import { PrimitiveList } from '@/components/PrimitiveList'
|
||||
|
||||
const mdComponents = {
|
||||
a: ({ href, children }) => (
|
||||
@ -316,28 +317,43 @@ const ZodArrayContent = ({
|
||||
const type = schema._def.type._def.innerType?._def.typeName
|
||||
if (type === 'ZodString' || type === 'ZodNumber' || type === 'ZodEnum')
|
||||
return (
|
||||
<Stack spacing={0}>
|
||||
<Stack
|
||||
spacing={0}
|
||||
marginTop={layout?.mergeWithLastField ? '-3' : undefined}
|
||||
>
|
||||
{layout?.label && <FormLabel>{layout.label}</FormLabel>}
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<PrimitiveList
|
||||
onItemsChange={(items) => {
|
||||
onDataChange(items)
|
||||
}}
|
||||
initialItems={data}
|
||||
addLabel={`Add ${layout?.itemLabel ?? ''}`}
|
||||
>
|
||||
{({ item, onItemChange }) => (
|
||||
<ZodFieldLayout
|
||||
schema={schema._def.type}
|
||||
data={item}
|
||||
blockDef={blockDef}
|
||||
blockOptions={blockOptions}
|
||||
isInAccordion={isInAccordion}
|
||||
onDataChange={onItemChange}
|
||||
width="full"
|
||||
/>
|
||||
)}
|
||||
</PrimitiveList>
|
||||
<Stack
|
||||
p="4"
|
||||
rounded="md"
|
||||
flex="1"
|
||||
borderWidth="1px"
|
||||
borderTopWidth={layout?.mergeWithLastField ? '0' : undefined}
|
||||
borderTopRadius={layout?.mergeWithLastField ? '0' : undefined}
|
||||
pt={layout?.mergeWithLastField ? '5' : undefined}
|
||||
>
|
||||
{type === 'ZodString' ? (
|
||||
<TagsInput items={data} onChange={onDataChange} />
|
||||
) : (
|
||||
<PrimitiveList
|
||||
onItemsChange={(items) => {
|
||||
onDataChange(items)
|
||||
}}
|
||||
initialItems={data}
|
||||
addLabel={`Add ${layout?.itemLabel ?? ''}`}
|
||||
>
|
||||
{({ item, onItemChange }) => (
|
||||
<ZodFieldLayout
|
||||
schema={schema._def.type}
|
||||
data={item}
|
||||
blockDef={blockDef}
|
||||
blockOptions={blockOptions}
|
||||
isInAccordion={isInAccordion}
|
||||
onDataChange={onItemChange}
|
||||
width="full"
|
||||
/>
|
||||
)}
|
||||
</PrimitiveList>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
|
@ -2,10 +2,9 @@ import { FormControl, FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { Settings } from '@typebot.io/schemas'
|
||||
import React from 'react'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { TextInput } from '@/components/inputs'
|
||||
import { env } from '@typebot.io/env'
|
||||
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
||||
import { PrimitiveList } from '@/components/PrimitiveList'
|
||||
import { TagsInput } from '@/components/TagsInput'
|
||||
import { env } from '@typebot.io/env'
|
||||
|
||||
type Props = {
|
||||
security: Settings['security']
|
||||
@ -30,20 +29,11 @@ export const SecurityForm = ({ security, onUpdate }: Props) => {
|
||||
By default your bot can be executed on any website.
|
||||
</MoreInfoTooltip>
|
||||
</FormLabel>
|
||||
<PrimitiveList
|
||||
initialItems={security?.allowedOrigins}
|
||||
onItemsChange={updateItems}
|
||||
addLabel="Add URL"
|
||||
>
|
||||
{({ item, onItemChange }) => (
|
||||
<TextInput
|
||||
width="full"
|
||||
defaultValue={item}
|
||||
onChange={onItemChange}
|
||||
placeholder={env.NEXT_PUBLIC_VIEWER_URL[0]}
|
||||
/>
|
||||
)}
|
||||
</PrimitiveList>
|
||||
<TagsInput
|
||||
items={security?.allowedOrigins}
|
||||
onChange={updateItems}
|
||||
placeholder={env.NEXT_PUBLIC_VIEWER_URL[0]}
|
||||
/>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
)
|
||||
|
Reference in New Issue
Block a user