✨ Add conditional choice items
Allows you to conditonnally display an item from the Buttons or the Picture choice input Closes #546
This commit is contained in:
@ -5,12 +5,22 @@ import {
|
|||||||
Fade,
|
Fade,
|
||||||
IconButton,
|
IconButton,
|
||||||
Flex,
|
Flex,
|
||||||
|
Popover,
|
||||||
|
PopoverAnchor,
|
||||||
|
PopoverArrow,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverContent,
|
||||||
|
Portal,
|
||||||
|
useColorModeValue,
|
||||||
|
SlideFade,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { PlusIcon } from '@/components/icons'
|
import { PlusIcon, SettingsIcon } from '@/components/icons'
|
||||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
import { ButtonItem, Item, ItemIndices, ItemType } from '@typebot.io/schemas'
|
import { ButtonItem, Item, ItemIndices, ItemType } from '@typebot.io/schemas'
|
||||||
import React, { useRef, useState } from 'react'
|
import React, { useRef, useState } from 'react'
|
||||||
import { isNotDefined } from '@typebot.io/lib'
|
import { isNotDefined } from '@typebot.io/lib'
|
||||||
|
import { useGraph } from '@/features/graph/providers/GraphProvider'
|
||||||
|
import { ButtonsItemSettings } from './ButtonsItemSettings'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
item: ButtonItem
|
item: ButtonItem
|
||||||
@ -20,8 +30,12 @@ type Props = {
|
|||||||
|
|
||||||
export const ButtonsItemNode = ({ item, indices, isMouseOver }: Props) => {
|
export const ButtonsItemNode = ({ item, indices, isMouseOver }: Props) => {
|
||||||
const { deleteItem, updateItem, createItem } = useTypebot()
|
const { deleteItem, updateItem, createItem } = useTypebot()
|
||||||
|
const { openedItemId, setOpenedItemId } = useGraph()
|
||||||
const [itemValue, setItemValue] = useState(item.content ?? 'Click to edit')
|
const [itemValue, setItemValue] = useState(item.content ?? 'Click to edit')
|
||||||
const editableRef = useRef<HTMLDivElement | null>(null)
|
const editableRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation()
|
||||||
|
|
||||||
const handleInputSubmit = () => {
|
const handleInputSubmit = () => {
|
||||||
if (itemValue === '') deleteItem(indices)
|
if (itemValue === '') deleteItem(indices)
|
||||||
@ -45,44 +59,101 @@ export const ButtonsItemNode = ({ item, indices, isMouseOver }: Props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateItemSettings = (settings: Omit<ButtonItem, 'content'>) => {
|
||||||
|
updateItem(indices, { ...item, ...settings })
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex px={4} py={2} justify="center" w="90%" pos="relative">
|
<Popover
|
||||||
<Editable
|
placement="right"
|
||||||
ref={editableRef}
|
isLazy
|
||||||
flex="1"
|
isOpen={openedItemId === item.id}
|
||||||
startWithEditView={isNotDefined(item.content)}
|
closeOnBlur={false}
|
||||||
value={itemValue}
|
>
|
||||||
onChange={setItemValue}
|
<PopoverAnchor>
|
||||||
onSubmit={handleInputSubmit}
|
<Flex px={4} py={2} justify="center" w="90%" pos="relative">
|
||||||
onKeyDownCapture={handleKeyPress}
|
<Editable
|
||||||
maxW="180px"
|
ref={editableRef}
|
||||||
>
|
flex="1"
|
||||||
<EditablePreview
|
startWithEditView={isNotDefined(item.content)}
|
||||||
w="full"
|
value={itemValue}
|
||||||
color={item.content !== 'Click to edit' ? 'inherit' : 'gray.500'}
|
onChange={setItemValue}
|
||||||
cursor="pointer"
|
onSubmit={handleInputSubmit}
|
||||||
/>
|
onKeyDownCapture={handleKeyPress}
|
||||||
<EditableInput />
|
maxW="180px"
|
||||||
</Editable>
|
>
|
||||||
<Fade
|
<EditablePreview
|
||||||
in={isMouseOver}
|
w="full"
|
||||||
style={{
|
color={item.content !== 'Click to edit' ? 'inherit' : 'gray.500'}
|
||||||
position: 'absolute',
|
cursor="pointer"
|
||||||
bottom: '-15px',
|
/>
|
||||||
zIndex: 3,
|
<EditableInput />
|
||||||
left: '90px',
|
</Editable>
|
||||||
}}
|
<HitboxExtension />
|
||||||
unmountOnExit
|
<SlideFade
|
||||||
>
|
offsetY="0px"
|
||||||
<IconButton
|
offsetX="-10px"
|
||||||
aria-label="Add item"
|
in={isMouseOver}
|
||||||
icon={<PlusIcon />}
|
style={{
|
||||||
size="xs"
|
position: 'absolute',
|
||||||
shadow="md"
|
right: '-60px',
|
||||||
colorScheme="gray"
|
zIndex: 3,
|
||||||
onClick={handlePlusClick}
|
}}
|
||||||
/>
|
unmountOnExit
|
||||||
</Fade>
|
>
|
||||||
</Flex>
|
<IconButton
|
||||||
|
aria-label="Open settings"
|
||||||
|
icon={<SettingsIcon />}
|
||||||
|
bgColor={useColorModeValue('white', 'gray.800')}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
shadow="md"
|
||||||
|
colorScheme="gray"
|
||||||
|
onClick={() => setOpenedItemId(item.id)}
|
||||||
|
/>
|
||||||
|
</SlideFade>
|
||||||
|
<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"
|
||||||
|
onClick={handlePlusClick}
|
||||||
|
/>
|
||||||
|
</Fade>
|
||||||
|
</Flex>
|
||||||
|
</PopoverAnchor>
|
||||||
|
<Portal>
|
||||||
|
<PopoverContent pos="relative" onMouseDown={handleMouseDown}>
|
||||||
|
<PopoverArrow />
|
||||||
|
<PopoverBody
|
||||||
|
py="6"
|
||||||
|
overflowY="scroll"
|
||||||
|
maxH="400px"
|
||||||
|
shadow="lg"
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<ButtonsItemSettings
|
||||||
|
item={item}
|
||||||
|
onSettingsChange={updateItemSettings}
|
||||||
|
/>
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Portal>
|
||||||
|
</Popover>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HitboxExtension = () => (
|
||||||
|
<Flex h="full" w="50px" pos="absolute" top="0" right="-70px" />
|
||||||
|
)
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Stack } from '@chakra-ui/react'
|
||||||
|
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
|
||||||
|
import { ConditionForm } from '@/features/blocks/logic/condition/components/ConditionForm'
|
||||||
|
import { ButtonItem, Condition, LogicalOperator } from '@typebot.io/schemas'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
item: ButtonItem
|
||||||
|
onSettingsChange: (updates: Omit<ButtonItem, 'content'>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ButtonsItemSettings = ({ item, onSettingsChange }: Props) => {
|
||||||
|
const updateIsDisplayConditionEnabled = (isEnabled: boolean) =>
|
||||||
|
onSettingsChange({
|
||||||
|
...item,
|
||||||
|
displayCondition: {
|
||||||
|
...item.displayCondition,
|
||||||
|
isEnabled,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateDisplayCondition = (condition: Condition) =>
|
||||||
|
onSettingsChange({
|
||||||
|
...item,
|
||||||
|
displayCondition: {
|
||||||
|
...item.displayCondition,
|
||||||
|
condition,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<SwitchWithRelatedSettings
|
||||||
|
label="Display condition"
|
||||||
|
initialValue={item.displayCondition?.isEnabled ?? false}
|
||||||
|
onCheckChange={updateIsDisplayConditionEnabled}
|
||||||
|
>
|
||||||
|
<ConditionForm
|
||||||
|
condition={
|
||||||
|
item.displayCondition?.condition ?? {
|
||||||
|
comparisons: [],
|
||||||
|
logicalOperator: LogicalOperator.AND,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onConditionChange={updateDisplayCondition}
|
||||||
|
/>
|
||||||
|
</SwitchWithRelatedSettings>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
@ -11,6 +11,9 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { ImageUploadContent } from '@/components/ImageUploadContent'
|
import { ImageUploadContent } from '@/components/ImageUploadContent'
|
||||||
|
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
|
||||||
|
import { ConditionForm } from '@/features/blocks/logic/condition/components/ConditionForm'
|
||||||
|
import { Condition, LogicalOperator } from '@typebot.io/schemas'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
typebotId: string
|
typebotId: string
|
||||||
@ -32,15 +35,35 @@ export const PictureChoiceItemSettings = ({
|
|||||||
const updateDescription = (description: string) =>
|
const updateDescription = (description: string) =>
|
||||||
onItemChange({ ...item, description })
|
onItemChange({ ...item, description })
|
||||||
|
|
||||||
|
const updateIsDisplayConditionEnabled = (isEnabled: boolean) =>
|
||||||
|
onItemChange({
|
||||||
|
...item,
|
||||||
|
displayCondition: {
|
||||||
|
...item.displayCondition,
|
||||||
|
isEnabled,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateDisplayCondition = (condition: Condition) =>
|
||||||
|
onItemChange({
|
||||||
|
...item,
|
||||||
|
displayCondition: {
|
||||||
|
...item.displayCondition,
|
||||||
|
condition,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack spacing={4}>
|
||||||
<HStack>
|
<HStack>
|
||||||
<Text fontWeight="medium">Image:</Text>
|
<Text fontWeight="medium">Image:</Text>
|
||||||
<Popover isLazy>
|
<Popover isLazy>
|
||||||
{({ onClose }) => (
|
{({ onClose }) => (
|
||||||
<>
|
<>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button size="sm">Pick an image</Button>
|
<Button size="sm">
|
||||||
|
{item.pictureSrc ? 'Change image' : 'Pick an image'}
|
||||||
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent p="4" w="500px">
|
<PopoverContent p="4" w="500px">
|
||||||
<ImageUploadContent
|
<ImageUploadContent
|
||||||
@ -67,6 +90,21 @@ export const PictureChoiceItemSettings = ({
|
|||||||
defaultValue={item.description}
|
defaultValue={item.description}
|
||||||
onChange={updateDescription}
|
onChange={updateDescription}
|
||||||
/>
|
/>
|
||||||
|
<SwitchWithRelatedSettings
|
||||||
|
label="Display condition"
|
||||||
|
initialValue={item.displayCondition?.isEnabled ?? false}
|
||||||
|
onCheckChange={updateIsDisplayConditionEnabled}
|
||||||
|
>
|
||||||
|
<ConditionForm
|
||||||
|
condition={
|
||||||
|
item.displayCondition?.condition ?? {
|
||||||
|
comparisons: [],
|
||||||
|
logicalOperator: LogicalOperator.AND,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onConditionChange={updateDisplayCondition}
|
||||||
|
/>
|
||||||
|
</SwitchWithRelatedSettings>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,78 @@
|
|||||||
|
import { Stack, Wrap, Tag, Text, useColorModeValue } from '@chakra-ui/react'
|
||||||
|
import { byId } from '@typebot.io/lib'
|
||||||
|
import { ComparisonOperators, Condition, Variable } from '@typebot.io/schemas'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
condition: Condition
|
||||||
|
variables: Variable[]
|
||||||
|
size?: 'xs' | 'sm'
|
||||||
|
displaySemicolon?: boolean
|
||||||
|
}
|
||||||
|
export const ConditionContent = ({
|
||||||
|
condition,
|
||||||
|
variables,
|
||||||
|
size = 'sm',
|
||||||
|
displaySemicolon,
|
||||||
|
}: Props) => {
|
||||||
|
const comparisonValueBg = useColorModeValue('gray.200', 'gray.700')
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
{condition.comparisons.map((comparison, idx) => {
|
||||||
|
const variable = variables.find(byId(comparison.variableId))
|
||||||
|
return (
|
||||||
|
<Wrap key={comparison.id} spacing={1} noOfLines={1}>
|
||||||
|
{idx === 0 && <Text fontSize={size}>IF</Text>}
|
||||||
|
{idx > 0 && (
|
||||||
|
<Text fontSize={size}>{condition.logicalOperator ?? ''}</Text>
|
||||||
|
)}
|
||||||
|
{variable?.name && (
|
||||||
|
<Tag bgColor="orange.400" color="white" size="sm">
|
||||||
|
{variable.name}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{comparison.comparisonOperator && (
|
||||||
|
<Text fontSize={size}>
|
||||||
|
{parseComparisonOperatorSymbol(comparison.comparisonOperator)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{comparison?.value && (
|
||||||
|
<Tag bgColor={comparisonValueBg} size="sm">
|
||||||
|
{comparison.value}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{idx === condition.comparisons.length - 1 && displaySemicolon && (
|
||||||
|
<Text fontSize={size}>:</Text>
|
||||||
|
)}
|
||||||
|
</Wrap>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseComparisonOperatorSymbol = (
|
||||||
|
operator: ComparisonOperators
|
||||||
|
): string => {
|
||||||
|
switch (operator) {
|
||||||
|
case ComparisonOperators.CONTAINS:
|
||||||
|
return 'contains'
|
||||||
|
case ComparisonOperators.EQUAL:
|
||||||
|
return '='
|
||||||
|
case ComparisonOperators.GREATER:
|
||||||
|
return '>'
|
||||||
|
case ComparisonOperators.IS_SET:
|
||||||
|
return 'is set'
|
||||||
|
case ComparisonOperators.LESS:
|
||||||
|
return '<'
|
||||||
|
case ComparisonOperators.NOT_EQUAL:
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
}
|
@ -1,30 +1,30 @@
|
|||||||
import { Flex } from '@chakra-ui/react'
|
import { Flex } from '@chakra-ui/react'
|
||||||
import { DropdownList } from '@/components/DropdownList'
|
import { DropdownList } from '@/components/DropdownList'
|
||||||
import { Comparison, ConditionItem, LogicalOperator } from '@typebot.io/schemas'
|
import { Comparison, Condition, LogicalOperator } from '@typebot.io/schemas'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { ComparisonItem } from './ComparisonItem'
|
import { ComparisonItem } from './ComparisonItem'
|
||||||
import { TableList } from '@/components/TableList'
|
import { TableList } from '@/components/TableList'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
itemContent: ConditionItem['content']
|
condition: Condition
|
||||||
onItemChange: (updates: Partial<ConditionItem>) => void
|
onConditionChange: (newCondition: Condition) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConditionItemForm = ({ itemContent, onItemChange }: Props) => {
|
export const ConditionForm = ({ condition, onConditionChange }: Props) => {
|
||||||
const handleComparisonsChange = (comparisons: Comparison[]) =>
|
const handleComparisonsChange = (comparisons: Comparison[]) =>
|
||||||
onItemChange({ content: { ...itemContent, comparisons } })
|
onConditionChange({ ...condition, comparisons })
|
||||||
const handleLogicalOperatorChange = (logicalOperator: LogicalOperator) =>
|
const handleLogicalOperatorChange = (logicalOperator: LogicalOperator) =>
|
||||||
onItemChange({ content: { ...itemContent, logicalOperator } })
|
onConditionChange({ ...condition, logicalOperator })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableList<Comparison>
|
<TableList<Comparison>
|
||||||
initialItems={itemContent.comparisons}
|
initialItems={condition.comparisons}
|
||||||
onItemsChange={handleComparisonsChange}
|
onItemsChange={handleComparisonsChange}
|
||||||
Item={ComparisonItem}
|
Item={ComparisonItem}
|
||||||
ComponentBetweenItems={() => (
|
ComponentBetweenItems={() => (
|
||||||
<Flex justify="center">
|
<Flex justify="center">
|
||||||
<DropdownList
|
<DropdownList
|
||||||
currentItem={itemContent.logicalOperator}
|
currentItem={condition.logicalOperator}
|
||||||
onItemSelect={handleLogicalOperatorChange}
|
onItemSelect={handleLogicalOperatorChange}
|
||||||
items={Object.values(LogicalOperator)}
|
items={Object.values(LogicalOperator)}
|
||||||
/>
|
/>
|
@ -1,9 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Stack,
|
|
||||||
Tag,
|
|
||||||
Text,
|
Text,
|
||||||
Flex,
|
Flex,
|
||||||
Wrap,
|
|
||||||
Fade,
|
Fade,
|
||||||
IconButton,
|
IconButton,
|
||||||
Popover,
|
Popover,
|
||||||
@ -12,23 +9,23 @@ import {
|
|||||||
PopoverArrow,
|
PopoverArrow,
|
||||||
PopoverBody,
|
PopoverBody,
|
||||||
useEventListener,
|
useEventListener,
|
||||||
useColorModeValue,
|
|
||||||
PopoverAnchor,
|
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 {
|
||||||
Comparison,
|
Comparison,
|
||||||
ConditionItem,
|
ConditionItem,
|
||||||
ComparisonOperators,
|
|
||||||
ItemType,
|
ItemType,
|
||||||
ItemIndices,
|
ItemIndices,
|
||||||
|
Condition,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import React, { useRef } from 'react'
|
import React, { useRef } from 'react'
|
||||||
import { byId, isNotDefined } from '@typebot.io/lib'
|
import { isNotDefined } from '@typebot.io/lib'
|
||||||
import { PlusIcon } from '@/components/icons'
|
import { PlusIcon } from '@/components/icons'
|
||||||
import { ConditionItemForm } from './ConditionItemForm'
|
import { ConditionForm } from './ConditionForm'
|
||||||
import { useGraph } from '@/features/graph/providers/GraphProvider'
|
import { useGraph } from '@/features/graph/providers/GraphProvider'
|
||||||
import { createId } from '@paralleldrive/cuid2'
|
import { createId } from '@paralleldrive/cuid2'
|
||||||
|
import { ConditionContent } from './ConditionContent'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
item: ConditionItem
|
item: ConditionItem
|
||||||
@ -37,7 +34,6 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ConditionItemNode = ({ item, isMouseOver, indices }: Props) => {
|
export const ConditionItemNode = ({ item, isMouseOver, indices }: Props) => {
|
||||||
const comparisonValueBg = useColorModeValue('gray.200', 'gray.700')
|
|
||||||
const { typebot, createItem, updateItem } = useTypebot()
|
const { typebot, createItem, updateItem } = useTypebot()
|
||||||
const { openedItemId, setOpenedItemId } = useGraph()
|
const { openedItemId, setOpenedItemId } = useGraph()
|
||||||
const ref = useRef<HTMLDivElement | null>(null)
|
const ref = useRef<HTMLDivElement | null>(null)
|
||||||
@ -48,8 +44,8 @@ export const ConditionItemNode = ({ item, isMouseOver, indices }: Props) => {
|
|||||||
setOpenedItemId(item.id)
|
setOpenedItemId(item.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleItemChange = (updates: Partial<ConditionItem>) => {
|
const updateCondition = (condition: Condition) => {
|
||||||
updateItem(indices, { ...item, ...updates })
|
updateItem(indices, { ...item, content: condition } as ConditionItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePlusClick = (event: React.MouseEvent) => {
|
const handlePlusClick = (event: React.MouseEvent) => {
|
||||||
@ -85,37 +81,10 @@ export const ConditionItemNode = ({ item, isMouseOver, indices }: Props) => {
|
|||||||
comparisonIsEmpty(item.content.comparisons[0]) ? (
|
comparisonIsEmpty(item.content.comparisons[0]) ? (
|
||||||
<Text color={'gray.500'}>Configure...</Text>
|
<Text color={'gray.500'}>Configure...</Text>
|
||||||
) : (
|
) : (
|
||||||
<Stack maxW="170px">
|
<ConditionContent
|
||||||
{item.content.comparisons.map((comparison, idx) => {
|
condition={item.content}
|
||||||
const variable = typebot?.variables.find(
|
variables={typebot?.variables ?? []}
|
||||||
byId(comparison.variableId)
|
/>
|
||||||
)
|
|
||||||
return (
|
|
||||||
<Wrap key={comparison.id} spacing={1} noOfLines={1}>
|
|
||||||
{idx > 0 && (
|
|
||||||
<Text>{item.content.logicalOperator ?? ''}</Text>
|
|
||||||
)}
|
|
||||||
{variable?.name && (
|
|
||||||
<Tag bgColor="orange.400" color="white">
|
|
||||||
{variable.name}
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
{comparison.comparisonOperator && (
|
|
||||||
<Text fontSize="sm">
|
|
||||||
{parseComparisonOperatorSymbol(
|
|
||||||
comparison.comparisonOperator
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{comparison?.value && (
|
|
||||||
<Tag bgColor={comparisonValueBg}>
|
|
||||||
<Text noOfLines={1}>{comparison.value}</Text>
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</Wrap>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Stack>
|
|
||||||
)}
|
)}
|
||||||
<Fade
|
<Fade
|
||||||
in={isMouseOver}
|
in={isMouseOver}
|
||||||
@ -148,9 +117,9 @@ export const ConditionItemNode = ({ item, isMouseOver, indices }: Props) => {
|
|||||||
shadow="lg"
|
shadow="lg"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<ConditionItemForm
|
<ConditionForm
|
||||||
itemContent={item.content}
|
condition={item.content}
|
||||||
onItemChange={handleItemChange}
|
onConditionChange={updateCondition}
|
||||||
/>
|
/>
|
||||||
</PopoverBody>
|
</PopoverBody>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
@ -163,30 +132,3 @@ const comparisonIsEmpty = (comparison: Comparison) =>
|
|||||||
isNotDefined(comparison.comparisonOperator) &&
|
isNotDefined(comparison.comparisonOperator) &&
|
||||||
isNotDefined(comparison.value) &&
|
isNotDefined(comparison.value) &&
|
||||||
isNotDefined(comparison.variableId)
|
isNotDefined(comparison.variableId)
|
||||||
|
|
||||||
const parseComparisonOperatorSymbol = (
|
|
||||||
operator: ComparisonOperators
|
|
||||||
): string => {
|
|
||||||
switch (operator) {
|
|
||||||
case ComparisonOperators.CONTAINS:
|
|
||||||
return 'contains'
|
|
||||||
case ComparisonOperators.EQUAL:
|
|
||||||
return '='
|
|
||||||
case ComparisonOperators.GREATER:
|
|
||||||
return '>'
|
|
||||||
case ComparisonOperators.IS_SET:
|
|
||||||
return 'is set'
|
|
||||||
case ComparisonOperators.LESS:
|
|
||||||
return '<'
|
|
||||||
case ComparisonOperators.NOT_EQUAL:
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Flex, useColorModeValue } from '@chakra-ui/react'
|
import { Flex, useColorModeValue, Stack } from '@chakra-ui/react'
|
||||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
import {
|
import {
|
||||||
ChoiceInputBlock,
|
ChoiceInputBlock,
|
||||||
@ -20,6 +20,7 @@ import {
|
|||||||
} from '@/features/graph/providers/GraphDndProvider'
|
} from '@/features/graph/providers/GraphDndProvider'
|
||||||
import { useGraph } from '@/features/graph/providers/GraphProvider'
|
import { useGraph } from '@/features/graph/providers/GraphProvider'
|
||||||
import { setMultipleRefs } from '@/helpers/setMultipleRefs'
|
import { setMultipleRefs } from '@/helpers/setMultipleRefs'
|
||||||
|
import { ConditionContent } from '@/features/blocks/logic/condition/components/ConditionContent'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
item: Item
|
item: Item
|
||||||
@ -71,12 +72,22 @@ export const ItemNode = ({
|
|||||||
renderMenu={() => <ItemNodeContextMenu indices={indices} />}
|
renderMenu={() => <ItemNodeContextMenu indices={indices} />}
|
||||||
>
|
>
|
||||||
{(ref, isContextMenuOpened) => (
|
{(ref, isContextMenuOpened) => (
|
||||||
<Flex
|
<Stack
|
||||||
data-testid="item"
|
data-testid="item"
|
||||||
pos="relative"
|
pos="relative"
|
||||||
ref={setMultipleRefs([ref, itemRef])}
|
ref={setMultipleRefs([ref, itemRef])}
|
||||||
w="full"
|
w="full"
|
||||||
>
|
>
|
||||||
|
{'displayCondition' in item &&
|
||||||
|
item.displayCondition?.isEnabled &&
|
||||||
|
item.displayCondition.condition && (
|
||||||
|
<ConditionContent
|
||||||
|
condition={item.displayCondition.condition}
|
||||||
|
variables={typebot?.variables ?? []}
|
||||||
|
size="xs"
|
||||||
|
displaySemicolon
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Flex
|
<Flex
|
||||||
align="center"
|
align="center"
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
@ -113,7 +124,7 @@ export const ItemNode = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import { ChoiceInputBlock, Variable } from '@typebot.io/schemas'
|
||||||
|
import { executeCondition } from '../../logic/condition/executeCondition'
|
||||||
|
|
||||||
|
export const filterChoiceItems =
|
||||||
|
(variables: Variable[]) =>
|
||||||
|
(block: ChoiceInputBlock): ChoiceInputBlock => {
|
||||||
|
const filteredItems = block.items.filter((item) => {
|
||||||
|
if (item.displayCondition?.isEnabled && item.displayCondition?.condition)
|
||||||
|
return executeCondition(variables)(item.displayCondition.condition)
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
...block,
|
||||||
|
items: filteredItems,
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ import { isDefined } from '@typebot.io/lib'
|
|||||||
import { deepParseVariables } from '@/features/variables/deepParseVariable'
|
import { deepParseVariables } from '@/features/variables/deepParseVariable'
|
||||||
import { transformStringVariablesToList } from '@/features/variables/transformVariablesToList'
|
import { transformStringVariablesToList } from '@/features/variables/transformVariablesToList'
|
||||||
import { updateVariables } from '@/features/variables/updateVariables'
|
import { updateVariables } from '@/features/variables/updateVariables'
|
||||||
|
import { filterChoiceItems } from './filterChoiceItems'
|
||||||
|
|
||||||
export const injectVariableValuesInButtonsInputBlock =
|
export const injectVariableValuesInButtonsInputBlock =
|
||||||
(state: SessionState) =>
|
(state: SessionState) =>
|
||||||
@ -30,7 +31,9 @@ export const injectVariableValuesInButtonsInputBlock =
|
|||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return deepParseVariables(state.typebot.variables)(block)
|
return deepParseVariables(state.typebot.variables)(
|
||||||
|
filterChoiceItems(state.typebot.variables)(block)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getVariableValue =
|
const getVariableValue =
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import { PictureChoiceBlock, Variable } from '@typebot.io/schemas'
|
||||||
|
import { executeCondition } from '../../logic/condition/executeCondition'
|
||||||
|
|
||||||
|
export const filterPictureChoiceItems =
|
||||||
|
(variables: Variable[]) =>
|
||||||
|
(block: PictureChoiceBlock): PictureChoiceBlock => {
|
||||||
|
const filteredItems = block.items.filter((item) => {
|
||||||
|
if (item.displayCondition?.isEnabled && item.displayCondition?.condition)
|
||||||
|
return executeCondition(variables)(item.displayCondition.condition)
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
...block,
|
||||||
|
items: filteredItems,
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ import {
|
|||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { isDefined } from '@typebot.io/lib'
|
import { isDefined } from '@typebot.io/lib'
|
||||||
import { deepParseVariables } from '@/features/variables/deepParseVariable'
|
import { deepParseVariables } from '@/features/variables/deepParseVariable'
|
||||||
|
import { filterPictureChoiceItems } from './filterPictureChoiceItems'
|
||||||
|
|
||||||
export const injectVariableValuesInPictureChoiceBlock =
|
export const injectVariableValuesInPictureChoiceBlock =
|
||||||
(variables: SessionState['typebot']['variables']) =>
|
(variables: SessionState['typebot']['variables']) =>
|
||||||
@ -51,5 +52,7 @@ export const injectVariableValuesInPictureChoiceBlock =
|
|||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return deepParseVariables(variables)(block)
|
return deepParseVariables(variables)(
|
||||||
|
filterPictureChoiceItems(variables)(block)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,34 +1,20 @@
|
|||||||
|
import { findUniqueVariableValue } from '@/features/variables/findUniqueVariableValue'
|
||||||
|
import { isNotDefined, isDefined } from '@typebot.io/lib'
|
||||||
import {
|
import {
|
||||||
Comparison,
|
Comparison,
|
||||||
ComparisonOperators,
|
ComparisonOperators,
|
||||||
ConditionBlock,
|
Condition,
|
||||||
LogicalOperator,
|
LogicalOperator,
|
||||||
SessionState,
|
|
||||||
Variable,
|
Variable,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { isNotDefined, isDefined } from '@typebot.io/lib'
|
import { parseVariables } from 'bot-engine'
|
||||||
import { ExecuteLogicResponse } from '@/features/chat/types'
|
|
||||||
import { findUniqueVariableValue } from '@/features/variables/findUniqueVariableValue'
|
|
||||||
import { parseVariables } from '@/features/variables/parseVariables'
|
|
||||||
|
|
||||||
export const executeCondition = (
|
export const executeCondition =
|
||||||
{ typebot: { variables } }: SessionState,
|
(variables: Variable[]) =>
|
||||||
block: ConditionBlock
|
(condition: Condition): boolean =>
|
||||||
): ExecuteLogicResponse => {
|
condition.logicalOperator === LogicalOperator.AND
|
||||||
const passedCondition = block.items.find((item) => {
|
? condition.comparisons.every(executeComparison(variables))
|
||||||
const { content } = item
|
: condition.comparisons.some(executeComparison(variables))
|
||||||
const isConditionPassed =
|
|
||||||
content.logicalOperator === LogicalOperator.AND
|
|
||||||
? content.comparisons.every(executeComparison(variables))
|
|
||||||
: content.comparisons.some(executeComparison(variables))
|
|
||||||
return isConditionPassed
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
outgoingEdgeId: passedCondition
|
|
||||||
? passedCondition.outgoingEdgeId
|
|
||||||
: block.outgoingEdgeId,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const executeComparison =
|
const executeComparison =
|
||||||
(variables: Variable[]) =>
|
(variables: Variable[]) =>
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import { ConditionBlock, SessionState } from '@typebot.io/schemas'
|
||||||
|
import { ExecuteLogicResponse } from '@/features/chat/types'
|
||||||
|
import { executeCondition } from './executeCondition'
|
||||||
|
|
||||||
|
export const executeConditionBlock = (
|
||||||
|
{ typebot: { variables } }: SessionState,
|
||||||
|
block: ConditionBlock
|
||||||
|
): ExecuteLogicResponse => {
|
||||||
|
const passedCondition = block.items.find((item) =>
|
||||||
|
executeCondition(variables)(item.content)
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
outgoingEdgeId: passedCondition
|
||||||
|
? passedCondition.outgoingEdgeId
|
||||||
|
: block.outgoingEdgeId,
|
||||||
|
}
|
||||||
|
}
|
@ -55,7 +55,7 @@ export const executeGroup =
|
|||||||
if (isInputBlock(block))
|
if (isInputBlock(block))
|
||||||
return {
|
return {
|
||||||
messages,
|
messages,
|
||||||
input: await injectVariablesValueInBlock(newSessionState)(block),
|
input: await parseInput(newSessionState)(block),
|
||||||
newSessionState: {
|
newSessionState: {
|
||||||
...newSessionState,
|
...newSessionState,
|
||||||
currentBlock: {
|
currentBlock: {
|
||||||
@ -183,7 +183,7 @@ const parseBubbleBlock =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const injectVariablesValueInBlock =
|
const parseInput =
|
||||||
(state: SessionState) =>
|
(state: SessionState) =>
|
||||||
async (block: InputBlock): Promise<ChatReply['input']> => {
|
async (block: InputBlock): Promise<ChatReply['input']> => {
|
||||||
switch (block.type) {
|
switch (block.type) {
|
||||||
|
@ -4,7 +4,7 @@ import { ExecuteLogicResponse } from '../types'
|
|||||||
import { executeScript } from '@/features/blocks/logic/script/executeScript'
|
import { executeScript } from '@/features/blocks/logic/script/executeScript'
|
||||||
import { executeJumpBlock } from '@/features/blocks/logic/jump/executeJumpBlock'
|
import { executeJumpBlock } from '@/features/blocks/logic/jump/executeJumpBlock'
|
||||||
import { executeRedirect } from '@/features/blocks/logic/redirect/executeRedirect'
|
import { executeRedirect } from '@/features/blocks/logic/redirect/executeRedirect'
|
||||||
import { executeCondition } from '@/features/blocks/logic/condition/executeCondition'
|
import { executeConditionBlock } from '@/features/blocks/logic/condition/executeConditionBlock'
|
||||||
import { executeSetVariable } from '@/features/blocks/logic/setVariable/executeSetVariable'
|
import { executeSetVariable } from '@/features/blocks/logic/setVariable/executeSetVariable'
|
||||||
import { executeTypebotLink } from '@/features/blocks/logic/typebotLink/executeTypebotLink'
|
import { executeTypebotLink } from '@/features/blocks/logic/typebotLink/executeTypebotLink'
|
||||||
import { executeAbTest } from '@/features/blocks/logic/abTest/executeAbTest'
|
import { executeAbTest } from '@/features/blocks/logic/abTest/executeAbTest'
|
||||||
@ -16,7 +16,7 @@ export const executeLogic =
|
|||||||
case LogicBlockType.SET_VARIABLE:
|
case LogicBlockType.SET_VARIABLE:
|
||||||
return executeSetVariable(state, block)
|
return executeSetVariable(state, block)
|
||||||
case LogicBlockType.CONDITION:
|
case LogicBlockType.CONDITION:
|
||||||
return executeCondition(state, block)
|
return executeConditionBlock(state, block)
|
||||||
case LogicBlockType.REDIRECT:
|
case LogicBlockType.REDIRECT:
|
||||||
return executeRedirect(state, block)
|
return executeRedirect(state, block)
|
||||||
case LogicBlockType.SCRIPT:
|
case LogicBlockType.SCRIPT:
|
||||||
|
@ -4,6 +4,7 @@ import { itemBaseSchema } from '../../items/baseSchemas'
|
|||||||
import { optionBaseSchema, blockBaseSchema } from '../baseSchemas'
|
import { optionBaseSchema, blockBaseSchema } from '../baseSchemas'
|
||||||
import { defaultButtonLabel } from './constants'
|
import { defaultButtonLabel } from './constants'
|
||||||
import { InputBlockType } from './enums'
|
import { InputBlockType } from './enums'
|
||||||
|
import { conditionSchema } from '../logic/condition'
|
||||||
|
|
||||||
export const choiceInputOptionsSchema = optionBaseSchema.merge(
|
export const choiceInputOptionsSchema = optionBaseSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
@ -26,6 +27,12 @@ export const buttonItemSchema = itemBaseSchema.merge(
|
|||||||
z.object({
|
z.object({
|
||||||
type: z.literal(ItemType.BUTTON),
|
type: z.literal(ItemType.BUTTON),
|
||||||
content: z.string().optional(),
|
content: z.string().optional(),
|
||||||
|
displayCondition: z
|
||||||
|
.object({
|
||||||
|
isEnabled: z.boolean().optional(),
|
||||||
|
condition: conditionSchema.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { itemBaseSchema } from '../../items/baseSchemas'
|
|||||||
import { optionBaseSchema, blockBaseSchema } from '../baseSchemas'
|
import { optionBaseSchema, blockBaseSchema } from '../baseSchemas'
|
||||||
import { defaultButtonLabel } from './constants'
|
import { defaultButtonLabel } from './constants'
|
||||||
import { InputBlockType } from './enums'
|
import { InputBlockType } from './enums'
|
||||||
|
import { conditionSchema } from '../logic/condition'
|
||||||
|
|
||||||
export const pictureChoiceOptionsSchema = optionBaseSchema.merge(
|
export const pictureChoiceOptionsSchema = optionBaseSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
@ -28,6 +29,12 @@ export const pictureChoiceItemSchema = itemBaseSchema.merge(
|
|||||||
pictureSrc: z.string().optional(),
|
pictureSrc: z.string().optional(),
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
|
displayCondition: z
|
||||||
|
.object({
|
||||||
|
isEnabled: z.boolean().optional(),
|
||||||
|
condition: conditionSchema.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ const comparisonSchema = z.object({
|
|||||||
value: z.string().optional(),
|
value: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const conditionContentSchema = z.object({
|
export const conditionSchema = z.object({
|
||||||
logicalOperator: z.nativeEnum(LogicalOperator),
|
logicalOperator: z.nativeEnum(LogicalOperator),
|
||||||
comparisons: z.array(comparisonSchema),
|
comparisons: z.array(comparisonSchema),
|
||||||
})
|
})
|
||||||
@ -37,7 +37,7 @@ const conditionContentSchema = z.object({
|
|||||||
export const conditionItemSchema = itemBaseSchema.merge(
|
export const conditionItemSchema = itemBaseSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
type: z.literal(ItemType.CONDITION),
|
type: z.literal(ItemType.CONDITION),
|
||||||
content: conditionContentSchema,
|
content: conditionSchema,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ export const conditionBlockSchema = blockBaseSchema.merge(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultConditionContent: ConditionContent = {
|
export const defaultConditionContent: Condition = {
|
||||||
comparisons: [],
|
comparisons: [],
|
||||||
logicalOperator: LogicalOperator.AND,
|
logicalOperator: LogicalOperator.AND,
|
||||||
}
|
}
|
||||||
@ -56,4 +56,4 @@ export const defaultConditionContent: ConditionContent = {
|
|||||||
export type ConditionItem = z.infer<typeof conditionItemSchema>
|
export type ConditionItem = z.infer<typeof conditionItemSchema>
|
||||||
export type Comparison = z.infer<typeof comparisonSchema>
|
export type Comparison = z.infer<typeof comparisonSchema>
|
||||||
export type ConditionBlock = z.infer<typeof conditionBlockSchema>
|
export type ConditionBlock = z.infer<typeof conditionBlockSchema>
|
||||||
export type ConditionContent = z.infer<typeof conditionContentSchema>
|
export type Condition = z.infer<typeof conditionSchema>
|
||||||
|
Reference in New Issue
Block a user