@ -57,7 +57,7 @@ export const ImageUploadContent = ({
|
|||||||
onClick={() => setCurrentTab('link')}
|
onClick={() => setCurrentTab('link')}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
Embed link
|
Link
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={currentTab === 'upload' ? 'solid' : 'ghost'}
|
variant={currentTab === 'upload' ? 'solid' : 'ghost'}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
AlertIcon,
|
AlertIcon,
|
||||||
|
Box,
|
||||||
Flex,
|
Flex,
|
||||||
Grid,
|
Grid,
|
||||||
GridItem,
|
GridItem,
|
||||||
@ -157,7 +158,7 @@ export const UnsplashPicker = ({ imageSize, onImageSelect }: Props) => {
|
|||||||
)}
|
)}
|
||||||
<Stack overflowY="scroll" maxH="400px" ref={scrollContainer}>
|
<Stack overflowY="scroll" maxH="400px" ref={scrollContainer}>
|
||||||
{images.length > 0 && (
|
{images.length > 0 && (
|
||||||
<Grid templateColumns="repeat(4, 1fr)" columnGap={2} rowGap={3}>
|
<Grid templateColumns="repeat(3, 1fr)" columnGap={2} rowGap={3}>
|
||||||
{images.map((image, index) => (
|
{images.map((image, index) => (
|
||||||
<GridItem
|
<GridItem
|
||||||
as={Stack}
|
as={Stack}
|
||||||
@ -190,11 +191,17 @@ type UnsplashImageProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const UnsplashImage = ({ image, onClick }: UnsplashImageProps) => {
|
const UnsplashImage = ({ image, onClick }: UnsplashImageProps) => {
|
||||||
const linkColor = useColorModeValue('gray.500', 'gray.400')
|
const [isImageHovered, setIsImageHovered] = useState(false)
|
||||||
|
|
||||||
const { user, urls, alt_description } = image
|
const { user, urls, alt_description } = image
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box
|
||||||
|
pos="relative"
|
||||||
|
onMouseEnter={() => setIsImageHovered(true)}
|
||||||
|
onMouseLeave={() => setIsImageHovered(false)}
|
||||||
|
h="full"
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
src={urls.thumb}
|
src={urls.thumb}
|
||||||
@ -204,17 +211,28 @@ const UnsplashImage = ({ image, onClick }: UnsplashImageProps) => {
|
|||||||
h="100%"
|
h="100%"
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
/>
|
/>
|
||||||
<TextLink
|
<Box
|
||||||
fontSize="xs"
|
pos="absolute"
|
||||||
isExternal
|
bottom={0}
|
||||||
href={`https://unsplash.com/@${user.username}?utm_source=${env(
|
left={0}
|
||||||
'UNSPLASH_APP_NAME'
|
bgColor="rgba(0,0,0,.5)"
|
||||||
)}&utm_medium=referral`}
|
px="2"
|
||||||
noOfLines={1}
|
rounded="md"
|
||||||
color={linkColor}
|
opacity={isImageHovered ? 1 : 0}
|
||||||
|
transition="opacity .2s ease-in-out"
|
||||||
>
|
>
|
||||||
{user.name}
|
<TextLink
|
||||||
</TextLink>
|
fontSize="xs"
|
||||||
</>
|
isExternal
|
||||||
|
href={`https://unsplash.com/@${user.username}?utm_source=${env(
|
||||||
|
'UNSPLASH_APP_NAME'
|
||||||
|
)}&utm_medium=referral`}
|
||||||
|
noOfLines={1}
|
||||||
|
color="white"
|
||||||
|
>
|
||||||
|
{user.name}
|
||||||
|
</TextLink>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
17
apps/builder/src/components/SwitchWithRelatedSettings.tsx
Normal file
17
apps/builder/src/components/SwitchWithRelatedSettings.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { SwitchWithLabel, SwitchWithLabelProps } from './inputs/SwitchWithLabel'
|
||||||
|
import { Stack } from '@chakra-ui/react'
|
||||||
|
|
||||||
|
type Props = SwitchWithLabelProps
|
||||||
|
|
||||||
|
export const SwitchWithRelatedSettings = ({ children, ...props }: Props) => (
|
||||||
|
<Stack
|
||||||
|
borderWidth={props.initialValue ? 1 : undefined}
|
||||||
|
rounded="md"
|
||||||
|
p={props.initialValue ? '4' : undefined}
|
||||||
|
spacing={4}
|
||||||
|
>
|
||||||
|
<SwitchWithLabel {...props} />
|
||||||
|
{props.initialValue && children}
|
||||||
|
</Stack>
|
||||||
|
)
|
@ -8,7 +8,7 @@ import {
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { MoreInfoTooltip } from '../MoreInfoTooltip'
|
import { MoreInfoTooltip } from '../MoreInfoTooltip'
|
||||||
|
|
||||||
type SwitchWithLabelProps = {
|
export type SwitchWithLabelProps = {
|
||||||
label: string
|
label: string
|
||||||
initialValue: boolean
|
initialValue: boolean
|
||||||
moreInfoContent?: string
|
moreInfoContent?: string
|
||||||
|
@ -48,7 +48,6 @@ test.describe.parallel('Image bubble block', () => {
|
|||||||
await page.goto(`/typebots/${typebotId}/edit`)
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
|
|
||||||
await page.click('text=Click to edit...')
|
await page.click('text=Click to edit...')
|
||||||
await page.click('text=Embed link')
|
|
||||||
await page.fill(
|
await page.fill(
|
||||||
'input[placeholder="Paste the image link..."]',
|
'input[placeholder="Paste the image link..."]',
|
||||||
unsplashImageSrc
|
unsplashImageSrc
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import { TextInput } from '@/components/inputs'
|
import { TextInput } from '@/components/inputs'
|
||||||
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
||||||
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
|
||||||
import { VariableSearchInput } from '@/components/inputs/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 '@typebot.io/schemas'
|
import {
|
||||||
|
ChoiceInputOptions,
|
||||||
|
Variable,
|
||||||
|
defaultChoiceInputOptions,
|
||||||
|
} from '@typebot.io/schemas'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
options?: ChoiceInputOptions
|
options?: ChoiceInputOptions
|
||||||
@ -18,6 +22,8 @@ export const ButtonsBlockSettings = ({ options, onOptionsChange }: Props) => {
|
|||||||
options && onOptionsChange({ ...options, isSearchable })
|
options && onOptionsChange({ ...options, isSearchable })
|
||||||
const updateButtonLabel = (buttonLabel: string) =>
|
const updateButtonLabel = (buttonLabel: string) =>
|
||||||
options && onOptionsChange({ ...options, buttonLabel })
|
options && onOptionsChange({ ...options, buttonLabel })
|
||||||
|
const updateSearchInputPlaceholder = (searchInputPlaceholder: string) =>
|
||||||
|
options && onOptionsChange({ ...options, searchInputPlaceholder })
|
||||||
const updateSaveVariable = (variable?: Variable) =>
|
const updateSaveVariable = (variable?: Variable) =>
|
||||||
options && onOptionsChange({ ...options, variableId: variable?.id })
|
options && onOptionsChange({ ...options, variableId: variable?.id })
|
||||||
const updateDynamicDataVariable = (variable?: Variable) =>
|
const updateDynamicDataVariable = (variable?: Variable) =>
|
||||||
@ -25,23 +31,31 @@ export const ButtonsBlockSettings = ({ options, onOptionsChange }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<SwitchWithLabel
|
<SwitchWithRelatedSettings
|
||||||
label="Multiple choice?"
|
label="Multiple choice?"
|
||||||
initialValue={options?.isMultipleChoice ?? false}
|
initialValue={options?.isMultipleChoice ?? false}
|
||||||
onCheckChange={updateIsMultiple}
|
onCheckChange={updateIsMultiple}
|
||||||
/>
|
>
|
||||||
<SwitchWithLabel
|
|
||||||
label="Is searchable?"
|
|
||||||
initialValue={options?.isSearchable ?? false}
|
|
||||||
onCheckChange={updateIsSearchable}
|
|
||||||
/>
|
|
||||||
{options?.isMultipleChoice && (
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Button label:"
|
label="Submit button label:"
|
||||||
defaultValue={options?.buttonLabel ?? 'Send'}
|
defaultValue={options?.buttonLabel ?? 'Send'}
|
||||||
onChange={updateButtonLabel}
|
onChange={updateButtonLabel}
|
||||||
/>
|
/>
|
||||||
)}
|
</SwitchWithRelatedSettings>
|
||||||
|
<SwitchWithRelatedSettings
|
||||||
|
label="Is searchable?"
|
||||||
|
initialValue={options?.isSearchable ?? false}
|
||||||
|
onCheckChange={updateIsSearchable}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
label="Input placeholder:"
|
||||||
|
defaultValue={
|
||||||
|
options?.searchInputPlaceholder ??
|
||||||
|
defaultChoiceInputOptions.searchInputPlaceholder
|
||||||
|
}
|
||||||
|
onChange={updateSearchInputPlaceholder}
|
||||||
|
/>
|
||||||
|
</SwitchWithRelatedSettings>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
Dynamic data:{' '}
|
Dynamic data:{' '}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
import { ImageIcon } from '@/components/icons'
|
||||||
|
import { IconProps } from '@chakra-ui/react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const PictureChoiceIcon = (props: IconProps) => (
|
||||||
|
<ImageIcon color="orange.500" {...props} />
|
||||||
|
)
|
@ -0,0 +1,148 @@
|
|||||||
|
import {
|
||||||
|
Fade,
|
||||||
|
IconButton,
|
||||||
|
Flex,
|
||||||
|
Image,
|
||||||
|
Popover,
|
||||||
|
Portal,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverArrow,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverAnchor,
|
||||||
|
useEventListener,
|
||||||
|
useColorModeValue,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { ImageIcon, PlusIcon } from '@/components/icons'
|
||||||
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
|
import { ItemIndices, ItemType } from '@typebot.io/schemas'
|
||||||
|
import React, { useRef } from 'react'
|
||||||
|
import { PictureChoiceItem } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
|
||||||
|
import { useGraph } from '@/features/graph/providers/GraphProvider'
|
||||||
|
import { PictureChoiceItemSettings } from './PictureChoiceItemSettings'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
item: PictureChoiceItem
|
||||||
|
indices: ItemIndices
|
||||||
|
isMouseOver: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PictureChoiceItemNode = ({
|
||||||
|
item,
|
||||||
|
indices,
|
||||||
|
isMouseOver,
|
||||||
|
}: Props) => {
|
||||||
|
const emptyImageBgColor = useColorModeValue('gray.100', 'gray.700')
|
||||||
|
const { openedItemId, setOpenedItemId } = useGraph()
|
||||||
|
const { updateItem, createItem, typebot } = useTypebot()
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
const handlePlusClick = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
const itemIndex = indices.itemIndex + 1
|
||||||
|
createItem(
|
||||||
|
{ blockId: item.blockId, type: ItemType.PICTURE_CHOICE },
|
||||||
|
{ ...indices, itemIndex }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation()
|
||||||
|
|
||||||
|
const openPopover = () => {
|
||||||
|
setOpenedItemId(item.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleItemChange = (updates: Partial<PictureChoiceItem>) => {
|
||||||
|
updateItem(indices, { ...item, ...updates })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseWheel = (e: WheelEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
useEventListener('wheel', handleMouseWheel, ref.current)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
placement="right"
|
||||||
|
isLazy
|
||||||
|
isOpen={openedItemId === item.id}
|
||||||
|
closeOnBlur={false}
|
||||||
|
>
|
||||||
|
<PopoverAnchor>
|
||||||
|
<Flex
|
||||||
|
px={4}
|
||||||
|
py={2}
|
||||||
|
justify="center"
|
||||||
|
w="full"
|
||||||
|
pos="relative"
|
||||||
|
onClick={openPopover}
|
||||||
|
data-testid="item-node"
|
||||||
|
userSelect="none"
|
||||||
|
>
|
||||||
|
{item.pictureSrc ? (
|
||||||
|
<Image
|
||||||
|
src={item.pictureSrc}
|
||||||
|
alt="Picture choice image"
|
||||||
|
rounded="md"
|
||||||
|
maxH="128px"
|
||||||
|
w="full"
|
||||||
|
objectFit="cover"
|
||||||
|
userSelect="none"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Flex
|
||||||
|
width="full"
|
||||||
|
height="100px"
|
||||||
|
bgColor={emptyImageBgColor}
|
||||||
|
rounded="md"
|
||||||
|
justify="center"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<ImageIcon />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
<Fade
|
||||||
|
in={isMouseOver}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '-15px',
|
||||||
|
zIndex: 3,
|
||||||
|
left: '90px',
|
||||||
|
}}
|
||||||
|
unmountOnExit
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Add item"
|
||||||
|
icon={<PlusIcon />}
|
||||||
|
size="xs"
|
||||||
|
shadow="md"
|
||||||
|
colorScheme="gray"
|
||||||
|
borderWidth={1}
|
||||||
|
onClick={handlePlusClick}
|
||||||
|
/>
|
||||||
|
</Fade>
|
||||||
|
</Flex>
|
||||||
|
</PopoverAnchor>
|
||||||
|
<Portal>
|
||||||
|
<PopoverContent pos="relative" onMouseDown={handleMouseDown}>
|
||||||
|
<PopoverArrow />
|
||||||
|
<PopoverBody
|
||||||
|
py="6"
|
||||||
|
overflowY="scroll"
|
||||||
|
maxH="400px"
|
||||||
|
shadow="lg"
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
{typebot && (
|
||||||
|
<PictureChoiceItemSettings
|
||||||
|
typebotId={typebot.id}
|
||||||
|
item={item}
|
||||||
|
onItemChange={handleItemChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Portal>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { TextInput, Textarea } from '@/components/inputs'
|
||||||
|
import { PictureChoiceItem } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
HStack,
|
||||||
|
Popover,
|
||||||
|
PopoverAnchor,
|
||||||
|
PopoverContent,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
useDisclosure,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { ImageUploadContent } from '@/components/ImageUploadContent'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
typebotId: string
|
||||||
|
item: PictureChoiceItem
|
||||||
|
onItemChange: (updates: Partial<PictureChoiceItem>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PictureChoiceItemSettings = ({
|
||||||
|
typebotId,
|
||||||
|
item,
|
||||||
|
onItemChange,
|
||||||
|
}: Props) => {
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
|
const updateTitle = (title: string) => onItemChange({ ...item, title })
|
||||||
|
|
||||||
|
const updateImage = (pictureSrc: string) => {
|
||||||
|
onItemChange({ ...item, pictureSrc })
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateDescription = (description: string) =>
|
||||||
|
onItemChange({ ...item, description })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<HStack>
|
||||||
|
<Text fontWeight="medium">Image:</Text>
|
||||||
|
<Popover isLazy isOpen={isOpen}>
|
||||||
|
<PopoverAnchor>
|
||||||
|
<Button size="sm" onClick={onOpen}>
|
||||||
|
Pick an image
|
||||||
|
</Button>
|
||||||
|
</PopoverAnchor>
|
||||||
|
<PopoverContent p="4" w="500px">
|
||||||
|
<ImageUploadContent
|
||||||
|
filePath={`typebots/${typebotId}/blocks/${item.blockId}/items/${item.id}`}
|
||||||
|
defaultUrl={item.pictureSrc}
|
||||||
|
onSubmit={updateImage}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</HStack>
|
||||||
|
<TextInput
|
||||||
|
label="Title:"
|
||||||
|
defaultValue={item.title}
|
||||||
|
onChange={updateTitle}
|
||||||
|
/>
|
||||||
|
<Textarea
|
||||||
|
label="Description:"
|
||||||
|
defaultValue={item.description}
|
||||||
|
onChange={updateDescription}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
import { BlockIndices } from '@typebot.io/schemas'
|
||||||
|
import React from 'react'
|
||||||
|
import { Stack, Tag, Wrap, Text } from '@chakra-ui/react'
|
||||||
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
|
import { SetVariableLabel } from '@/components/SetVariableLabel'
|
||||||
|
import { ItemNodesList } from '@/features/graph/components/nodes/item/ItemNodesList'
|
||||||
|
import { PictureChoiceBlock } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
block: PictureChoiceBlock
|
||||||
|
indices: BlockIndices
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PictureChoiceNode = ({ block, indices }: Props) => {
|
||||||
|
const { typebot } = useTypebot()
|
||||||
|
const dynamicVariableName = typebot?.variables.find(
|
||||||
|
(variable) =>
|
||||||
|
variable.id === block.options.dynamicItems?.pictureSrcsVariableId
|
||||||
|
)?.name
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack w="full">
|
||||||
|
{block.options.dynamicItems?.isEnabled && dynamicVariableName ? (
|
||||||
|
<Wrap spacing={1}>
|
||||||
|
<Text>Display</Text>
|
||||||
|
<Tag bg="orange.400" color="white">
|
||||||
|
{dynamicVariableName}
|
||||||
|
</Tag>
|
||||||
|
<Text>pictures</Text>
|
||||||
|
</Wrap>
|
||||||
|
) : (
|
||||||
|
<ItemNodesList block={block} indices={indices} />
|
||||||
|
)}
|
||||||
|
{block.options.variableId ? (
|
||||||
|
<SetVariableLabel
|
||||||
|
variableId={block.options.variableId}
|
||||||
|
variables={typebot?.variables}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,142 @@
|
|||||||
|
import { TextInput } from '@/components/inputs'
|
||||||
|
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
|
||||||
|
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||||
|
import { Variable } from '@typebot.io/schemas'
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
PictureChoiceBlock,
|
||||||
|
defaultPictureChoiceOptions,
|
||||||
|
} from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
|
||||||
|
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
options?: PictureChoiceBlock['options']
|
||||||
|
onOptionsChange: (options: PictureChoiceBlock['options']) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PictureChoiceSettings = ({ options, onOptionsChange }: Props) => {
|
||||||
|
const updateIsMultiple = (isMultipleChoice: boolean) =>
|
||||||
|
options && onOptionsChange({ ...options, isMultipleChoice })
|
||||||
|
const updateButtonLabel = (buttonLabel: string) =>
|
||||||
|
options && onOptionsChange({ ...options, buttonLabel })
|
||||||
|
const updateSaveVariable = (variable?: Variable) =>
|
||||||
|
options && onOptionsChange({ ...options, variableId: variable?.id })
|
||||||
|
const updateSearchInputPlaceholder = (searchInputPlaceholder: string) =>
|
||||||
|
options && onOptionsChange({ ...options, searchInputPlaceholder })
|
||||||
|
const updateIsSearchable = (isSearchable: boolean) =>
|
||||||
|
options && onOptionsChange({ ...options, isSearchable })
|
||||||
|
|
||||||
|
const updateIsDynamicItemsEnabled = (isEnabled: boolean) =>
|
||||||
|
options &&
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
dynamicItems: {
|
||||||
|
...options.dynamicItems,
|
||||||
|
isEnabled,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateDynamicItemsPictureSrcsVariable = (variable?: Variable) =>
|
||||||
|
options &&
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
dynamicItems: {
|
||||||
|
...options.dynamicItems,
|
||||||
|
pictureSrcsVariableId: variable?.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateDynamicItemsTitlesVariable = (variable?: Variable) =>
|
||||||
|
options &&
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
dynamicItems: {
|
||||||
|
...options.dynamicItems,
|
||||||
|
titlesVariableId: variable?.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateDynamicItemsDescriptionsVariable = (variable?: Variable) =>
|
||||||
|
options &&
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
dynamicItems: {
|
||||||
|
...options.dynamicItems,
|
||||||
|
descriptionsVariableId: variable?.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<SwitchWithRelatedSettings
|
||||||
|
label="Is searchable?"
|
||||||
|
initialValue={options?.isSearchable ?? false}
|
||||||
|
onCheckChange={updateIsSearchable}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
label="Input placeholder:"
|
||||||
|
defaultValue={
|
||||||
|
options?.searchInputPlaceholder ??
|
||||||
|
defaultPictureChoiceOptions.searchInputPlaceholder
|
||||||
|
}
|
||||||
|
onChange={updateSearchInputPlaceholder}
|
||||||
|
/>
|
||||||
|
</SwitchWithRelatedSettings>
|
||||||
|
<SwitchWithRelatedSettings
|
||||||
|
label="Multiple choice?"
|
||||||
|
initialValue={options?.isMultipleChoice ?? false}
|
||||||
|
onCheckChange={updateIsMultiple}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
label="Submit button label:"
|
||||||
|
defaultValue={options?.buttonLabel ?? 'Send'}
|
||||||
|
onChange={updateButtonLabel}
|
||||||
|
/>
|
||||||
|
</SwitchWithRelatedSettings>
|
||||||
|
|
||||||
|
<SwitchWithRelatedSettings
|
||||||
|
label="Dynamic items?"
|
||||||
|
initialValue={options?.dynamicItems?.isEnabled ?? false}
|
||||||
|
onCheckChange={updateIsDynamicItemsEnabled}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<FormLabel mb="0" htmlFor="variable">
|
||||||
|
Images:
|
||||||
|
</FormLabel>
|
||||||
|
<VariableSearchInput
|
||||||
|
initialVariableId={options?.dynamicItems?.pictureSrcsVariableId}
|
||||||
|
onSelectVariable={updateDynamicItemsPictureSrcsVariable}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<FormLabel mb="0" htmlFor="variable">
|
||||||
|
Titles:
|
||||||
|
</FormLabel>
|
||||||
|
<VariableSearchInput
|
||||||
|
initialVariableId={options?.dynamicItems?.titlesVariableId}
|
||||||
|
onSelectVariable={updateDynamicItemsTitlesVariable}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<FormLabel mb="0" htmlFor="variable">
|
||||||
|
Descriptions:
|
||||||
|
</FormLabel>
|
||||||
|
<VariableSearchInput
|
||||||
|
initialVariableId={options?.dynamicItems?.descriptionsVariableId}
|
||||||
|
onSelectVariable={updateDynamicItemsDescriptionsVariable}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</SwitchWithRelatedSettings>
|
||||||
|
|
||||||
|
<Stack>
|
||||||
|
<FormLabel mb="0" htmlFor="variable">
|
||||||
|
Save answer in a variable:
|
||||||
|
</FormLabel>
|
||||||
|
<VariableSearchInput
|
||||||
|
initialVariableId={options?.variableId}
|
||||||
|
onSelectVariable={updateSaveVariable}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,121 @@
|
|||||||
|
import test, { expect } from '@playwright/test'
|
||||||
|
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
|
||||||
|
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
|
||||||
|
import { InputBlockType, ItemType } from '@typebot.io/schemas'
|
||||||
|
import { createId } from '@paralleldrive/cuid2'
|
||||||
|
import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
|
||||||
|
|
||||||
|
const firstImageSrc =
|
||||||
|
'https://images.unsplash.com/flagged/photo-1575517111839-3a3843ee7f5d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2940&q=80'
|
||||||
|
|
||||||
|
const secondImageSrc =
|
||||||
|
'https://images.unsplash.com/photo-1582582621959-48d27397dc69?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2938&q=80'
|
||||||
|
|
||||||
|
const thirdImageSrc =
|
||||||
|
'https://images.unsplash.com/photo-1564019472231-4586c552dc27?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80'
|
||||||
|
|
||||||
|
test.describe.parallel('Picture choice input block', () => {
|
||||||
|
test('can edit items', async ({ page }) => {
|
||||||
|
const typebotId = createId()
|
||||||
|
await createTypebots([
|
||||||
|
{
|
||||||
|
id: typebotId,
|
||||||
|
...parseDefaultGroupWithBlock({
|
||||||
|
type: InputBlockType.PICTURE_CHOICE,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'choice1',
|
||||||
|
blockId: 'block1',
|
||||||
|
type: ItemType.PICTURE_CHOICE,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: { ...defaultPictureChoiceOptions },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
|
await page.getByTestId('item-node').click()
|
||||||
|
await page.getByRole('button', { name: 'Pick an image' }).click()
|
||||||
|
await page.getByPlaceholder('Paste the image link...').fill(firstImageSrc)
|
||||||
|
await page.getByLabel('Title:').fill('First image')
|
||||||
|
await page.getByLabel('Description:').fill('First description')
|
||||||
|
await page.getByText('Default').click()
|
||||||
|
await page.getByRole('img', { name: 'Picture choice image' }).hover()
|
||||||
|
await page.getByRole('button', { name: 'Add item' }).click()
|
||||||
|
await page.getByTestId('item-node').last().click()
|
||||||
|
await page.getByRole('button', { name: 'Pick an image' }).click()
|
||||||
|
await page.getByPlaceholder('Paste the image link...').fill(secondImageSrc)
|
||||||
|
await page.getByLabel('Title:').fill('Second image')
|
||||||
|
await page.getByLabel('Description:').fill('Second description')
|
||||||
|
await page.getByRole('img', { name: 'Picture choice image' }).last().hover()
|
||||||
|
await page.getByRole('button', { name: 'Add item' }).click()
|
||||||
|
await page.getByTestId('item-node').last().click()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Pick an image' })
|
||||||
|
).toHaveCount(1)
|
||||||
|
await page.getByRole('button', { name: 'Pick an image' }).click()
|
||||||
|
await page.getByPlaceholder('Paste the image link...').fill(thirdImageSrc)
|
||||||
|
await page.getByLabel('Title:').fill('Third image')
|
||||||
|
await page.getByLabel('Description:').fill('Third description')
|
||||||
|
await page.getByRole('button', { name: 'Preview' }).click()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', {
|
||||||
|
name: 'First image First image First description',
|
||||||
|
})
|
||||||
|
).toBeVisible()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', {
|
||||||
|
name: 'Second image Second image Second description',
|
||||||
|
})
|
||||||
|
).toBeVisible()
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'Third image Third image Third description',
|
||||||
|
})
|
||||||
|
.click()
|
||||||
|
await expect(page.getByTestId('guest-bubble')).toBeVisible()
|
||||||
|
await expect(
|
||||||
|
page.locator('typebot-standard').getByText('Third image')
|
||||||
|
).toBeVisible()
|
||||||
|
|
||||||
|
await page.getByTestId('block2-icon').click()
|
||||||
|
await page.getByText('Multiple choice?').click()
|
||||||
|
await page.getByLabel('Submit button label:').fill('Go')
|
||||||
|
await page.getByRole('button', { name: 'Restart' }).click()
|
||||||
|
await page
|
||||||
|
.getByRole('checkbox', {
|
||||||
|
name: 'First image First image First description',
|
||||||
|
})
|
||||||
|
.click()
|
||||||
|
await page
|
||||||
|
.getByRole('checkbox', {
|
||||||
|
name: 'Second image Second image Second description',
|
||||||
|
})
|
||||||
|
.click()
|
||||||
|
await page.getByRole('button', { name: 'Go' }).click()
|
||||||
|
await expect(
|
||||||
|
page.locator('typebot-standard').getByText('First image, Second image')
|
||||||
|
).toBeVisible()
|
||||||
|
|
||||||
|
await page.getByTestId('block2-icon').click()
|
||||||
|
await page.getByText('Is searchable?').click()
|
||||||
|
await page.getByLabel('Input placeholder:').fill('Search...')
|
||||||
|
await page.getByRole('button', { name: 'Restart' }).click()
|
||||||
|
await page.getByPlaceholder('Search...').fill('second')
|
||||||
|
await expect(
|
||||||
|
page.getByRole('checkbox', {
|
||||||
|
name: 'First image First image First description',
|
||||||
|
})
|
||||||
|
).toBeHidden()
|
||||||
|
await page
|
||||||
|
.getByRole('checkbox', {
|
||||||
|
name: 'Second image Second image Second description',
|
||||||
|
})
|
||||||
|
.click()
|
||||||
|
await page.getByRole('button', { name: 'Go' }).click()
|
||||||
|
await expect(
|
||||||
|
page.locator('typebot-standard').getByText('Second image')
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
@ -6,7 +6,6 @@ import {
|
|||||||
Wrap,
|
Wrap,
|
||||||
Fade,
|
Fade,
|
||||||
IconButton,
|
IconButton,
|
||||||
PopoverTrigger,
|
|
||||||
Popover,
|
Popover,
|
||||||
Portal,
|
Portal,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@ -14,6 +13,7 @@ import {
|
|||||||
PopoverBody,
|
PopoverBody,
|
||||||
useEventListener,
|
useEventListener,
|
||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
|
PopoverAnchor,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
import {
|
import {
|
||||||
@ -79,7 +79,7 @@ export const ConditionItemNode = ({ item, isMouseOver, indices }: Props) => {
|
|||||||
isOpen={openedItemId === item.id}
|
isOpen={openedItemId === item.id}
|
||||||
closeOnBlur={false}
|
closeOnBlur={false}
|
||||||
>
|
>
|
||||||
<PopoverTrigger>
|
<PopoverAnchor>
|
||||||
<Flex p={3} pos="relative" w="full" onClick={openPopover}>
|
<Flex p={3} pos="relative" w="full" onClick={openPopover}>
|
||||||
{item.content.comparisons.length === 0 ||
|
{item.content.comparisons.length === 0 ||
|
||||||
comparisonIsEmpty(item.content.comparisons[0]) ? (
|
comparisonIsEmpty(item.content.comparisons[0]) ? (
|
||||||
@ -101,7 +101,7 @@ export const ConditionItemNode = ({ item, isMouseOver, indices }: Props) => {
|
|||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
{comparison.comparisonOperator && (
|
{comparison.comparisonOperator && (
|
||||||
<Text>
|
<Text fontSize="sm">
|
||||||
{parseComparisonOperatorSymbol(
|
{parseComparisonOperatorSymbol(
|
||||||
comparison.comparisonOperator
|
comparison.comparisonOperator
|
||||||
)}
|
)}
|
||||||
@ -137,7 +137,7 @@ export const ConditionItemNode = ({ item, isMouseOver, indices }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</Fade>
|
</Fade>
|
||||||
</Flex>
|
</Flex>
|
||||||
</PopoverTrigger>
|
</PopoverAnchor>
|
||||||
<Portal>
|
<Portal>
|
||||||
<PopoverContent pos="relative" onMouseDown={handleMouseDown}>
|
<PopoverContent pos="relative" onMouseDown={handleMouseDown}>
|
||||||
<PopoverArrow />
|
<PopoverArrow />
|
||||||
@ -164,7 +164,9 @@ const comparisonIsEmpty = (comparison: Comparison) =>
|
|||||||
isNotDefined(comparison.value) &&
|
isNotDefined(comparison.value) &&
|
||||||
isNotDefined(comparison.variableId)
|
isNotDefined(comparison.variableId)
|
||||||
|
|
||||||
const parseComparisonOperatorSymbol = (operator: ComparisonOperators) => {
|
const parseComparisonOperatorSymbol = (
|
||||||
|
operator: ComparisonOperators
|
||||||
|
): string => {
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case ComparisonOperators.CONTAINS:
|
case ComparisonOperators.CONTAINS:
|
||||||
return 'contains'
|
return 'contains'
|
||||||
@ -178,5 +180,13 @@ const parseComparisonOperatorSymbol = (operator: ComparisonOperators) => {
|
|||||||
return '<'
|
return '<'
|
||||||
case ComparisonOperators.NOT_EQUAL:
|
case ComparisonOperators.NOT_EQUAL:
|
||||||
return '!='
|
return '!='
|
||||||
|
case ComparisonOperators.ENDS_WITH:
|
||||||
|
return 'ends with'
|
||||||
|
case ComparisonOperators.STARTS_WITH:
|
||||||
|
return 'starts with'
|
||||||
|
case ComparisonOperators.IS_EMPTY:
|
||||||
|
return 'is empty'
|
||||||
|
case ComparisonOperators.NOT_CONTAINS:
|
||||||
|
return 'not contains'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ import { RedirectIcon } from '@/features/blocks/logic/redirect/components/Redire
|
|||||||
import { SetVariableIcon } from '@/features/blocks/logic/setVariable/components/SetVariableIcon'
|
import { SetVariableIcon } from '@/features/blocks/logic/setVariable/components/SetVariableIcon'
|
||||||
import { TypebotLinkIcon } from '@/features/blocks/logic/typebotLink/components/TypebotLinkIcon'
|
import { TypebotLinkIcon } from '@/features/blocks/logic/typebotLink/components/TypebotLinkIcon'
|
||||||
import { AbTestIcon } from '@/features/blocks/logic/abTest/components/AbTestIcon'
|
import { AbTestIcon } from '@/features/blocks/logic/abTest/components/AbTestIcon'
|
||||||
|
import { PictureChoiceIcon } from '@/features/blocks/inputs/pictureChoice/components/PictureChoiceIcon'
|
||||||
|
|
||||||
type BlockIconProps = { type: BlockType } & IconProps
|
type BlockIconProps = { type: BlockType } & IconProps
|
||||||
|
|
||||||
@ -72,6 +73,8 @@ export const BlockIcon = ({ type, ...props }: BlockIconProps): JSX.Element => {
|
|||||||
return <PhoneInputIcon color={orange} {...props} />
|
return <PhoneInputIcon color={orange} {...props} />
|
||||||
case InputBlockType.CHOICE:
|
case InputBlockType.CHOICE:
|
||||||
return <ButtonsInputIcon color={orange} {...props} />
|
return <ButtonsInputIcon color={orange} {...props} />
|
||||||
|
case InputBlockType.PICTURE_CHOICE:
|
||||||
|
return <PictureChoiceIcon color={orange} {...props} />
|
||||||
case InputBlockType.PAYMENT:
|
case InputBlockType.PAYMENT:
|
||||||
return <PaymentInputIcon color={orange} {...props} />
|
return <PaymentInputIcon color={orange} {...props} />
|
||||||
case InputBlockType.RATING:
|
case InputBlockType.RATING:
|
||||||
|
@ -13,69 +13,71 @@ type Props = { type: BlockType }
|
|||||||
export const BlockLabel = ({ type }: Props): JSX.Element => {
|
export const BlockLabel = ({ type }: Props): JSX.Element => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'start':
|
case 'start':
|
||||||
return <Text>Start</Text>
|
return <Text fontSize="sm">Start</Text>
|
||||||
case BubbleBlockType.TEXT:
|
case BubbleBlockType.TEXT:
|
||||||
case InputBlockType.TEXT:
|
case InputBlockType.TEXT:
|
||||||
return <Text>Text</Text>
|
return <Text fontSize="sm">Text</Text>
|
||||||
case BubbleBlockType.IMAGE:
|
case BubbleBlockType.IMAGE:
|
||||||
return <Text>Image</Text>
|
return <Text fontSize="sm">Image</Text>
|
||||||
case BubbleBlockType.VIDEO:
|
case BubbleBlockType.VIDEO:
|
||||||
return <Text>Video</Text>
|
return <Text fontSize="sm">Video</Text>
|
||||||
case BubbleBlockType.EMBED:
|
case BubbleBlockType.EMBED:
|
||||||
return <Text>Embed</Text>
|
return <Text fontSize="sm">Embed</Text>
|
||||||
case BubbleBlockType.AUDIO:
|
case BubbleBlockType.AUDIO:
|
||||||
return <Text>Audio</Text>
|
return <Text fontSize="sm">Audio</Text>
|
||||||
case InputBlockType.NUMBER:
|
case InputBlockType.NUMBER:
|
||||||
return <Text>Number</Text>
|
return <Text fontSize="sm">Number</Text>
|
||||||
case InputBlockType.EMAIL:
|
case InputBlockType.EMAIL:
|
||||||
return <Text>Email</Text>
|
return <Text fontSize="sm">Email</Text>
|
||||||
case InputBlockType.URL:
|
case InputBlockType.URL:
|
||||||
return <Text>Website</Text>
|
return <Text fontSize="sm">Website</Text>
|
||||||
case InputBlockType.DATE:
|
case InputBlockType.DATE:
|
||||||
return <Text>Date</Text>
|
return <Text fontSize="sm">Date</Text>
|
||||||
case InputBlockType.PHONE:
|
case InputBlockType.PHONE:
|
||||||
return <Text>Phone</Text>
|
return <Text fontSize="sm">Phone</Text>
|
||||||
case InputBlockType.CHOICE:
|
case InputBlockType.CHOICE:
|
||||||
return <Text>Button</Text>
|
return <Text fontSize="sm">Button</Text>
|
||||||
|
case InputBlockType.PICTURE_CHOICE:
|
||||||
|
return <Text fontSize="sm">Pic choice</Text>
|
||||||
case InputBlockType.PAYMENT:
|
case InputBlockType.PAYMENT:
|
||||||
return <Text>Payment</Text>
|
return <Text fontSize="sm">Payment</Text>
|
||||||
case InputBlockType.RATING:
|
case InputBlockType.RATING:
|
||||||
return <Text>Rating</Text>
|
return <Text fontSize="sm">Rating</Text>
|
||||||
case InputBlockType.FILE:
|
case InputBlockType.FILE:
|
||||||
return <Text>File</Text>
|
return <Text fontSize="sm">File</Text>
|
||||||
case LogicBlockType.SET_VARIABLE:
|
case LogicBlockType.SET_VARIABLE:
|
||||||
return <Text>Set variable</Text>
|
return <Text fontSize="sm">Set variable</Text>
|
||||||
case LogicBlockType.CONDITION:
|
case LogicBlockType.CONDITION:
|
||||||
return <Text>Condition</Text>
|
return <Text fontSize="sm">Condition</Text>
|
||||||
case LogicBlockType.REDIRECT:
|
case LogicBlockType.REDIRECT:
|
||||||
return <Text>Redirect</Text>
|
return <Text fontSize="sm">Redirect</Text>
|
||||||
case LogicBlockType.SCRIPT:
|
case LogicBlockType.SCRIPT:
|
||||||
return <Text>Script</Text>
|
return <Text fontSize="sm">Script</Text>
|
||||||
case LogicBlockType.TYPEBOT_LINK:
|
case LogicBlockType.TYPEBOT_LINK:
|
||||||
return <Text>Typebot</Text>
|
return <Text fontSize="sm">Typebot</Text>
|
||||||
case LogicBlockType.WAIT:
|
case LogicBlockType.WAIT:
|
||||||
return <Text>Wait</Text>
|
return <Text fontSize="sm">Wait</Text>
|
||||||
case LogicBlockType.JUMP:
|
case LogicBlockType.JUMP:
|
||||||
return <Text>Jump</Text>
|
return <Text fontSize="sm">Jump</Text>
|
||||||
case LogicBlockType.AB_TEST:
|
case LogicBlockType.AB_TEST:
|
||||||
return <Text>AB Test</Text>
|
return <Text fontSize="sm">AB Test</Text>
|
||||||
case IntegrationBlockType.GOOGLE_SHEETS:
|
case IntegrationBlockType.GOOGLE_SHEETS:
|
||||||
return <Text>Sheets</Text>
|
return <Text fontSize="sm">Sheets</Text>
|
||||||
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
||||||
return <Text>Analytics</Text>
|
return <Text fontSize="sm">Analytics</Text>
|
||||||
case IntegrationBlockType.WEBHOOK:
|
case IntegrationBlockType.WEBHOOK:
|
||||||
return <Text>Webhook</Text>
|
return <Text fontSize="sm">Webhook</Text>
|
||||||
case IntegrationBlockType.ZAPIER:
|
case IntegrationBlockType.ZAPIER:
|
||||||
return <Text>Zapier</Text>
|
return <Text fontSize="sm">Zapier</Text>
|
||||||
case IntegrationBlockType.MAKE_COM:
|
case IntegrationBlockType.MAKE_COM:
|
||||||
return <Text>Make.com</Text>
|
return <Text fontSize="sm">Make.com</Text>
|
||||||
case IntegrationBlockType.PABBLY_CONNECT:
|
case IntegrationBlockType.PABBLY_CONNECT:
|
||||||
return <Text>Pabbly</Text>
|
return <Text fontSize="sm">Pabbly</Text>
|
||||||
case IntegrationBlockType.EMAIL:
|
case IntegrationBlockType.EMAIL:
|
||||||
return <Text>Email</Text>
|
return <Text fontSize="sm">Email</Text>
|
||||||
case IntegrationBlockType.CHATWOOT:
|
case IntegrationBlockType.CHATWOOT:
|
||||||
return <Text>Chatwoot</Text>
|
return <Text fontSize="sm">Chatwoot</Text>
|
||||||
case IntegrationBlockType.OPEN_AI:
|
case IntegrationBlockType.OPEN_AI:
|
||||||
return <Text>OpenAI</Text>
|
return <Text fontSize="sm">OpenAI</Text>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,24 +3,24 @@ import {
|
|||||||
Item,
|
Item,
|
||||||
BlockWithItems,
|
BlockWithItems,
|
||||||
defaultConditionContent,
|
defaultConditionContent,
|
||||||
ItemType,
|
|
||||||
Block,
|
Block,
|
||||||
LogicBlockType,
|
LogicBlockType,
|
||||||
InputBlockType,
|
InputBlockType,
|
||||||
ConditionItem,
|
ConditionItem,
|
||||||
ButtonItem,
|
ButtonItem,
|
||||||
|
PictureChoiceItem,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { SetTypebot } from '../TypebotProvider'
|
import { SetTypebot } from '../TypebotProvider'
|
||||||
import { Draft, produce } from 'immer'
|
import { Draft, produce } from 'immer'
|
||||||
import { cleanUpEdgeDraft } from './edges'
|
import { cleanUpEdgeDraft } from './edges'
|
||||||
import { byId, blockHasItems } from '@typebot.io/lib'
|
import { byId, blockHasItems } from '@typebot.io/lib'
|
||||||
import { createId } from '@paralleldrive/cuid2'
|
import { createId } from '@paralleldrive/cuid2'
|
||||||
|
import { DraggabbleItem } from '@/features/graph/providers/GraphDndProvider'
|
||||||
|
|
||||||
type NewItem = Pick<
|
type NewItem = Pick<DraggabbleItem, 'blockId' | 'outgoingEdgeId' | 'type'> &
|
||||||
ConditionItem | ButtonItem,
|
Partial<DraggabbleItem>
|
||||||
'blockId' | 'outgoingEdgeId' | 'type'
|
|
||||||
> &
|
type BlockWithCreatableItems = Extract<Block, { items: DraggabbleItem[] }>
|
||||||
Partial<ConditionItem | ButtonItem>
|
|
||||||
|
|
||||||
export type ItemsActions = {
|
export type ItemsActions = {
|
||||||
createItem: (item: NewItem, indices: ItemIndices) => void
|
createItem: (item: NewItem, indices: ItemIndices) => void
|
||||||
@ -29,31 +29,40 @@ export type ItemsActions = {
|
|||||||
deleteItem: (indices: ItemIndices) => void
|
deleteItem: (indices: ItemIndices) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const createItem = (block: Draft<Block>, item: NewItem, itemIndex: number) => {
|
const createItem = (
|
||||||
|
block: Draft<BlockWithCreatableItems>,
|
||||||
|
item: NewItem,
|
||||||
|
itemIndex: number
|
||||||
|
): Item => {
|
||||||
switch (block.type) {
|
switch (block.type) {
|
||||||
case LogicBlockType.CONDITION: {
|
case LogicBlockType.CONDITION: {
|
||||||
if (item.type === ItemType.CONDITION) {
|
const baseItem = item as ConditionItem
|
||||||
const newItem = {
|
const newItem = {
|
||||||
...item,
|
...baseItem,
|
||||||
id: 'id' in item && item.id ? item.id : createId(),
|
id: 'id' in item && item.id ? item.id : createId(),
|
||||||
content: item.content ?? defaultConditionContent,
|
content: baseItem.content ?? defaultConditionContent,
|
||||||
}
|
|
||||||
block.items.splice(itemIndex, 0, newItem)
|
|
||||||
return newItem
|
|
||||||
}
|
}
|
||||||
break
|
block.items.splice(itemIndex, 0, newItem)
|
||||||
|
return newItem
|
||||||
}
|
}
|
||||||
case InputBlockType.CHOICE: {
|
case InputBlockType.CHOICE: {
|
||||||
if (item.type === ItemType.BUTTON) {
|
const baseItem = item as ButtonItem
|
||||||
const newItem = {
|
const newItem = {
|
||||||
...item,
|
...baseItem,
|
||||||
id: 'id' in item && item.id ? item.id : createId(),
|
id: 'id' in item && item.id ? item.id : createId(),
|
||||||
content: item.content,
|
content: baseItem.content,
|
||||||
}
|
|
||||||
block.items.splice(itemIndex, 0, newItem)
|
|
||||||
return newItem
|
|
||||||
}
|
}
|
||||||
break
|
block.items.splice(itemIndex, 0, newItem)
|
||||||
|
return newItem
|
||||||
|
}
|
||||||
|
case InputBlockType.PICTURE_CHOICE: {
|
||||||
|
const baseItem = item as PictureChoiceItem
|
||||||
|
const newItem = {
|
||||||
|
...baseItem,
|
||||||
|
id: 'id' in baseItem && item.id ? item.id : createId(),
|
||||||
|
}
|
||||||
|
block.items.splice(itemIndex, 0, newItem)
|
||||||
|
return newItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -65,7 +74,9 @@ const itemsAction = (setTypebot: SetTypebot): ItemsActions => ({
|
|||||||
) =>
|
) =>
|
||||||
setTypebot((typebot) =>
|
setTypebot((typebot) =>
|
||||||
produce(typebot, (typebot) => {
|
produce(typebot, (typebot) => {
|
||||||
const block = typebot.groups[groupIndex].blocks[blockIndex]
|
const block = typebot.groups[groupIndex].blocks[
|
||||||
|
blockIndex
|
||||||
|
] as BlockWithCreatableItems
|
||||||
|
|
||||||
const newItem = createItem(block, item, itemIndex)
|
const newItem = createItem(block, item, itemIndex)
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ import { ItemNodesList } from '../item/ItemNodesList'
|
|||||||
import { GoogleAnalyticsNodeBody } from '@/features/blocks/integrations/googleAnalytics/components/GoogleAnalyticsNodeBody'
|
import { GoogleAnalyticsNodeBody } from '@/features/blocks/integrations/googleAnalytics/components/GoogleAnalyticsNodeBody'
|
||||||
import { ChatwootNodeBody } from '@/features/blocks/integrations/chatwoot/components/ChatwootNodeBody'
|
import { ChatwootNodeBody } from '@/features/blocks/integrations/chatwoot/components/ChatwootNodeBody'
|
||||||
import { AbTestNodeBody } from '@/features/blocks/logic/abTest/components/AbTestNodeBody'
|
import { AbTestNodeBody } from '@/features/blocks/logic/abTest/components/AbTestNodeBody'
|
||||||
|
import { PictureChoiceNode } from '@/features/blocks/inputs/pictureChoice/components/PictureChoiceNode'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: Block | StartBlock
|
block: Block | StartBlock
|
||||||
@ -98,6 +99,9 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
|||||||
case InputBlockType.CHOICE: {
|
case InputBlockType.CHOICE: {
|
||||||
return <ButtonsBlockNode block={block} indices={indices} />
|
return <ButtonsBlockNode block={block} indices={indices} />
|
||||||
}
|
}
|
||||||
|
case InputBlockType.PICTURE_CHOICE: {
|
||||||
|
return <PictureChoiceNode block={block} indices={indices} />
|
||||||
|
}
|
||||||
case InputBlockType.PHONE: {
|
case InputBlockType.PHONE: {
|
||||||
return (
|
return (
|
||||||
<PhoneNodeContent
|
<PhoneNodeContent
|
||||||
|
@ -30,7 +30,7 @@ export const HelpDocButton = ({ blockType }: HelpDocButtonProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getHelpDocUrl = (blockType: BlockWithOptions['type']): string | null => {
|
const getHelpDocUrl = (blockType: BlockWithOptions['type']): string => {
|
||||||
switch (blockType) {
|
switch (blockType) {
|
||||||
case LogicBlockType.TYPEBOT_LINK:
|
case LogicBlockType.TYPEBOT_LINK:
|
||||||
return 'https://docs.typebot.io/editor/blocks/logic/typebot-link'
|
return 'https://docs.typebot.io/editor/blocks/logic/typebot-link'
|
||||||
@ -76,7 +76,15 @@ const getHelpDocUrl = (blockType: BlockWithOptions['type']): string | null => {
|
|||||||
return 'https://docs.typebot.io/editor/blocks/integrations/pabbly'
|
return 'https://docs.typebot.io/editor/blocks/integrations/pabbly'
|
||||||
case IntegrationBlockType.WEBHOOK:
|
case IntegrationBlockType.WEBHOOK:
|
||||||
return 'https://docs.typebot.io/editor/blocks/integrations/webhook'
|
return 'https://docs.typebot.io/editor/blocks/integrations/webhook'
|
||||||
default:
|
case InputBlockType.PICTURE_CHOICE:
|
||||||
return null
|
return 'https://docs.typebot.io/editor/blocks/inputs/picture-choice'
|
||||||
|
case IntegrationBlockType.OPEN_AI:
|
||||||
|
return 'https://docs.typebot.io/editor/blocks/integrations/openai'
|
||||||
|
case IntegrationBlockType.MAKE_COM:
|
||||||
|
return 'https://docs.typebot.io/editor/blocks/integrations/make-com'
|
||||||
|
case LogicBlockType.AB_TEST:
|
||||||
|
return 'https://docs.typebot.io/editor/blocks/logic/abTest'
|
||||||
|
case LogicBlockType.JUMP:
|
||||||
|
return 'https://docs.typebot.io/editor/blocks/logic/jump'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,7 @@ import { PhoneInputSettings } from '@/features/blocks/inputs/phone/components/Ph
|
|||||||
import { GoogleSheetsSettings } from '@/features/blocks/integrations/googleSheets/components/GoogleSheetsSettings'
|
import { GoogleSheetsSettings } from '@/features/blocks/integrations/googleSheets/components/GoogleSheetsSettings'
|
||||||
import { ChatwootSettings } from '@/features/blocks/integrations/chatwoot/components/ChatwootSettings'
|
import { ChatwootSettings } from '@/features/blocks/integrations/chatwoot/components/ChatwootSettings'
|
||||||
import { AbTestSettings } from '@/features/blocks/logic/abTest/components/AbTestSettings'
|
import { AbTestSettings } from '@/features/blocks/logic/abTest/components/AbTestSettings'
|
||||||
|
import { PictureChoiceSettings } from '@/features/blocks/inputs/pictureChoice/components/PictureChoiceSettings'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: BlockWithOptions
|
block: BlockWithOptions
|
||||||
@ -160,6 +161,14 @@ export const BlockSettings = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
case InputBlockType.PICTURE_CHOICE: {
|
||||||
|
return (
|
||||||
|
<PictureChoiceSettings
|
||||||
|
options={block.options}
|
||||||
|
onOptionsChange={updateOptions}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
case InputBlockType.PAYMENT: {
|
case InputBlockType.PAYMENT: {
|
||||||
return (
|
return (
|
||||||
<PaymentSettings
|
<PaymentSettings
|
||||||
|
@ -109,6 +109,7 @@ export const ItemNode = ({
|
|||||||
}}
|
}}
|
||||||
pos="absolute"
|
pos="absolute"
|
||||||
right="-49px"
|
right="-49px"
|
||||||
|
bottom="9px"
|
||||||
pointerEvents="all"
|
pointerEvents="all"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ButtonsItemNode } from '@/features/blocks/inputs/buttons/components/ButtonsItemNode'
|
import { ButtonsItemNode } from '@/features/blocks/inputs/buttons/components/ButtonsItemNode'
|
||||||
|
import { PictureChoiceItemNode } from '@/features/blocks/inputs/pictureChoice/components/PictureChoiceItemNode'
|
||||||
import { ConditionItemNode } from '@/features/blocks/logic/condition/components/ConditionItemNode'
|
import { ConditionItemNode } from '@/features/blocks/logic/condition/components/ConditionItemNode'
|
||||||
import { Item, ItemIndices, ItemType } from '@typebot.io/schemas'
|
import { Item, ItemIndices, ItemType } from '@typebot.io/schemas'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
@ -9,7 +10,11 @@ type Props = {
|
|||||||
isMouseOver: boolean
|
isMouseOver: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ItemNodeContent = ({ item, indices, isMouseOver }: Props) => {
|
export const ItemNodeContent = ({
|
||||||
|
item,
|
||||||
|
indices,
|
||||||
|
isMouseOver,
|
||||||
|
}: Props): JSX.Element => {
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
case ItemType.BUTTON:
|
case ItemType.BUTTON:
|
||||||
return (
|
return (
|
||||||
@ -20,6 +25,14 @@ export const ItemNodeContent = ({ item, indices, isMouseOver }: Props) => {
|
|||||||
indices={indices}
|
indices={indices}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
case ItemType.PICTURE_CHOICE:
|
||||||
|
return (
|
||||||
|
<PictureChoiceItemNode
|
||||||
|
item={item}
|
||||||
|
isMouseOver={isMouseOver}
|
||||||
|
indices={indices}
|
||||||
|
/>
|
||||||
|
)
|
||||||
case ItemType.CONDITION:
|
case ItemType.CONDITION:
|
||||||
return (
|
return (
|
||||||
<ConditionItemNode
|
<ConditionItemNode
|
||||||
|
@ -75,7 +75,7 @@ export const ItemNodesList = ({
|
|||||||
}, [block.id, mouseOverBlock?.id, showPlaceholders])
|
}, [block.id, mouseOverBlock?.id, showPlaceholders])
|
||||||
|
|
||||||
const handleMouseMoveOnBlock = (event: MouseEvent) => {
|
const handleMouseMoveOnBlock = (event: MouseEvent) => {
|
||||||
if (!isDraggingOnCurrentBlock) return
|
if (!isDraggingOnCurrentBlock || !showPlaceholders) return
|
||||||
const index = computeNearestPlaceholderIndex(event.pageY, placeholderRefs)
|
const index = computeNearestPlaceholderIndex(event.pageY, placeholderRefs)
|
||||||
setExpandedPlaceholderIndex(index)
|
setExpandedPlaceholderIndex(index)
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ test.describe.parallel('Settings page', () => {
|
|||||||
await expect(favIconImg).toHaveAttribute('src', '/favicon.png')
|
await expect(favIconImg).toHaveAttribute('src', '/favicon.png')
|
||||||
await favIconImg.click()
|
await favIconImg.click()
|
||||||
await expect(page.locator('text=Giphy')).toBeHidden()
|
await expect(page.locator('text=Giphy')).toBeHidden()
|
||||||
await page.click('button:has-text("Embed link")')
|
await page.click('button:has-text("Link")')
|
||||||
await page.fill(
|
await page.fill(
|
||||||
'input[placeholder="Paste the image link..."]',
|
'input[placeholder="Paste the image link..."]',
|
||||||
favIconUrl
|
favIconUrl
|
||||||
@ -92,7 +92,7 @@ test.describe.parallel('Settings page', () => {
|
|||||||
await expect(websiteImg).toHaveAttribute('src', '/viewer-preview.png')
|
await expect(websiteImg).toHaveAttribute('src', '/viewer-preview.png')
|
||||||
await websiteImg.click()
|
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="Link"')
|
||||||
await page.fill('input[placeholder="Paste the image link..."]', imageUrl)
|
await page.fill('input[placeholder="Paste the image link..."]', imageUrl)
|
||||||
await expect(websiteImg).toHaveAttribute('src', imageUrl)
|
await expect(websiteImg).toHaveAttribute('src', imageUrl)
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ test.describe.parallel('Theme page', () => {
|
|||||||
// Host avatar
|
// Host avatar
|
||||||
await expect(page.locator('[data-testid="default-avatar"]')).toBeVisible()
|
await expect(page.locator('[data-testid="default-avatar"]')).toBeVisible()
|
||||||
await page.click('[data-testid="default-avatar"]')
|
await page.click('[data-testid="default-avatar"]')
|
||||||
await page.click('button:has-text("Embed link")')
|
await page.click('button:has-text("Link")')
|
||||||
await page.fill(
|
await page.fill(
|
||||||
'input[placeholder="Paste the image link..."]',
|
'input[placeholder="Paste the image link..."]',
|
||||||
hostAvatarUrl
|
hostAvatarUrl
|
||||||
@ -169,7 +169,7 @@ test.describe.parallel('Theme page', () => {
|
|||||||
page.locator('[data-testid="default-avatar"] >> nth=-1')
|
page.locator('[data-testid="default-avatar"] >> nth=-1')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
await page.click('[data-testid="default-avatar"]')
|
await page.click('[data-testid="default-avatar"]')
|
||||||
await page.click('button:has-text("Embed link")')
|
await page.click('button:has-text("Link")')
|
||||||
await page
|
await page
|
||||||
.locator('input[placeholder="Paste the image link..."]')
|
.locator('input[placeholder="Paste the image link..."]')
|
||||||
.fill(guestAvatarUrl)
|
.fill(guestAvatarUrl)
|
||||||
|
@ -1,9 +1,18 @@
|
|||||||
import { isChoiceInput, isConditionBlock, isDefined } from '@typebot.io/lib'
|
import {
|
||||||
|
isChoiceInput,
|
||||||
|
isConditionBlock,
|
||||||
|
isDefined,
|
||||||
|
isPictureChoiceInput,
|
||||||
|
} from '@typebot.io/lib'
|
||||||
import { Block, InputBlockType, LogicBlockType } from '@typebot.io/schemas'
|
import { Block, InputBlockType, LogicBlockType } from '@typebot.io/schemas'
|
||||||
|
|
||||||
export const hasDefaultConnector = (block: Block) =>
|
export const hasDefaultConnector = (block: Block) =>
|
||||||
(!isChoiceInput(block) &&
|
(!isChoiceInput(block) &&
|
||||||
|
!isPictureChoiceInput(block) &&
|
||||||
!isConditionBlock(block) &&
|
!isConditionBlock(block) &&
|
||||||
block.type !== LogicBlockType.AB_TEST) ||
|
block.type !== LogicBlockType.AB_TEST) ||
|
||||||
(block.type === InputBlockType.CHOICE &&
|
(block.type === InputBlockType.CHOICE &&
|
||||||
isDefined(block.options.dynamicVariableId))
|
isDefined(block.options.dynamicVariableId)) ||
|
||||||
|
(block.type === InputBlockType.PICTURE_CHOICE &&
|
||||||
|
block.options.dynamicItems?.isEnabled &&
|
||||||
|
block.options.dynamicItems?.pictureSrcsVariableId)
|
||||||
|
@ -43,18 +43,19 @@ import {
|
|||||||
ItemType,
|
ItemType,
|
||||||
LogicBlockType,
|
LogicBlockType,
|
||||||
defaultAbTestOptions,
|
defaultAbTestOptions,
|
||||||
|
BlockWithItems,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
|
import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
|
||||||
|
|
||||||
const parseDefaultItems = (
|
const parseDefaultItems = (
|
||||||
type:
|
type: BlockWithItems['type'],
|
||||||
| LogicBlockType.CONDITION
|
|
||||||
| InputBlockType.CHOICE
|
|
||||||
| LogicBlockType.AB_TEST,
|
|
||||||
blockId: string
|
blockId: string
|
||||||
): Item[] => {
|
): Item[] => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case InputBlockType.CHOICE:
|
case InputBlockType.CHOICE:
|
||||||
return [{ id: createId(), blockId, type: ItemType.BUTTON }]
|
return [{ id: createId(), blockId, type: ItemType.BUTTON }]
|
||||||
|
case InputBlockType.PICTURE_CHOICE:
|
||||||
|
return [{ id: createId(), blockId, type: ItemType.PICTURE_CHOICE }]
|
||||||
case LogicBlockType.CONDITION:
|
case LogicBlockType.CONDITION:
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -103,6 +104,8 @@ const parseDefaultBlockOptions = (type: BlockWithOptionsType): BlockOptions => {
|
|||||||
return defaultUrlInputOptions
|
return defaultUrlInputOptions
|
||||||
case InputBlockType.CHOICE:
|
case InputBlockType.CHOICE:
|
||||||
return defaultChoiceInputOptions
|
return defaultChoiceInputOptions
|
||||||
|
case InputBlockType.PICTURE_CHOICE:
|
||||||
|
return defaultPictureChoiceOptions
|
||||||
case InputBlockType.PAYMENT:
|
case InputBlockType.PAYMENT:
|
||||||
return defaultPaymentInputOptions
|
return defaultPaymentInputOptions
|
||||||
case InputBlockType.RATING:
|
case InputBlockType.RATING:
|
||||||
|
11
apps/docs/docs/editor/blocks/inputs/picture-choice.md
Normal file
11
apps/docs/docs/editor/blocks/inputs/picture-choice.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Picture choice
|
||||||
|
|
||||||
|
The Picture choice input block allows you to offer your user predefined choices illustrated with a picture, either single choice options or multiple choices
|
||||||
|
|
||||||
|
<img
|
||||||
|
src="/img/blocks/inputs/picture-choice.png"
|
||||||
|
width="100%"
|
||||||
|
alt="Picture choice overview"
|
||||||
|
/>
|
||||||
|
|
||||||
|
For advanced configuration, check out the [Buttons block](./buttons) documentation. It works the same way.
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
BIN
apps/docs/static/img/blocks/inputs/picture-choice.png
vendored
Normal file
BIN
apps/docs/static/img/blocks/inputs/picture-choice.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 MiB |
@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
SessionState,
|
||||||
|
VariableWithValue,
|
||||||
|
ItemType,
|
||||||
|
PictureChoiceBlock,
|
||||||
|
} from '@typebot.io/schemas'
|
||||||
|
import { isDefined } from '@typebot.io/lib'
|
||||||
|
import { deepParseVariables } from '@/features/variables/deepParseVariable'
|
||||||
|
|
||||||
|
export const injectVariableValuesInPictureChoiceBlock =
|
||||||
|
(variables: SessionState['typebot']['variables']) =>
|
||||||
|
(block: PictureChoiceBlock): PictureChoiceBlock => {
|
||||||
|
if (
|
||||||
|
block.options.dynamicItems?.isEnabled &&
|
||||||
|
block.options.dynamicItems.pictureSrcsVariableId
|
||||||
|
) {
|
||||||
|
const pictureSrcsVariable = variables.find(
|
||||||
|
(variable) =>
|
||||||
|
variable.id === block.options.dynamicItems?.pictureSrcsVariableId &&
|
||||||
|
isDefined(variable.value)
|
||||||
|
) as VariableWithValue | undefined
|
||||||
|
if (!pictureSrcsVariable || typeof pictureSrcsVariable.value === 'string')
|
||||||
|
return block
|
||||||
|
const titlesVariable = block.options.dynamicItems.titlesVariableId
|
||||||
|
? (variables.find(
|
||||||
|
(variable) =>
|
||||||
|
variable.id === block.options.dynamicItems?.titlesVariableId &&
|
||||||
|
isDefined(variable.value)
|
||||||
|
) as VariableWithValue | undefined)
|
||||||
|
: undefined
|
||||||
|
const descriptionsVariable = block.options.dynamicItems
|
||||||
|
.descriptionsVariableId
|
||||||
|
? (variables.find(
|
||||||
|
(variable) =>
|
||||||
|
variable.id ===
|
||||||
|
block.options.dynamicItems?.descriptionsVariableId &&
|
||||||
|
isDefined(variable.value)
|
||||||
|
) as VariableWithValue | undefined)
|
||||||
|
: undefined
|
||||||
|
return {
|
||||||
|
...block,
|
||||||
|
items: pictureSrcsVariable.value
|
||||||
|
.filter(isDefined)
|
||||||
|
.map((pictureSrc, idx) => ({
|
||||||
|
id: idx.toString(),
|
||||||
|
type: ItemType.PICTURE_CHOICE,
|
||||||
|
blockId: block.id,
|
||||||
|
pictureSrc,
|
||||||
|
title: titlesVariable?.value?.[idx] ?? '',
|
||||||
|
description: descriptionsVariable?.value?.[idx] ?? '',
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deepParseVariables(variables)(block)
|
||||||
|
}
|
@ -21,6 +21,7 @@ import { executeIntegration } from './executeIntegration'
|
|||||||
import { injectVariableValuesInButtonsInputBlock } from '@/features/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock'
|
import { injectVariableValuesInButtonsInputBlock } from '@/features/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock'
|
||||||
import { deepParseVariables } from '@/features/variables/deepParseVariable'
|
import { deepParseVariables } from '@/features/variables/deepParseVariable'
|
||||||
import { computePaymentInputRuntimeOptions } from '@/features/blocks/inputs/payment/computePaymentInputRuntimeOptions'
|
import { computePaymentInputRuntimeOptions } from '@/features/blocks/inputs/payment/computePaymentInputRuntimeOptions'
|
||||||
|
import { injectVariableValuesInPictureChoiceBlock } from '@/features/blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock'
|
||||||
|
|
||||||
export const executeGroup =
|
export const executeGroup =
|
||||||
(
|
(
|
||||||
@ -185,6 +186,11 @@ const injectVariablesValueInBlock =
|
|||||||
block
|
block
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
case InputBlockType.PICTURE_CHOICE: {
|
||||||
|
return injectVariableValuesInPictureChoiceBlock(
|
||||||
|
state.typebot.variables
|
||||||
|
)(block)
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return deepParseVariables(state.typebot.variables)({
|
return deepParseVariables(state.typebot.variables)({
|
||||||
...block,
|
...block,
|
||||||
|
@ -170,5 +170,7 @@ const Input = ({
|
|||||||
return (
|
return (
|
||||||
<FileUploadForm block={block} onSubmit={onSubmit} onSkip={onSkip} />
|
<FileUploadForm block={block} onSubmit={onSubmit} onSkip={onSkip} />
|
||||||
)
|
)
|
||||||
|
default:
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/js",
|
"name": "@typebot.io/js",
|
||||||
"version": "0.0.45",
|
"version": "0.0.46",
|
||||||
"description": "Javascript library to display typebots on your website",
|
"description": "Javascript library to display typebots on your website",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
@ -297,3 +297,40 @@ textarea {
|
|||||||
-ms-overflow-style: none; /* IE and Edge */
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.typebot-picture-button {
|
||||||
|
color: var(--typebot-button-color);
|
||||||
|
background-color: var(--typebot-button-bg-color);
|
||||||
|
border-radius: var(--typebot-border-radius);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
width: 236px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typebot-picture-button > img,
|
||||||
|
.typebot-selectable-picture > img {
|
||||||
|
border-radius: var(--typebot-border-radius) var(--typebot-border-radius) 0 0;
|
||||||
|
min-width: 200px;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 200px;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typebot-selectable-picture {
|
||||||
|
border: 1px solid rgba(var(--typebot-button-bg-color-rgb), 0.25);
|
||||||
|
border-radius: var(--typebot-border-radius);
|
||||||
|
color: var(--typebot-container-color);
|
||||||
|
background-color: rgba(var(--typebot-button-bg-color-rgb), 0.08);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
width: 236px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typebot-selectable-picture:hover {
|
||||||
|
background-color: rgba(var(--typebot-button-bg-color-rgb), 0.12);
|
||||||
|
border-color: rgba(var(--typebot-button-bg-color-rgb), 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typebot-selectable-picture.selected {
|
||||||
|
background-color: rgba(var(--typebot-button-bg-color-rgb), 0.18);
|
||||||
|
border-color: rgba(var(--typebot-button-bg-color-rgb), 0.35);
|
||||||
|
}
|
||||||
|
@ -12,6 +12,7 @@ import type {
|
|||||||
TextInputBlock,
|
TextInputBlock,
|
||||||
Theme,
|
Theme,
|
||||||
UrlInputBlock,
|
UrlInputBlock,
|
||||||
|
PictureChoiceBlock,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/enums'
|
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/enums'
|
||||||
import { GuestBubble } from './bubbles/GuestBubble'
|
import { GuestBubble } from './bubbles/GuestBubble'
|
||||||
@ -30,8 +31,8 @@ import { isMobile } from '@/utils/isMobileSignal'
|
|||||||
import { PaymentForm } from '@/features/blocks/inputs/payment'
|
import { PaymentForm } from '@/features/blocks/inputs/payment'
|
||||||
import { MultipleChoicesForm } from '@/features/blocks/inputs/buttons/components/MultipleChoicesForm'
|
import { MultipleChoicesForm } from '@/features/blocks/inputs/buttons/components/MultipleChoicesForm'
|
||||||
import { Buttons } from '@/features/blocks/inputs/buttons/components/Buttons'
|
import { Buttons } from '@/features/blocks/inputs/buttons/components/Buttons'
|
||||||
import { SearchableButtons } from '@/features/blocks/inputs/buttons/components/SearchableButtons'
|
import { SinglePictureChoice } from '@/features/blocks/inputs/pictureChoice/SinglePictureChoice'
|
||||||
import { SearchableMultipleChoicesForm } from '@/features/blocks/inputs/buttons/components/SearchableMultipleChoicesForm'
|
import { MultiplePictureChoice } from '@/features/blocks/inputs/pictureChoice/MultiplePictureChoice'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: NonNullable<ChatReply['input']>
|
block: NonNullable<ChatReply['input']>
|
||||||
@ -165,42 +166,40 @@ const Input = (props: {
|
|||||||
{(block) => (
|
{(block) => (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={!block.options.isMultipleChoice}>
|
<Match when={!block.options.isMultipleChoice}>
|
||||||
<Switch>
|
<Buttons
|
||||||
<Match when={block.options.isSearchable}>
|
inputIndex={props.inputIndex}
|
||||||
<SearchableButtons
|
defaultItems={block.items}
|
||||||
inputIndex={props.inputIndex}
|
options={block.options}
|
||||||
defaultItems={block.items}
|
onSubmit={onSubmit}
|
||||||
onSubmit={onSubmit}
|
/>
|
||||||
/>
|
|
||||||
</Match>
|
|
||||||
<Match when={!block.options.isSearchable}>
|
|
||||||
<Buttons
|
|
||||||
inputIndex={props.inputIndex}
|
|
||||||
items={block.items}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
/>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={block.options.isMultipleChoice}>
|
<Match when={block.options.isMultipleChoice}>
|
||||||
<Switch>
|
<MultipleChoicesForm
|
||||||
<Match when={block.options.isSearchable}>
|
inputIndex={props.inputIndex}
|
||||||
<SearchableMultipleChoicesForm
|
defaultItems={block.items}
|
||||||
inputIndex={props.inputIndex}
|
options={block.options}
|
||||||
defaultItems={block.items}
|
onSubmit={onSubmit}
|
||||||
options={block.options}
|
/>
|
||||||
onSubmit={onSubmit}
|
</Match>
|
||||||
/>
|
</Switch>
|
||||||
</Match>
|
)}
|
||||||
<Match when={!block.options.isSearchable}>
|
</Match>
|
||||||
<MultipleChoicesForm
|
<Match when={isPictureChoiceBlock(props.block)} keyed>
|
||||||
inputIndex={props.inputIndex}
|
{(block) => (
|
||||||
items={block.items}
|
<Switch>
|
||||||
options={block.options}
|
<Match when={!block.options.isMultipleChoice}>
|
||||||
onSubmit={onSubmit}
|
<SinglePictureChoice
|
||||||
/>
|
defaultItems={block.items}
|
||||||
</Match>
|
options={block.options}
|
||||||
</Switch>
|
onSubmit={onSubmit}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={block.options.isMultipleChoice}>
|
||||||
|
<MultiplePictureChoice
|
||||||
|
defaultItems={block.items}
|
||||||
|
options={block.options}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
)}
|
)}
|
||||||
@ -240,3 +239,8 @@ const isButtonsBlock = (
|
|||||||
block: ChatReply['input']
|
block: ChatReply['input']
|
||||||
): ChoiceInputBlock | undefined =>
|
): ChoiceInputBlock | undefined =>
|
||||||
block?.type === InputBlockType.CHOICE ? block : undefined
|
block?.type === InputBlockType.CHOICE ? block : undefined
|
||||||
|
|
||||||
|
const isPictureChoiceBlock = (
|
||||||
|
block: ChatReply['input']
|
||||||
|
): PictureChoiceBlock | undefined =>
|
||||||
|
block?.type === InputBlockType.PICTURE_CHOICE ? block : undefined
|
||||||
|
@ -1,41 +1,82 @@
|
|||||||
import { Button } from '@/components/Button'
|
import { Button } from '@/components/Button'
|
||||||
|
import { SearchInput } from '@/components/inputs/SearchInput'
|
||||||
import { InputSubmitContent } from '@/types'
|
import { InputSubmitContent } from '@/types'
|
||||||
import { isMobile } from '@/utils/isMobileSignal'
|
import { isMobile } from '@/utils/isMobileSignal'
|
||||||
import type { ChoiceInputBlock } from '@typebot.io/schemas'
|
import type { ChoiceInputBlock } from '@typebot.io/schemas'
|
||||||
import { For } from 'solid-js'
|
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice'
|
||||||
|
import { For, Show, createSignal, onMount } from 'solid-js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
inputIndex: number
|
inputIndex: number
|
||||||
items: ChoiceInputBlock['items']
|
defaultItems: ChoiceInputBlock['items']
|
||||||
|
options: ChoiceInputBlock['options']
|
||||||
onSubmit: (value: InputSubmitContent) => void
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Buttons = (props: Props) => {
|
export const Buttons = (props: Props) => {
|
||||||
|
let inputRef: HTMLInputElement | undefined
|
||||||
|
const [filteredItems, setFilteredItems] = createSignal(props.defaultItems)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!isMobile() && inputRef) inputRef.focus()
|
||||||
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line solid/reactivity
|
// eslint-disable-next-line solid/reactivity
|
||||||
const handleClick = (itemIndex: number) => () =>
|
const handleClick = (itemIndex: number) => () =>
|
||||||
props.onSubmit({ value: props.items[itemIndex].content ?? '' })
|
props.onSubmit({ value: filteredItems()[itemIndex].content ?? '' })
|
||||||
|
|
||||||
|
const filterItems = (inputValue: string) => {
|
||||||
|
setFilteredItems(
|
||||||
|
props.defaultItems.filter((item) =>
|
||||||
|
item.content?.toLowerCase().includes((inputValue ?? '').toLowerCase())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-wrap justify-end gap-2">
|
<div class="flex flex-col gap-2 w-full">
|
||||||
<For each={props.items}>
|
<Show when={props.options.isSearchable}>
|
||||||
{(item, index) => (
|
<div class="flex items-end typebot-input w-full">
|
||||||
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
<SearchInput
|
||||||
<Button
|
ref={inputRef}
|
||||||
on:click={handleClick(index())}
|
onInput={filterItems}
|
||||||
data-itemid={item.id}
|
placeholder={
|
||||||
class="w-full"
|
props.options.searchInputPlaceholder ??
|
||||||
>
|
defaultChoiceInputOptions.searchInputPlaceholder
|
||||||
{item.content}
|
}
|
||||||
</Button>
|
onClear={() => setFilteredItems(props.defaultItems)}
|
||||||
{props.inputIndex === 0 && props.items.length === 1 && (
|
/>
|
||||||
<span class="flex h-3 w-3 absolute top-0 right-0 -mt-1 -mr-1 ping">
|
</div>
|
||||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full brightness-200 opacity-75" />
|
</Show>
|
||||||
<span class="relative inline-flex rounded-full h-3 w-3 brightness-150" />
|
|
||||||
</span>
|
<div
|
||||||
)}
|
class={
|
||||||
</span>
|
'flex flex-wrap justify-end gap-2' +
|
||||||
)}
|
(props.options.isSearchable
|
||||||
</For>
|
? ' overflow-y-scroll max-h-80 rounded-md hide-scrollbar'
|
||||||
|
: '')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For each={filteredItems()}>
|
||||||
|
{(item, index) => (
|
||||||
|
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
||||||
|
<Button
|
||||||
|
on:click={handleClick(index())}
|
||||||
|
data-itemid={item.id}
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
{item.content}
|
||||||
|
</Button>
|
||||||
|
{props.inputIndex === 0 && props.defaultItems.length === 1 && (
|
||||||
|
<span class="flex h-3 w-3 absolute top-0 right-0 -mt-1 -mr-1 ping">
|
||||||
|
<span class="animate-ping absolute inline-flex h-full w-full rounded-full brightness-200 opacity-75" />
|
||||||
|
<span class="relative inline-flex rounded-full h-3 w-3 brightness-150" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,17 @@ import { Show } from 'solid-js'
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isChecked: boolean
|
isChecked: boolean
|
||||||
|
class?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Checkbox = (props: Props) => {
|
export const Checkbox = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={'w-4 h-4 typebot-checkbox' + (props.isChecked ? ' checked' : '')}
|
class={
|
||||||
|
'w-4 h-4 typebot-checkbox' +
|
||||||
|
(props.isChecked ? ' checked' : '') +
|
||||||
|
(props.class ? ` ${props.class}` : '')
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Show when={props.isChecked}>
|
<Show when={props.isChecked}>
|
||||||
<CheckIcon />
|
<CheckIcon />
|
||||||
|
@ -1,58 +1,98 @@
|
|||||||
import { SendButton } from '@/components/SendButton'
|
import { SendButton } from '@/components/SendButton'
|
||||||
import { InputSubmitContent } from '@/types'
|
import { InputSubmitContent } from '@/types'
|
||||||
import { isMobile } from '@/utils/isMobileSignal'
|
import { isMobile } from '@/utils/isMobileSignal'
|
||||||
import type { ChoiceInputBlock } from '@typebot.io/schemas'
|
import { ChoiceInputBlock } from '@typebot.io/schemas'
|
||||||
import { createSignal, For } from 'solid-js'
|
import { createSignal, For, onMount, Show } from 'solid-js'
|
||||||
import { Checkbox } from './Checkbox'
|
import { Checkbox } from './Checkbox'
|
||||||
|
import { SearchInput } from '@/components/inputs/SearchInput'
|
||||||
|
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
inputIndex: number
|
inputIndex: number
|
||||||
items: ChoiceInputBlock['items']
|
defaultItems: ChoiceInputBlock['items']
|
||||||
options: ChoiceInputBlock['options']
|
options: ChoiceInputBlock['options']
|
||||||
onSubmit: (value: InputSubmitContent) => void
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MultipleChoicesForm = (props: Props) => {
|
export const MultipleChoicesForm = (props: Props) => {
|
||||||
const [selectedIndices, setSelectedIndices] = createSignal<number[]>([])
|
let inputRef: HTMLInputElement | undefined
|
||||||
|
const [filteredItems, setFilteredItems] = createSignal(props.defaultItems)
|
||||||
|
const [selectedItemIds, setSelectedItemIds] = createSignal<string[]>([])
|
||||||
|
|
||||||
const handleClick = (itemIndex: number) => {
|
onMount(() => {
|
||||||
toggleSelectedItemIndex(itemIndex)
|
if (!isMobile() && inputRef) inputRef.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClick = (itemId: string) => {
|
||||||
|
toggleSelectedItemId(itemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleSelectedItemIndex = (itemIndex: number) => {
|
const toggleSelectedItemId = (itemId: string) => {
|
||||||
const existingIndex = selectedIndices().indexOf(itemIndex)
|
const existingIndex = selectedItemIds().indexOf(itemId)
|
||||||
if (existingIndex !== -1) {
|
if (existingIndex !== -1) {
|
||||||
setSelectedIndices((selectedIndices) =>
|
setSelectedItemIds((selectedItemIds) =>
|
||||||
selectedIndices.filter((index) => index !== itemIndex)
|
selectedItemIds.filter((selectedItemId) => selectedItemId !== itemId)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
setSelectedIndices((selectedIndices) => [...selectedIndices, itemIndex])
|
setSelectedItemIds((selectedIndices) => [...selectedIndices, itemId])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = () =>
|
const handleSubmit = () =>
|
||||||
props.onSubmit({
|
props.onSubmit({
|
||||||
value: selectedIndices()
|
value: selectedItemIds()
|
||||||
.map((itemIndex) => props.items[itemIndex].content)
|
.map(
|
||||||
|
(selectedItemId) =>
|
||||||
|
props.defaultItems.find((item) => item.id === selectedItemId)
|
||||||
|
?.content
|
||||||
|
)
|
||||||
.join(', '),
|
.join(', '),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const filterItems = (inputValue: string) => {
|
||||||
|
setFilteredItems(
|
||||||
|
props.defaultItems.filter((item) =>
|
||||||
|
item.content?.toLowerCase().includes((inputValue ?? '').toLowerCase())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form class="flex flex-col items-end gap-2" onSubmit={handleSubmit}>
|
<form class="flex flex-col items-end gap-2 w-full" onSubmit={handleSubmit}>
|
||||||
<div class="flex flex-wrap justify-end gap-2">
|
<Show when={props.options.isSearchable}>
|
||||||
<For each={props.items}>
|
<div class="flex items-end typebot-input w-full">
|
||||||
{(item, index) => (
|
<SearchInput
|
||||||
|
ref={inputRef}
|
||||||
|
onInput={filterItems}
|
||||||
|
placeholder={
|
||||||
|
props.options.searchInputPlaceholder ??
|
||||||
|
defaultChoiceInputOptions.searchInputPlaceholder
|
||||||
|
}
|
||||||
|
onClear={() => setFilteredItems(props.defaultItems)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div
|
||||||
|
class={
|
||||||
|
'flex flex-wrap justify-end gap-2' +
|
||||||
|
(props.options.isSearchable
|
||||||
|
? ' overflow-y-scroll max-h-80 rounded-md hide-scrollbar'
|
||||||
|
: '')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For each={filteredItems()}>
|
||||||
|
{(item) => (
|
||||||
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
||||||
<div
|
<div
|
||||||
role="checkbox"
|
role="checkbox"
|
||||||
aria-checked={selectedIndices().some(
|
aria-checked={selectedItemIds().some(
|
||||||
(selectedIndex) => selectedIndex === index()
|
(selectedItemId) => selectedItemId === item.id
|
||||||
)}
|
)}
|
||||||
on:click={() => handleClick(index())}
|
on:click={() => handleClick(item.id)}
|
||||||
class={
|
class={
|
||||||
'w-full py-2 px-4 font-semibold focus:outline-none cursor-pointer select-none typebot-selectable' +
|
'w-full py-2 px-4 font-semibold focus:outline-none cursor-pointer select-none typebot-selectable' +
|
||||||
(selectedIndices().some(
|
(selectedItemIds().some(
|
||||||
(selectedIndex) => selectedIndex === index()
|
(selectedItemId) => selectedItemId === item.id
|
||||||
)
|
)
|
||||||
? ' selected'
|
? ' selected'
|
||||||
: '')
|
: '')
|
||||||
@ -61,8 +101,8 @@ export const MultipleChoicesForm = (props: Props) => {
|
|||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
isChecked={selectedIndices().some(
|
isChecked={selectedItemIds().some(
|
||||||
(selectedIndex) => selectedIndex === index()
|
(selectedItemId) => selectedItemId === item.id
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span>{item.content}</span>
|
<span>{item.content}</span>
|
||||||
@ -71,8 +111,38 @@ export const MultipleChoicesForm = (props: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
<For
|
||||||
|
each={selectedItemIds().filter((selectedItemId) =>
|
||||||
|
filteredItems().every((item) => item.id !== selectedItemId)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{(selectedItemId) => (
|
||||||
|
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
||||||
|
<div
|
||||||
|
role="checkbox"
|
||||||
|
aria-checked
|
||||||
|
on:click={() => handleClick(selectedItemId)}
|
||||||
|
class={
|
||||||
|
'w-full py-2 px-4 font-semibold focus:outline-none cursor-pointer select-none typebot-selectable selected'
|
||||||
|
}
|
||||||
|
data-itemid={selectedItemId}
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Checkbox isChecked />
|
||||||
|
<span>
|
||||||
|
{
|
||||||
|
props.defaultItems.find(
|
||||||
|
(item) => item.id === selectedItemId
|
||||||
|
)?.content
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
</div>
|
</div>
|
||||||
{selectedIndices().length > 0 && (
|
{selectedItemIds().length > 0 && (
|
||||||
<SendButton disableIcon>
|
<SendButton disableIcon>
|
||||||
{props.options?.buttonLabel ?? 'Send'}
|
{props.options?.buttonLabel ?? 'Send'}
|
||||||
</SendButton>
|
</SendButton>
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
import { Button } from '@/components/Button'
|
|
||||||
import { SearchInput } from '@/components/inputs/SearchInput'
|
|
||||||
import { InputSubmitContent } from '@/types'
|
|
||||||
import { isMobile } from '@/utils/isMobileSignal'
|
|
||||||
import type { ChoiceInputBlock } from '@typebot.io/schemas'
|
|
||||||
import { For, createSignal, onMount } from 'solid-js'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
inputIndex: number
|
|
||||||
defaultItems: ChoiceInputBlock['items']
|
|
||||||
onSubmit: (value: InputSubmitContent) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SearchableButtons = (props: Props) => {
|
|
||||||
let inputRef: HTMLInputElement | undefined
|
|
||||||
const [filteredItems, setFilteredItems] = createSignal(props.defaultItems)
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (!isMobile() && inputRef) inputRef.focus()
|
|
||||||
})
|
|
||||||
|
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
const handleClick = (itemIndex: number) => () =>
|
|
||||||
props.onSubmit({ value: filteredItems()[itemIndex].content ?? '' })
|
|
||||||
|
|
||||||
const filterItems = (inputValue: string) => {
|
|
||||||
setFilteredItems(
|
|
||||||
props.defaultItems.filter((item) =>
|
|
||||||
item.content?.toLowerCase().includes((inputValue ?? '').toLowerCase())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="flex flex-col gap-2 w-full">
|
|
||||||
<div class="flex items-end typebot-input w-full">
|
|
||||||
<SearchInput
|
|
||||||
ref={inputRef}
|
|
||||||
onInput={filterItems}
|
|
||||||
placeholder="Filter the options..."
|
|
||||||
onClear={() => setFilteredItems(props.defaultItems)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap justify-end gap-2 overflow-y-scroll max-h-80 rounded-md hide-scrollbar">
|
|
||||||
<For each={filteredItems()}>
|
|
||||||
{(item, index) => (
|
|
||||||
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
|
||||||
<Button
|
|
||||||
on:click={handleClick(index())}
|
|
||||||
data-itemid={item.id}
|
|
||||||
class="w-full"
|
|
||||||
>
|
|
||||||
{item.content}
|
|
||||||
</Button>
|
|
||||||
{props.inputIndex === 0 && props.defaultItems.length === 1 && (
|
|
||||||
<span class="flex h-3 w-3 absolute top-0 right-0 -mt-1 -mr-1 ping">
|
|
||||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full brightness-200 opacity-75" />
|
|
||||||
<span class="relative inline-flex rounded-full h-3 w-3 brightness-150" />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,132 +0,0 @@
|
|||||||
import { SendButton } from '@/components/SendButton'
|
|
||||||
import { InputSubmitContent } from '@/types'
|
|
||||||
import { isMobile } from '@/utils/isMobileSignal'
|
|
||||||
import type { ChoiceInputBlock } from '@typebot.io/schemas'
|
|
||||||
import { createSignal, For } from 'solid-js'
|
|
||||||
import { Checkbox } from './Checkbox'
|
|
||||||
import { SearchInput } from '@/components/inputs/SearchInput'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
inputIndex: number
|
|
||||||
defaultItems: ChoiceInputBlock['items']
|
|
||||||
options: ChoiceInputBlock['options']
|
|
||||||
onSubmit: (value: InputSubmitContent) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SearchableMultipleChoicesForm = (props: Props) => {
|
|
||||||
let inputRef: HTMLInputElement | undefined
|
|
||||||
const [filteredItems, setFilteredItems] = createSignal(props.defaultItems)
|
|
||||||
const [selectedItemIds, setSelectedItemIds] = createSignal<string[]>([])
|
|
||||||
|
|
||||||
const handleClick = (itemId: string) => {
|
|
||||||
toggleSelectedItemId(itemId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleSelectedItemId = (itemId: string) => {
|
|
||||||
const existingIndex = selectedItemIds().indexOf(itemId)
|
|
||||||
if (existingIndex !== -1) {
|
|
||||||
setSelectedItemIds((selectedItemIds) =>
|
|
||||||
selectedItemIds.filter((selectedItemId) => selectedItemId !== itemId)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
setSelectedItemIds((selectedIndices) => [...selectedIndices, itemId])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = () =>
|
|
||||||
props.onSubmit({
|
|
||||||
value: props.defaultItems
|
|
||||||
.filter((item) => selectedItemIds().includes(item.id))
|
|
||||||
.map((item) => item.content)
|
|
||||||
.join(', '),
|
|
||||||
})
|
|
||||||
|
|
||||||
const filterItems = (inputValue: string) => {
|
|
||||||
setFilteredItems(
|
|
||||||
props.defaultItems.filter((item) =>
|
|
||||||
item.content?.toLowerCase().includes((inputValue ?? '').toLowerCase())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form class="flex flex-col items-end gap-2 w-full" onSubmit={handleSubmit}>
|
|
||||||
<div class="flex items-end typebot-input w-full">
|
|
||||||
<SearchInput
|
|
||||||
ref={inputRef}
|
|
||||||
onInput={filterItems}
|
|
||||||
placeholder="Filter the options..."
|
|
||||||
onClear={() => setFilteredItems(props.defaultItems)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap justify-end gap-2 overflow-y-scroll max-h-80 rounded-md hide-scrollbar">
|
|
||||||
<For each={filteredItems()}>
|
|
||||||
{(item) => (
|
|
||||||
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
|
||||||
<div
|
|
||||||
role="checkbox"
|
|
||||||
aria-checked={selectedItemIds().some(
|
|
||||||
(selectedItemId) => selectedItemId === item.id
|
|
||||||
)}
|
|
||||||
on:click={() => handleClick(item.id)}
|
|
||||||
class={
|
|
||||||
'w-full py-2 px-4 font-semibold focus:outline-none cursor-pointer select-none typebot-selectable' +
|
|
||||||
(selectedItemIds().some(
|
|
||||||
(selectedItemId) => selectedItemId === item.id
|
|
||||||
)
|
|
||||||
? ' selected'
|
|
||||||
: '')
|
|
||||||
}
|
|
||||||
data-itemid={item.id}
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Checkbox
|
|
||||||
isChecked={selectedItemIds().some(
|
|
||||||
(selectedItemId) => selectedItemId === item.id
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<span>{item.content}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
<For
|
|
||||||
each={selectedItemIds().filter((selectedItemId) =>
|
|
||||||
filteredItems().every((item) => item.id !== selectedItemId)
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{(selectedItemId) => (
|
|
||||||
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
|
||||||
<div
|
|
||||||
role="checkbox"
|
|
||||||
aria-checked
|
|
||||||
on:click={() => handleClick(selectedItemId)}
|
|
||||||
class={
|
|
||||||
'w-full py-2 px-4 font-semibold focus:outline-none cursor-pointer select-none typebot-selectable selected'
|
|
||||||
}
|
|
||||||
data-itemid={selectedItemId}
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Checkbox isChecked />
|
|
||||||
<span>
|
|
||||||
{
|
|
||||||
props.defaultItems.find(
|
|
||||||
(item) => item.id === selectedItemId
|
|
||||||
)?.content
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
{selectedItemIds().length > 0 && (
|
|
||||||
<SendButton disableIcon>
|
|
||||||
{props.options?.buttonLabel ?? 'Send'}
|
|
||||||
</SendButton>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
@ -0,0 +1,215 @@
|
|||||||
|
import { InputSubmitContent } from '@/types'
|
||||||
|
import {
|
||||||
|
PictureChoiceBlock,
|
||||||
|
defaultPictureChoiceOptions,
|
||||||
|
} from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
|
||||||
|
import { For, Show, createSignal, onMount } from 'solid-js'
|
||||||
|
import { Checkbox } from '../buttons/components/Checkbox'
|
||||||
|
import { SendButton } from '@/components'
|
||||||
|
import { isDefined, isEmpty } from '@typebot.io/lib'
|
||||||
|
import { SearchInput } from '@/components/inputs/SearchInput'
|
||||||
|
import { isMobile } from '@/utils/isMobileSignal'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
defaultItems: PictureChoiceBlock['items']
|
||||||
|
options: PictureChoiceBlock['options']
|
||||||
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MultiplePictureChoice = (props: Props) => {
|
||||||
|
let inputRef: HTMLInputElement | undefined
|
||||||
|
const [filteredItems, setFilteredItems] = createSignal(props.defaultItems)
|
||||||
|
const [selectedItemIds, setSelectedItemIds] = createSignal<string[]>([])
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!isMobile() && inputRef) inputRef.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClick = (itemId: string) => {
|
||||||
|
toggleSelectedItemId(itemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleSelectedItemId = (itemId: string) => {
|
||||||
|
const existingIndex = selectedItemIds().indexOf(itemId)
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
setSelectedItemIds((selectedItemIds) =>
|
||||||
|
selectedItemIds.filter((selectedItemId) => selectedItemId !== itemId)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setSelectedItemIds((selectedIndices) => [...selectedIndices, itemId])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () =>
|
||||||
|
props.onSubmit({
|
||||||
|
value: selectedItemIds()
|
||||||
|
.map((selectedItemId) => {
|
||||||
|
const item = props.defaultItems.find(
|
||||||
|
(item) => item.id === selectedItemId
|
||||||
|
)
|
||||||
|
return item?.title ?? item?.pictureSrc
|
||||||
|
})
|
||||||
|
.join(', '),
|
||||||
|
})
|
||||||
|
|
||||||
|
const filterItems = (inputValue: string) => {
|
||||||
|
setFilteredItems(
|
||||||
|
props.defaultItems.filter(
|
||||||
|
(item) =>
|
||||||
|
item.title
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes((inputValue ?? '').toLowerCase()) ||
|
||||||
|
item.description
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes((inputValue ?? '').toLowerCase())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form class="flex flex-col gap-2 w-full items-end" onSubmit={handleSubmit}>
|
||||||
|
<Show when={props.options.isSearchable}>
|
||||||
|
<div class="flex items-end typebot-input w-full">
|
||||||
|
<SearchInput
|
||||||
|
ref={inputRef}
|
||||||
|
onInput={filterItems}
|
||||||
|
placeholder={
|
||||||
|
props.options.searchInputPlaceholder ??
|
||||||
|
defaultPictureChoiceOptions.searchInputPlaceholder
|
||||||
|
}
|
||||||
|
onClear={() => setFilteredItems(props.defaultItems)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div
|
||||||
|
class={
|
||||||
|
'flex flex-wrap justify-end gap-2' +
|
||||||
|
(props.options.isSearchable
|
||||||
|
? ' overflow-y-scroll max-h-[464px] rounded-md hide-scrollbar'
|
||||||
|
: '')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For each={filteredItems()}>
|
||||||
|
{(item, index) => (
|
||||||
|
<div
|
||||||
|
role="checkbox"
|
||||||
|
aria-checked={selectedItemIds().some(
|
||||||
|
(selectedItemId) => selectedItemId === item.id
|
||||||
|
)}
|
||||||
|
on:click={() => handleClick(item.id)}
|
||||||
|
class={
|
||||||
|
'flex flex-col focus:outline-none cursor-pointer select-none typebot-selectable-picture' +
|
||||||
|
(selectedItemIds().some(
|
||||||
|
(selectedItemId) => selectedItemId === item.id
|
||||||
|
)
|
||||||
|
? ' selected'
|
||||||
|
: '')
|
||||||
|
}
|
||||||
|
data-itemid={item.id}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={item.pictureSrc}
|
||||||
|
alt={item.title ?? `Picture ${index() + 1}`}
|
||||||
|
elementtiming={`Picture choice ${index() + 1}`}
|
||||||
|
fetchpriority={'high'}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class={
|
||||||
|
'flex gap-3 py-2 flex-shrink-0' +
|
||||||
|
(isEmpty(item.title) && isEmpty(item.description)
|
||||||
|
? ' justify-center'
|
||||||
|
: ' pl-4')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
isChecked={selectedItemIds().some(
|
||||||
|
(selectedItemId) => selectedItemId === item.id
|
||||||
|
)}
|
||||||
|
class={item.title || item.description ? 'mt-1' : undefined}
|
||||||
|
/>
|
||||||
|
<Show when={item.title || item.description}>
|
||||||
|
<div class="flex flex-col gap-1 ">
|
||||||
|
<Show when={item.title}>
|
||||||
|
<span class="font-semibold">{item.title}</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={item.description}>
|
||||||
|
<span class="text-sm">{item.description}</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<For
|
||||||
|
each={selectedItemIds()
|
||||||
|
.filter((selectedItemId) =>
|
||||||
|
filteredItems().every((item) => item.id !== selectedItemId)
|
||||||
|
)
|
||||||
|
.map((selectedItemId) =>
|
||||||
|
props.defaultItems.find((item) => item.id === selectedItemId)
|
||||||
|
)
|
||||||
|
.filter(isDefined)}
|
||||||
|
>
|
||||||
|
{(selectedItem, index) => (
|
||||||
|
<div
|
||||||
|
role="checkbox"
|
||||||
|
aria-checked
|
||||||
|
on:click={() => handleClick(selectedItem.id)}
|
||||||
|
class={
|
||||||
|
'flex flex-col focus:outline-none cursor-pointer select-none typebot-selectable-picture selected'
|
||||||
|
}
|
||||||
|
data-itemid={selectedItem.id}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
props.defaultItems.find((item) => item.id === selectedItem.id)
|
||||||
|
?.pictureSrc
|
||||||
|
}
|
||||||
|
alt={selectedItem.title ?? `Selected picture ${index() + 1}`}
|
||||||
|
elementtiming={`Selected picture choice ${index() + 1}`}
|
||||||
|
fetchpriority={'high'}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class={
|
||||||
|
'flex gap-3 py-2 flex-shrink-0' +
|
||||||
|
(isEmpty(selectedItem.title) &&
|
||||||
|
isEmpty(selectedItem.description)
|
||||||
|
? ' justify-center'
|
||||||
|
: ' pl-4')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
isChecked={selectedItemIds().some(
|
||||||
|
(selectedItemId) => selectedItemId === selectedItem.id
|
||||||
|
)}
|
||||||
|
class={
|
||||||
|
selectedItem.title || selectedItem.description
|
||||||
|
? 'mt-1'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Show when={selectedItem.title || selectedItem.description}>
|
||||||
|
<div class="flex flex-col gap-1 ">
|
||||||
|
<Show when={selectedItem.title}>
|
||||||
|
<span class="font-semibold">{selectedItem.title}</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={selectedItem.description}>
|
||||||
|
<span class="text-sm">{selectedItem.description}</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
{selectedItemIds().length > 0 && (
|
||||||
|
<SendButton disableIcon>
|
||||||
|
{props.options?.buttonLabel ??
|
||||||
|
defaultPictureChoiceOptions.buttonLabel}
|
||||||
|
</SendButton>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
import { SearchInput } from '@/components/inputs/SearchInput'
|
||||||
|
import { InputSubmitContent } from '@/types'
|
||||||
|
import { isMobile } from '@/utils/isMobileSignal'
|
||||||
|
import { PictureChoiceBlock } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
|
||||||
|
import { For, Show, createSignal, onMount } from 'solid-js'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
defaultItems: PictureChoiceBlock['items']
|
||||||
|
options: PictureChoiceBlock['options']
|
||||||
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SinglePictureChoice = (props: Props) => {
|
||||||
|
let inputRef: HTMLInputElement | undefined
|
||||||
|
const [filteredItems, setFilteredItems] = createSignal(props.defaultItems)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!isMobile() && inputRef) inputRef.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line solid/reactivity
|
||||||
|
const handleClick = (itemIndex: number) => () => {
|
||||||
|
const pictureSrc = filteredItems()[itemIndex].pictureSrc
|
||||||
|
if (!pictureSrc) return
|
||||||
|
return props.onSubmit({
|
||||||
|
value: filteredItems()[itemIndex].title ?? pictureSrc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterItems = (inputValue: string) => {
|
||||||
|
setFilteredItems(
|
||||||
|
props.defaultItems.filter(
|
||||||
|
(item) =>
|
||||||
|
item.title
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes((inputValue ?? '').toLowerCase()) ||
|
||||||
|
item.description
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes((inputValue ?? '').toLowerCase())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex flex-col gap-2 w-full">
|
||||||
|
<Show when={props.options.isSearchable}>
|
||||||
|
<div class="flex items-end typebot-input w-full">
|
||||||
|
<SearchInput
|
||||||
|
ref={inputRef}
|
||||||
|
onInput={filterItems}
|
||||||
|
placeholder={props.options.searchInputPlaceholder ?? ''}
|
||||||
|
onClear={() => setFilteredItems(props.defaultItems)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div
|
||||||
|
class={
|
||||||
|
'gap-2 flex flex-wrap justify-end' +
|
||||||
|
(props.options.isSearchable
|
||||||
|
? ' overflow-y-scroll max-h-[464px] rounded-md hide-scrollbar'
|
||||||
|
: '')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For each={filteredItems()}>
|
||||||
|
{(item, index) => (
|
||||||
|
<button
|
||||||
|
// eslint-disable-next-line solid/reactivity
|
||||||
|
on:click={handleClick(index())}
|
||||||
|
data-itemid={item.id}
|
||||||
|
class="flex flex-col typebot-picture-button focus:outline-none filter hover:brightness-90 active:brightness-75"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={item.pictureSrc}
|
||||||
|
alt={item.title ?? `Picture ${index() + 1}`}
|
||||||
|
elementtiming={`Picture choice ${index() + 1}`}
|
||||||
|
fetchpriority={'high'}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class={
|
||||||
|
'flex flex-col gap-1 py-2 flex-shrink-0 px-4 w-full' +
|
||||||
|
(item.description ? ' items-start' : '')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="font-semibold">{item.title}</span>
|
||||||
|
<span class="text-sm">{item.description}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/react",
|
"name": "@typebot.io/react",
|
||||||
"version": "0.0.45",
|
"version": "0.0.46",
|
||||||
"description": "React library to display typebots on your website",
|
"description": "React library to display typebots on your website",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
BubbleBlockType,
|
BubbleBlockType,
|
||||||
ComparisonOperators,
|
ComparisonOperators,
|
||||||
InputBlockType,
|
InputBlockType,
|
||||||
|
ItemType,
|
||||||
LogicalOperator,
|
LogicalOperator,
|
||||||
LogicBlockType,
|
LogicBlockType,
|
||||||
StartTypebot,
|
StartTypebot,
|
||||||
@ -292,7 +293,7 @@ export const leadGenerationTypebot: StartTypebot = {
|
|||||||
{
|
{
|
||||||
id: 'clckrlksq00113b6sz8naxdwx',
|
id: 'clckrlksq00113b6sz8naxdwx',
|
||||||
blockId: 'clckrlksq00103b6s3exi90al',
|
blockId: 'clckrlksq00103b6s3exi90al',
|
||||||
type: 1,
|
type: ItemType.CONDITION,
|
||||||
content: {
|
content: {
|
||||||
comparisons: [
|
comparisons: [
|
||||||
{
|
{
|
||||||
@ -323,7 +324,7 @@ export const leadGenerationTypebot: StartTypebot = {
|
|||||||
{
|
{
|
||||||
id: 'clckrm1zr001a3b6s1hlfm2jh',
|
id: 'clckrm1zr001a3b6s1hlfm2jh',
|
||||||
blockId: 'clckrm1zr00193b6szpz37plc',
|
blockId: 'clckrm1zr00193b6szpz37plc',
|
||||||
type: 1,
|
type: ItemType.CONDITION,
|
||||||
content: {
|
content: {
|
||||||
comparisons: [
|
comparisons: [
|
||||||
{
|
{
|
||||||
|
@ -18,6 +18,7 @@ import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/enums
|
|||||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/enums'
|
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/enums'
|
||||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/enums'
|
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/enums'
|
||||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/enums'
|
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/enums'
|
||||||
|
import { PictureChoiceBlock } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
|
||||||
|
|
||||||
export const sendRequest = async <ResponseData>(
|
export const sendRequest = async <ResponseData>(
|
||||||
params:
|
params:
|
||||||
@ -90,6 +91,10 @@ export const isTextInputBlock = (block: Block): block is TextInputBlock =>
|
|||||||
export const isChoiceInput = (block: Block): block is ChoiceInputBlock =>
|
export const isChoiceInput = (block: Block): block is ChoiceInputBlock =>
|
||||||
block.type === InputBlockType.CHOICE
|
block.type === InputBlockType.CHOICE
|
||||||
|
|
||||||
|
export const isPictureChoiceInput = (
|
||||||
|
block: Block
|
||||||
|
): block is PictureChoiceBlock => block.type === InputBlockType.PICTURE_CHOICE
|
||||||
|
|
||||||
export const isSingleChoiceInput = (block: Block): block is ChoiceInputBlock =>
|
export const isSingleChoiceInput = (block: Block): block is ChoiceInputBlock =>
|
||||||
block.type === InputBlockType.CHOICE &&
|
block.type === InputBlockType.CHOICE &&
|
||||||
'options' in block &&
|
'options' in block &&
|
||||||
@ -138,7 +143,8 @@ export const blockTypeHasItems = (
|
|||||||
| LogicBlockType.AB_TEST =>
|
| LogicBlockType.AB_TEST =>
|
||||||
type === LogicBlockType.CONDITION ||
|
type === LogicBlockType.CONDITION ||
|
||||||
type === InputBlockType.CHOICE ||
|
type === InputBlockType.CHOICE ||
|
||||||
type === LogicBlockType.AB_TEST
|
type === LogicBlockType.AB_TEST ||
|
||||||
|
type === InputBlockType.PICTURE_CHOICE
|
||||||
|
|
||||||
export const blockHasItems = (
|
export const blockHasItems = (
|
||||||
block: Block
|
block: Block
|
||||||
@ -158,7 +164,7 @@ interface Omit {
|
|||||||
|
|
||||||
export const omit: Omit = (obj, ...keys) => {
|
export const omit: Omit = (obj, ...keys) => {
|
||||||
const ret = {} as {
|
const ret = {} as {
|
||||||
[K in keyof typeof obj]: typeof obj[K]
|
[K in keyof typeof obj]: (typeof obj)[K]
|
||||||
}
|
}
|
||||||
let key: keyof typeof obj
|
let key: keyof typeof obj
|
||||||
for (key in obj) {
|
for (key in obj) {
|
||||||
|
@ -11,14 +11,16 @@ export const choiceInputOptionsSchema = optionBaseSchema.merge(
|
|||||||
buttonLabel: z.string(),
|
buttonLabel: z.string(),
|
||||||
dynamicVariableId: z.string().optional(),
|
dynamicVariableId: z.string().optional(),
|
||||||
isSearchable: z.boolean().optional(),
|
isSearchable: z.boolean().optional(),
|
||||||
|
searchInputPlaceholder: z.string().optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultChoiceInputOptions: ChoiceInputOptions = {
|
export const defaultChoiceInputOptions = {
|
||||||
buttonLabel: defaultButtonLabel,
|
buttonLabel: defaultButtonLabel,
|
||||||
|
searchInputPlaceholder: 'Filter the options...',
|
||||||
isMultipleChoice: false,
|
isMultipleChoice: false,
|
||||||
isSearchable: false,
|
isSearchable: false,
|
||||||
}
|
} as const
|
||||||
|
|
||||||
export const buttonItemSchema = itemBaseSchema.merge(
|
export const buttonItemSchema = itemBaseSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
|
@ -6,6 +6,7 @@ export enum InputBlockType {
|
|||||||
DATE = 'date input',
|
DATE = 'date input',
|
||||||
PHONE = 'phone number input',
|
PHONE = 'phone number input',
|
||||||
CHOICE = 'choice input',
|
CHOICE = 'choice input',
|
||||||
|
PICTURE_CHOICE = 'picture choice input',
|
||||||
PAYMENT = 'payment input',
|
PAYMENT = 'payment input',
|
||||||
RATING = 'rating input',
|
RATING = 'rating input',
|
||||||
FILE = 'file input',
|
FILE = 'file input',
|
||||||
|
@ -10,3 +10,4 @@ export * from './phone'
|
|||||||
export * from './rating'
|
export * from './rating'
|
||||||
export * from './text'
|
export * from './text'
|
||||||
export * from './url'
|
export * from './url'
|
||||||
|
export * from './pictureChoice'
|
||||||
|
48
packages/schemas/features/blocks/inputs/pictureChoice.ts
Normal file
48
packages/schemas/features/blocks/inputs/pictureChoice.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { ItemType } from '../../items/enums'
|
||||||
|
import { itemBaseSchema } from '../../items/baseSchemas'
|
||||||
|
import { optionBaseSchema, blockBaseSchema } from '../baseSchemas'
|
||||||
|
import { defaultButtonLabel } from './constants'
|
||||||
|
import { InputBlockType } from './enums'
|
||||||
|
|
||||||
|
export const pictureChoiceOptionsSchema = optionBaseSchema.merge(
|
||||||
|
z.object({
|
||||||
|
isMultipleChoice: z.boolean().optional(),
|
||||||
|
isSearchable: z.boolean().optional(),
|
||||||
|
buttonLabel: z.string(),
|
||||||
|
searchInputPlaceholder: z.string(),
|
||||||
|
dynamicItems: z
|
||||||
|
.object({
|
||||||
|
isEnabled: z.boolean().optional(),
|
||||||
|
titlesVariableId: z.string().optional(),
|
||||||
|
descriptionsVariableId: z.string().optional(),
|
||||||
|
pictureSrcsVariableId: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const pictureChoiceItemSchema = itemBaseSchema.merge(
|
||||||
|
z.object({
|
||||||
|
type: z.literal(ItemType.PICTURE_CHOICE),
|
||||||
|
pictureSrc: z.string().optional(),
|
||||||
|
title: z.string().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const pictureChoiceBlockSchema = blockBaseSchema.merge(
|
||||||
|
z.object({
|
||||||
|
type: z.enum([InputBlockType.PICTURE_CHOICE]),
|
||||||
|
items: z.array(pictureChoiceItemSchema),
|
||||||
|
options: pictureChoiceOptionsSchema,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export type PictureChoiceItem = z.infer<typeof pictureChoiceItemSchema>
|
||||||
|
export type PictureChoiceBlock = z.infer<typeof pictureChoiceBlockSchema>
|
||||||
|
|
||||||
|
export const defaultPictureChoiceOptions: PictureChoiceBlock['options'] = {
|
||||||
|
buttonLabel: defaultButtonLabel,
|
||||||
|
searchInputPlaceholder: 'Filter the options...',
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod'
|
import { ZodDiscriminatedUnionOption, z } from 'zod'
|
||||||
import { BubbleBlockType } from './bubbles/enums'
|
import { BubbleBlockType } from './bubbles/enums'
|
||||||
import { ChoiceInputBlock, choiceInputSchema } from './inputs/choice'
|
import { choiceInputSchema } from './inputs/choice'
|
||||||
import { InputBlockType } from './inputs/enums'
|
import { InputBlockType } from './inputs/enums'
|
||||||
import { IntegrationBlockType } from './integrations/enums'
|
import { IntegrationBlockType } from './integrations/enums'
|
||||||
import { ConditionBlock, conditionBlockSchema } from './logic/condition'
|
import { ConditionBlock, conditionBlockSchema } from './logic/condition'
|
||||||
@ -43,9 +43,10 @@ import {
|
|||||||
typebotLinkBlockSchema,
|
typebotLinkBlockSchema,
|
||||||
waitBlockSchema,
|
waitBlockSchema,
|
||||||
abTestBlockSchema,
|
abTestBlockSchema,
|
||||||
AbTestBlock,
|
|
||||||
} from './logic'
|
} from './logic'
|
||||||
import { jumpBlockSchema } from './logic/jump'
|
import { jumpBlockSchema } from './logic/jump'
|
||||||
|
import { pictureChoiceBlockSchema } from './inputs/pictureChoice'
|
||||||
|
import { Item } from '../items'
|
||||||
|
|
||||||
export type DraggableBlock =
|
export type DraggableBlock =
|
||||||
| BubbleBlock
|
| BubbleBlock
|
||||||
@ -66,10 +67,7 @@ export type DraggableBlockType =
|
|||||||
| LogicBlockType
|
| LogicBlockType
|
||||||
| IntegrationBlockType
|
| IntegrationBlockType
|
||||||
|
|
||||||
export type BlockWithOptions =
|
export type BlockWithOptions = Extract<Block, { options: any }>
|
||||||
| InputBlock
|
|
||||||
| Exclude<LogicBlock, ConditionBlock>
|
|
||||||
| IntegrationBlock
|
|
||||||
|
|
||||||
export type BlockWithOptionsType =
|
export type BlockWithOptionsType =
|
||||||
| InputBlockType
|
| InputBlockType
|
||||||
@ -81,8 +79,6 @@ export type BlockOptions =
|
|||||||
| LogicBlockOptions
|
| LogicBlockOptions
|
||||||
| IntegrationBlockOptions
|
| IntegrationBlockOptions
|
||||||
|
|
||||||
export type BlockWithItems = ConditionBlock | ChoiceInputBlock | AbTestBlock
|
|
||||||
|
|
||||||
export type BlockBase = z.infer<typeof blockBaseSchema>
|
export type BlockBase = z.infer<typeof blockBaseSchema>
|
||||||
|
|
||||||
export type BlockIndices = {
|
export type BlockIndices = {
|
||||||
@ -90,18 +86,7 @@ export type BlockIndices = {
|
|||||||
blockIndex: number
|
blockIndex: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const bubbleBlockSchema = z.discriminatedUnion('type', [
|
export const inputBlockSchemas = [
|
||||||
textBubbleBlockSchema,
|
|
||||||
imageBubbleBlockSchema,
|
|
||||||
videoBubbleBlockSchema,
|
|
||||||
embedBubbleBlockSchema,
|
|
||||||
audioBubbleBlockSchema,
|
|
||||||
])
|
|
||||||
|
|
||||||
export type BubbleBlock = z.infer<typeof bubbleBlockSchema>
|
|
||||||
export type BubbleBlockContent = BubbleBlock['content']
|
|
||||||
|
|
||||||
export const inputBlockSchema = z.discriminatedUnion('type', [
|
|
||||||
textInputSchema,
|
textInputSchema,
|
||||||
choiceInputSchema,
|
choiceInputSchema,
|
||||||
emailInputSchema,
|
emailInputSchema,
|
||||||
@ -112,12 +97,17 @@ export const inputBlockSchema = z.discriminatedUnion('type', [
|
|||||||
paymentInputSchema,
|
paymentInputSchema,
|
||||||
ratingInputBlockSchema,
|
ratingInputBlockSchema,
|
||||||
fileInputStepSchema,
|
fileInputStepSchema,
|
||||||
])
|
pictureChoiceBlockSchema,
|
||||||
|
] as const
|
||||||
|
|
||||||
export type InputBlock = z.infer<typeof inputBlockSchema>
|
export const blockSchema = z.discriminatedUnion('type', [
|
||||||
export type InputBlockOptions = InputBlock['options']
|
startBlockSchema,
|
||||||
|
textBubbleBlockSchema,
|
||||||
export const logicBlockSchema = z.discriminatedUnion('type', [
|
imageBubbleBlockSchema,
|
||||||
|
videoBubbleBlockSchema,
|
||||||
|
embedBubbleBlockSchema,
|
||||||
|
audioBubbleBlockSchema,
|
||||||
|
...inputBlockSchemas,
|
||||||
scriptBlockSchema,
|
scriptBlockSchema,
|
||||||
conditionBlockSchema,
|
conditionBlockSchema,
|
||||||
redirectBlockSchema,
|
redirectBlockSchema,
|
||||||
@ -126,19 +116,6 @@ export const logicBlockSchema = z.discriminatedUnion('type', [
|
|||||||
waitBlockSchema,
|
waitBlockSchema,
|
||||||
jumpBlockSchema,
|
jumpBlockSchema,
|
||||||
abTestBlockSchema,
|
abTestBlockSchema,
|
||||||
])
|
|
||||||
|
|
||||||
export type LogicBlock = z.infer<typeof logicBlockSchema>
|
|
||||||
|
|
||||||
export type LogicBlockOptions = LogicBlock extends
|
|
||||||
| {
|
|
||||||
options?: infer Options
|
|
||||||
}
|
|
||||||
| {}
|
|
||||||
? Options
|
|
||||||
: never
|
|
||||||
|
|
||||||
export const integrationBlockSchema = z.discriminatedUnion('type', [
|
|
||||||
chatwootBlockSchema,
|
chatwootBlockSchema,
|
||||||
googleAnalyticsBlockSchema,
|
googleAnalyticsBlockSchema,
|
||||||
googleSheetsBlockSchema,
|
googleSheetsBlockSchema,
|
||||||
@ -150,15 +127,24 @@ export const integrationBlockSchema = z.discriminatedUnion('type', [
|
|||||||
zapierBlockSchema,
|
zapierBlockSchema,
|
||||||
])
|
])
|
||||||
|
|
||||||
export type IntegrationBlock = z.infer<typeof integrationBlockSchema>
|
export type Block = z.infer<typeof blockSchema>
|
||||||
|
|
||||||
|
export type BubbleBlock = Extract<Block, { type: BubbleBlockType }>
|
||||||
|
export type BubbleBlockContent = BubbleBlock['content']
|
||||||
|
|
||||||
|
export type InputBlock = Extract<Block, { type: InputBlockType }>
|
||||||
|
export type InputBlockOptions = InputBlock['options']
|
||||||
|
|
||||||
|
export type LogicBlock = Extract<Block, { type: LogicBlockType }>
|
||||||
|
export type LogicBlockOptions = LogicBlock extends
|
||||||
|
| {
|
||||||
|
options?: infer Options
|
||||||
|
}
|
||||||
|
| {}
|
||||||
|
? Options
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type IntegrationBlock = Extract<Block, { type: IntegrationBlockType }>
|
||||||
export type IntegrationBlockOptions = IntegrationBlock['options']
|
export type IntegrationBlockOptions = IntegrationBlock['options']
|
||||||
|
|
||||||
export const blockSchema = z.union([
|
export type BlockWithItems = Extract<Block, { items: Item[] }>
|
||||||
startBlockSchema,
|
|
||||||
bubbleBlockSchema,
|
|
||||||
inputBlockSchema,
|
|
||||||
logicBlockSchema,
|
|
||||||
integrationBlockSchema,
|
|
||||||
])
|
|
||||||
|
|
||||||
export type Block = z.infer<typeof blockSchema>
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { z } from 'zod'
|
import { ZodDiscriminatedUnion, z } from 'zod'
|
||||||
import {
|
import {
|
||||||
googleAnalyticsOptionsSchema,
|
googleAnalyticsOptionsSchema,
|
||||||
paymentInputRuntimeOptionsSchema,
|
paymentInputRuntimeOptionsSchema,
|
||||||
@ -16,7 +16,7 @@ import {
|
|||||||
} from './blocks/bubbles'
|
} from './blocks/bubbles'
|
||||||
import { answerSchema } from './answer'
|
import { answerSchema } from './answer'
|
||||||
import { BubbleBlockType } from './blocks/bubbles/enums'
|
import { BubbleBlockType } from './blocks/bubbles/enums'
|
||||||
import { inputBlockSchema } from './blocks/schemas'
|
import { inputBlockSchemas } from './blocks/schemas'
|
||||||
|
|
||||||
const typebotInSessionStateSchema = publicTypebotSchema.pick({
|
const typebotInSessionStateSchema = publicTypebotSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
@ -229,7 +229,8 @@ const clientSideActionSchema = z
|
|||||||
|
|
||||||
export const chatReplySchema = z.object({
|
export const chatReplySchema = z.object({
|
||||||
messages: z.array(chatMessageSchema),
|
messages: z.array(chatMessageSchema),
|
||||||
input: inputBlockSchema
|
input: z
|
||||||
|
.discriminatedUnion('type', [...inputBlockSchemas])
|
||||||
.and(
|
.and(
|
||||||
z.object({
|
z.object({
|
||||||
prefilledValue: z.string().optional(),
|
prefilledValue: z.string().optional(),
|
||||||
|
@ -2,4 +2,5 @@ export enum ItemType {
|
|||||||
BUTTON,
|
BUTTON,
|
||||||
CONDITION,
|
CONDITION,
|
||||||
AB_TEST,
|
AB_TEST,
|
||||||
|
PICTURE_CHOICE,
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,14 @@ import { z } from 'zod'
|
|||||||
import { buttonItemSchema } from '../blocks/inputs/choice'
|
import { buttonItemSchema } from '../blocks/inputs/choice'
|
||||||
import { conditionItemSchema } from '../blocks/logic/condition'
|
import { conditionItemSchema } from '../blocks/logic/condition'
|
||||||
import { aItemSchema, bItemSchema } from '../blocks'
|
import { aItemSchema, bItemSchema } from '../blocks'
|
||||||
|
import { pictureChoiceItemSchema } from '../blocks/inputs/pictureChoice'
|
||||||
|
|
||||||
const itemSchema = buttonItemSchema
|
const itemSchema = z.union([
|
||||||
.or(conditionItemSchema)
|
buttonItemSchema,
|
||||||
.or(aItemSchema)
|
conditionItemSchema,
|
||||||
.or(bItemSchema)
|
pictureChoiceItemSchema,
|
||||||
|
aItemSchema,
|
||||||
|
bItemSchema,
|
||||||
|
])
|
||||||
|
|
||||||
export type Item = z.infer<typeof itemSchema>
|
export type Item = z.infer<typeof itemSchema>
|
||||||
|
Reference in New Issue
Block a user