2
0

🚸 (condition) Enable multiple condition items in one block

Closes #162
This commit is contained in:
Baptiste Arnaud
2022-11-16 14:56:09 +01:00
parent 96eb77d94b
commit 6725c17a02
24 changed files with 327 additions and 216 deletions

View File

@@ -18,7 +18,7 @@ type Props = {
isMouseOver: boolean isMouseOver: boolean
} }
export const ButtonNodeContent = ({ item, indices, isMouseOver }: Props) => { export const ButtonsItemNode = ({ item, indices, isMouseOver }: Props) => {
const { deleteItem, updateItem, createItem } = useTypebot() const { deleteItem, updateItem, createItem } = useTypebot()
const [initialContent] = useState(item.content ?? '') const [initialContent] = useState(item.content ?? '')
const [itemValue, setItemValue] = useState(item.content ?? 'Click to edit') const [itemValue, setItemValue] = useState(item.content ?? 'Click to edit')

View File

@@ -0,0 +1,3 @@
export * from './ButtonsItemNode'
export * from './ButtonsIcon'
export * from './ButtonsOptionsForm'

View File

@@ -1,3 +1 @@
export { ButtonsOptionsForm } from './components/ButtonsOptionsForm' export * from './components'
export { ButtonNodeContent } from './components/ButtonNodeContent'
export { ButtonsInputIcon } from './components/ButtonsInputIcon'

View File

@@ -1,26 +1,16 @@
import { Flex } from '@chakra-ui/react' import { Flex } from '@chakra-ui/react'
import { DropdownList } from '@/components/DropdownList' import { DropdownList } from '@/components/DropdownList'
import { import { Comparison, ConditionItem, LogicalOperator } from 'models'
Comparison,
ConditionItem,
ConditionBlock,
LogicalOperator,
} from 'models'
import React from 'react' import React from 'react'
import { ComparisonItem } from './ComparisonsItem' import { ComparisonItem } from './ComparisonItem'
import { TableList } from '@/components/TableList' import { TableList } from '@/components/TableList'
type ConditionSettingsBodyProps = { type Props = {
block: ConditionBlock itemContent: ConditionItem['content']
onItemChange: (updates: Partial<ConditionItem>) => void onItemChange: (updates: Partial<ConditionItem>) => void
} }
export const ConditionSettingsBody = ({ export const ConditionItemForm = ({ itemContent, onItemChange }: Props) => {
block,
onItemChange,
}: ConditionSettingsBodyProps) => {
const itemContent = block.items[0].content
const handleComparisonsChange = (comparisons: Comparison[]) => const handleComparisonsChange = (comparisons: Comparison[]) =>
onItemChange({ content: { ...itemContent, comparisons } }) onItemChange({ content: { ...itemContent, comparisons } })
const handleLogicalOperatorChange = (logicalOperator: LogicalOperator) => const handleLogicalOperatorChange = (logicalOperator: LogicalOperator) =>

View File

@@ -0,0 +1 @@
export * from './ConditionItemForm'

View File

@@ -0,0 +1,180 @@
import {
Stack,
Tag,
Text,
Flex,
Wrap,
Fade,
IconButton,
PopoverTrigger,
Popover,
Portal,
PopoverContent,
PopoverArrow,
PopoverBody,
useEventListener,
} from '@chakra-ui/react'
import { useTypebot } from '@/features/editor'
import {
Comparison,
ConditionItem,
ComparisonOperators,
ItemType,
ItemIndices,
} from 'models'
import React, { useRef } from 'react'
import { byId, isNotDefined } from 'utils'
import { PlusIcon } from '@/components/icons'
import { ConditionItemForm } from './ConditionItemForm'
import { useGraph } from '@/features/graph'
import cuid from 'cuid'
type Props = {
item: ConditionItem
isMouseOver: boolean
indices: ItemIndices
}
export const ConditionItemNode = ({ item, isMouseOver, indices }: Props) => {
const { typebot, createItem, updateItem } = useTypebot()
const { openedItemId, setOpenedItemId } = useGraph()
const ref = useRef<HTMLDivElement | null>(null)
const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation()
const openPopover = () => {
setOpenedItemId(item.id)
}
const handleItemChange = (updates: Partial<ConditionItem>) => {
updateItem(indices, { ...item, ...updates })
}
const handlePlusClick = (event: React.MouseEvent) => {
event.stopPropagation()
const itemIndex = indices.itemIndex + 1
const newItemId = cuid()
createItem(
{
blockId: item.blockId,
type: ItemType.CONDITION,
id: newItemId,
},
{ ...indices, itemIndex }
)
setOpenedItemId(newItemId)
}
const handleMouseWheel = (e: WheelEvent) => {
e.stopPropagation()
}
useEventListener('wheel', handleMouseWheel, ref.current)
return (
<Popover
placement="left"
isLazy
isOpen={openedItemId === item.id}
closeOnBlur={false}
>
<PopoverTrigger>
<Flex p={3} pos="relative" w="full" onClick={openPopover}>
{item.content.comparisons.length === 0 ||
comparisonIsEmpty(item.content.comparisons[0]) ? (
<Text color={'gray.500'}>Configure...</Text>
) : (
<Stack maxW="170px">
{item.content.comparisons.map((comparison, idx) => {
const variable = typebot?.variables.find(
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>
{parseComparisonOperatorSymbol(
comparison.comparisonOperator
)}
</Text>
)}
{comparison?.value && (
<Tag bgColor={'gray.200'}>
<Text noOfLines={1}>{comparison.value}</Text>
</Tag>
)}
</Wrap>
)
})}
</Stack>
)}
<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>
</PopoverTrigger>
<Portal>
<PopoverContent pos="relative" onMouseDown={handleMouseDown}>
<PopoverArrow />
<PopoverBody
py="6"
overflowY="scroll"
maxH="400px"
shadow="lg"
ref={ref}
>
<ConditionItemForm
itemContent={item.content}
onItemChange={handleItemChange}
/>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
)
}
const comparisonIsEmpty = (comparison: Comparison) =>
isNotDefined(comparison.comparisonOperator) &&
isNotDefined(comparison.value) &&
isNotDefined(comparison.variableId)
const parseComparisonOperatorSymbol = (operator: ComparisonOperators) => {
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 '!='
}
}

View File

@@ -1,73 +0,0 @@
import { Stack, Tag, Text, Flex, Wrap } from '@chakra-ui/react'
import { useTypebot } from '@/features/editor'
import { Comparison, ConditionItem, ComparisonOperators } from 'models'
import React from 'react'
import { byId, isNotDefined } from 'utils'
type Props = {
item: ConditionItem
}
export const ConditionNodeContent = ({ item }: Props) => {
const { typebot } = useTypebot()
return (
<Flex px={2} py={2}>
{item.content.comparisons.length === 0 ||
comparisonIsEmpty(item.content.comparisons[0]) ? (
<Text color={'gray.500'}>Configure...</Text>
) : (
<Stack maxW="170px">
{item.content.comparisons.map((comparison, idx) => {
const variable = typebot?.variables.find(
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>
{parseComparisonOperatorSymbol(
comparison.comparisonOperator
)}
</Text>
)}
{comparison?.value && (
<Tag bgColor={'gray.200'}>
<Text noOfLines={1}>{comparison.value}</Text>
</Tag>
)}
</Wrap>
)
})}
</Stack>
)}
</Flex>
)
}
const comparisonIsEmpty = (comparison: Comparison) =>
isNotDefined(comparison.comparisonOperator) &&
isNotDefined(comparison.value) &&
isNotDefined(comparison.variableId)
const parseComparisonOperatorSymbol = (operator: ComparisonOperators) => {
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 '!='
}
}

View File

@@ -1 +0,0 @@
export { ConditionSettingsBody } from './ConditonSettingsBody'

View File

@@ -1,3 +1,2 @@
export { ConditionSettingsBody } from './components/ConditionSettingsBody' export * from './components/ConditionItemNode'
export { ConditionNodeContent } from './components/ConditionNodeContent' export * from './components/ConditionIcon'
export { ConditionIcon } from './components/ConditionIcon'

View File

@@ -1,9 +1,9 @@
import { import {
ItemIndices, ItemIndices,
Item, Item,
InputBlockType,
BlockWithItems, BlockWithItems,
ButtonItem, defaultConditionContent,
ItemType,
} from 'models' } from 'models'
import { SetTypebot } from '../TypebotProvider' import { SetTypebot } from '../TypebotProvider'
import produce from 'immer' import produce from 'immer'
@@ -12,10 +12,7 @@ import { byId, blockHasItems } from 'utils'
import cuid from 'cuid' import cuid from 'cuid'
export type ItemsActions = { export type ItemsActions = {
createItem: ( createItem: (item: Item | Omit<Item, 'id'>, indices: ItemIndices) => void
item: ButtonItem | Omit<ButtonItem, 'id'>,
indices: ItemIndices
) => void
updateItem: (indices: ItemIndices, updates: Partial<Omit<Item, 'id'>>) => void updateItem: (indices: ItemIndices, updates: Partial<Omit<Item, 'id'>>) => void
detachItemFromBlock: (indices: ItemIndices) => void detachItemFromBlock: (indices: ItemIndices) => void
deleteItem: (indices: ItemIndices) => void deleteItem: (indices: ItemIndices) => void
@@ -23,18 +20,23 @@ export type ItemsActions = {
const itemsAction = (setTypebot: SetTypebot): ItemsActions => ({ const itemsAction = (setTypebot: SetTypebot): ItemsActions => ({
createItem: ( createItem: (
item: ButtonItem | Omit<ButtonItem, 'id'>, item: Item | Omit<Item, 'id'>,
{ groupIndex, blockIndex, itemIndex }: ItemIndices { groupIndex, blockIndex, itemIndex }: ItemIndices
) => ) =>
setTypebot((typebot) => setTypebot((typebot) =>
produce(typebot, (typebot) => { produce(typebot, (typebot) => {
const block = typebot.groups[groupIndex].blocks[blockIndex] const block = typebot.groups[groupIndex].blocks[
if (block.type !== InputBlockType.CHOICE) return blockIndex
] as BlockWithItems
const newItem = { const newItem = {
...item,
blockId: block.id,
id: 'id' in item ? item.id : cuid(), id: 'id' in item ? item.id : cuid(),
} content:
item.type === ItemType.CONDITION
? defaultConditionContent
: undefined,
...item,
} as Item
if (item.outgoingEdgeId) { if (item.outgoingEdgeId) {
const edgeIndex = typebot.edges.findIndex(byId(item.outgoingEdgeId)) const edgeIndex = typebot.edges.findIndex(byId(item.outgoingEdgeId))
edgeIndex !== -1 edgeIndex !== -1

View File

@@ -48,6 +48,7 @@ export const Graph = ({
const { const {
setGraphPosition: setGlobalGraphPosition, setGraphPosition: setGlobalGraphPosition,
setOpenedBlockId, setOpenedBlockId,
setOpenedItemId,
setPreviewingEdge, setPreviewingEdge,
connectingIds, connectingIds,
} = useGraph() } = useGraph()
@@ -126,6 +127,7 @@ export const Graph = ({
const handleClick = () => { const handleClick = () => {
setOpenedBlockId(undefined) setOpenedBlockId(undefined)
setOpenedItemId(undefined)
setPreviewingEdge(undefined) setPreviewingEdge(undefined)
} }

View File

@@ -9,12 +9,12 @@ import React, { useEffect, useRef, useState } from 'react'
import { import {
BubbleBlock, BubbleBlock,
BubbleBlockContent, BubbleBlockContent,
ConditionBlock,
DraggableBlock, DraggableBlock,
Block, Block,
BlockWithOptions, BlockWithOptions,
TextBubbleContent, TextBubbleContent,
TextBubbleBlock, TextBubbleBlock,
LogicBlockType,
} from 'models' } from 'models'
import { isBubbleBlock, isTextBubbleBlock } from 'utils' import { isBubbleBlock, isTextBubbleBlock } from 'utils'
import { BlockNodeContent } from './BlockNodeContent/BlockNodeContent' import { BlockNodeContent } from './BlockNodeContent/BlockNodeContent'
@@ -28,7 +28,12 @@ import { BlockSettings } from './SettingsPopoverContent/SettingsPopoverContent'
import { TextBubbleEditor } from '../../../../blocks/bubbles/textBubble/components/TextBubbleEditor' import { TextBubbleEditor } from '../../../../blocks/bubbles/textBubble/components/TextBubbleEditor'
import { TargetEndpoint } from '../../Endpoints' import { TargetEndpoint } from '../../Endpoints'
import { MediaBubblePopoverContent } from './MediaBubblePopoverContent' import { MediaBubblePopoverContent } from './MediaBubblePopoverContent'
import { NodePosition, useDragDistance, useGraph } from '../../../providers' import {
NodePosition,
useBlockDnd,
useDragDistance,
useGraph,
} from '../../../providers'
import { ContextMenu } from '@/components/ContextMenu' import { ContextMenu } from '@/components/ContextMenu'
import { setMultipleRefs } from '@/utils/helpers' import { setMultipleRefs } from '@/utils/helpers'
import { hasDefaultConnector } from '../../../utils' import { hasDefaultConnector } from '../../../utils'
@@ -53,6 +58,7 @@ export const BlockNode = ({
setFocusedGroupId, setFocusedGroupId,
previewingEdge, previewingEdge,
} = useGraph() } = useGraph()
const { mouseOverBlock, setMouseOverBlock, draggedItem } = useBlockDnd()
const { typebot, updateBlock } = useTypebot() const { typebot, updateBlock } = useTypebot()
const [isConnecting, setIsConnecting] = useState(false) const [isConnecting, setIsConnecting] = useState(false)
const [isPopoverOpened, setIsPopoverOpened] = useState( const [isPopoverOpened, setIsPopoverOpened] = useState(
@@ -99,6 +105,8 @@ export const BlockNode = ({
} }
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (draggedItem !== undefined)
setMouseOverBlock({ id: block.id, ref: blockRef })
if (connectingIds) if (connectingIds)
setConnectingIds({ setConnectingIds({
...connectingIds, ...connectingIds,
@@ -107,6 +115,7 @@ export const BlockNode = ({
} }
const handleMouseLeave = () => { const handleMouseLeave = () => {
if (mouseOverBlock) setMouseOverBlock(undefined)
if (connectingIds?.target) if (connectingIds?.target)
setConnectingIds({ setConnectingIds({
...connectingIds, ...connectingIds,
@@ -238,9 +247,8 @@ export const BlockNode = ({
) )
} }
const hasSettingsPopover = ( const hasSettingsPopover = (block: Block): block is BlockWithOptions =>
block: Block !isBubbleBlock(block) && block.type !== LogicBlockType.CONDITION
): block is BlockWithOptions | ConditionBlock => !isBubbleBlock(block)
const isMediaBubbleBlock = ( const isMediaBubbleBlock = (
block: Block block: Block

View File

@@ -8,8 +8,6 @@ import {
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { ExpandIcon } from '@/components/icons' import { ExpandIcon } from '@/components/icons'
import { import {
ConditionItem,
ConditionBlock,
InputBlockType, InputBlockType,
IntegrationBlockType, IntegrationBlockType,
LogicBlockType, LogicBlockType,
@@ -34,7 +32,6 @@ import { SendEmailSettings } from '@/features/blocks/integrations/sendEmail'
import { WebhookSettings } from '@/features/blocks/integrations/webhook' import { WebhookSettings } from '@/features/blocks/integrations/webhook'
import { ZapierSettings } from '@/features/blocks/integrations/zapier' import { ZapierSettings } from '@/features/blocks/integrations/zapier'
import { CodeSettings } from '@/features/blocks/logic/code' import { CodeSettings } from '@/features/blocks/logic/code'
import { ConditionSettingsBody } from '@/features/blocks/logic/condition'
import { RedirectSettings } from '@/features/blocks/logic/redirect' import { RedirectSettings } from '@/features/blocks/logic/redirect'
import { SetVariableSettings } from '@/features/blocks/logic/setVariable' import { SetVariableSettings } from '@/features/blocks/logic/setVariable'
import { TypebotLinkSettingsForm } from '@/features/blocks/logic/typebotLink' import { TypebotLinkSettingsForm } from '@/features/blocks/logic/typebotLink'
@@ -42,7 +39,7 @@ import { ButtonsOptionsForm } from '@/features/blocks/inputs/buttons'
import { ChatwootSettingsForm } from '@/features/blocks/integrations/chatwoot' import { ChatwootSettingsForm } from '@/features/blocks/integrations/chatwoot'
type Props = { type Props = {
block: BlockWithOptions | ConditionBlock block: BlockWithOptions
webhook?: Webhook webhook?: Webhook
onExpandClick: () => void onExpandClick: () => void
onBlockChange: (updates: Partial<Block>) => void onBlockChange: (updates: Partial<Block>) => void
@@ -88,18 +85,14 @@ export const BlockSettings = ({
block, block,
onBlockChange, onBlockChange,
}: { }: {
block: BlockWithOptions | ConditionBlock block: BlockWithOptions
webhook?: Webhook webhook?: Webhook
onBlockChange: (block: Partial<Block>) => void onBlockChange: (block: Partial<Block>) => void
}): JSX.Element => { }): JSX.Element => {
const handleOptionsChange = (options: BlockOptions) => { const handleOptionsChange = (options: BlockOptions) => {
onBlockChange({ options } as Partial<Block>) onBlockChange({ options } as Partial<Block>)
} }
const handleItemChange = (updates: Partial<ConditionItem>) => {
onBlockChange({
items: [{ ...(block as ConditionBlock).items[0], ...updates }],
} as Partial<Block>)
}
switch (block.type) { switch (block.type) {
case InputBlockType.TEXT: { case InputBlockType.TEXT: {
return ( return (
@@ -189,11 +182,6 @@ export const BlockSettings = ({
/> />
) )
} }
case LogicBlockType.CONDITION: {
return (
<ConditionSettingsBody block={block} onItemChange={handleItemChange} />
)
}
case LogicBlockType.REDIRECT: { case LogicBlockType.REDIRECT: {
return ( return (
<RedirectSettings <RedirectSettings

View File

@@ -6,13 +6,7 @@ import {
useDragDistance, useDragDistance,
} from '../../../providers' } from '../../../providers'
import { useTypebot } from '@/features/editor' import { useTypebot } from '@/features/editor'
import { import { ChoiceInputBlock, Item, ItemIndices } from 'models'
ButtonItem,
ChoiceInputBlock,
Item,
ItemIndices,
ItemType,
} from 'models'
import React, { useRef, useState } from 'react' import React, { useRef, useState } from 'react'
import { SourceEndpoint } from '../../Endpoints/SourceEndpoint' import { SourceEndpoint } from '../../Endpoints/SourceEndpoint'
import { ItemNodeContent } from './ItemNodeContent' import { ItemNodeContent } from './ItemNodeContent'
@@ -25,30 +19,37 @@ type Props = {
indices: ItemIndices indices: ItemIndices
onMouseDown?: ( onMouseDown?: (
blockNodePosition: { absolute: Coordinates; relative: Coordinates }, blockNodePosition: { absolute: Coordinates; relative: Coordinates },
item: ButtonItem item: Item
) => void ) => void
connectionDisabled?: boolean
} }
export const ItemNode = ({ item, indices, onMouseDown }: Props) => { export const ItemNode = ({
item,
indices,
onMouseDown,
connectionDisabled,
}: Props) => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
const { previewingEdge } = useGraph() const { previewingEdge } = useGraph()
const [isMouseOver, setIsMouseOver] = useState(false) const [isMouseOver, setIsMouseOver] = useState(false)
const itemRef = useRef<HTMLDivElement | null>(null) const itemRef = useRef<HTMLDivElement | null>(null)
const isPreviewing = previewingEdge?.from.itemId === item.id const isPreviewing = previewingEdge?.from.itemId === item.id
const isConnectable = !( const isConnectable =
typebot?.groups[indices.groupIndex].blocks[ !connectionDisabled &&
indices.blockIndex !(
] as ChoiceInputBlock typebot?.groups[indices.groupIndex].blocks[
)?.options?.isMultipleChoice indices.blockIndex
const isReadOnly = item.type === ItemType.CONDITION ] as ChoiceInputBlock
)?.options?.isMultipleChoice
const onDrag = (position: NodePosition) => { const onDrag = (position: NodePosition) => {
if (!onMouseDown || item.type !== ItemType.BUTTON) return if (!onMouseDown) return
onMouseDown(position, item) onMouseDown(position, item)
} }
useDragDistance({ useDragDistance({
ref: itemRef, ref: itemRef,
onDrag, onDrag,
isDisabled: !onMouseDown || item.type !== ItemType.BUTTON, isDisabled: !onMouseDown,
}) })
const handleMouseEnter = () => setIsMouseOver(true) const handleMouseEnter = () => setIsMouseOver(true)
@@ -63,19 +64,19 @@ export const ItemNode = ({ item, indices, onMouseDown }: Props) => {
data-testid="item" data-testid="item"
pos="relative" pos="relative"
ref={setMultipleRefs([ref, itemRef])} ref={setMultipleRefs([ref, itemRef])}
w="full"
> >
<Flex <Flex
align="center" align="center"
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
shadow="sm" shadow="sm"
_hover={isReadOnly ? {} : { shadow: 'md' }} _hover={{ shadow: 'md' }}
transition="box-shadow 200ms, border-color 200ms" transition="box-shadow 200ms, border-color 200ms"
rounded="md" rounded="md"
borderWidth={isOpened || isPreviewing ? '2px' : '1px'} borderWidth={isOpened || isPreviewing ? '2px' : '1px'}
borderColor={isOpened || isPreviewing ? 'blue.400' : 'gray.100'} borderColor={isOpened || isPreviewing ? 'blue.400' : 'gray.100'}
margin={isOpened || isPreviewing ? '-1px' : 0} margin={isOpened || isPreviewing ? '-1px' : 0}
pointerEvents={isReadOnly ? 'none' : 'all'}
bgColor="white" bgColor="white"
w="full" w="full"
> >

View File

@@ -1,5 +1,5 @@
import { ButtonNodeContent } from '@/features/blocks/inputs/buttons' import { ButtonsItemNode } from '@/features/blocks/inputs/buttons'
import { ConditionNodeContent } from '@/features/blocks/logic/condition' import { ConditionItemNode } from '@/features/blocks/logic/condition'
import { Item, ItemIndices, ItemType } from 'models' import { Item, ItemIndices, ItemType } from 'models'
import React from 'react' import React from 'react'
@@ -13,13 +13,19 @@ export const ItemNodeContent = ({ item, indices, isMouseOver }: Props) => {
switch (item.type) { switch (item.type) {
case ItemType.BUTTON: case ItemType.BUTTON:
return ( return (
<ButtonNodeContent <ButtonsItemNode
item={item} item={item}
isMouseOver={isMouseOver} isMouseOver={isMouseOver}
indices={indices} indices={indices}
/> />
) )
case ItemType.CONDITION: case ItemType.CONDITION:
return <ConditionNodeContent item={item} /> return (
<ConditionItemNode
item={item}
isMouseOver={isMouseOver}
indices={indices}
/>
)
} }
} }

View File

@@ -6,16 +6,10 @@ import {
useGraph, useGraph,
} from '../../../providers' } from '../../../providers'
import { useTypebot } from '@/features/editor' import { useTypebot } from '@/features/editor'
import { import { BlockIndices, BlockWithItems, LogicBlockType, Item } from 'models'
ButtonItem,
BlockIndices,
BlockWithItems,
LogicBlockType,
} from 'models'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { ItemNode } from './ItemNode' import { ItemNode } from './ItemNode'
import { SourceEndpoint } from '../../Endpoints' import { SourceEndpoint } from '../../Endpoints'
import { ItemNodeOverlay } from './ItemNodeOverlay'
type Props = { type Props = {
block: BlockWithItems block: BlockWithItems
@@ -27,14 +21,13 @@ export const ItemNodesList = ({
indices: { groupIndex, blockIndex }, indices: { groupIndex, blockIndex },
}: Props) => { }: Props) => {
const { typebot, createItem, detachItemFromBlock } = useTypebot() const { typebot, createItem, detachItemFromBlock } = useTypebot()
const { draggedItem, setDraggedItem, mouseOverGroup } = useBlockDnd() const { draggedItem, setDraggedItem, mouseOverBlock } = useBlockDnd()
const placeholderRefs = useRef<HTMLDivElement[]>([]) const placeholderRefs = useRef<HTMLDivElement[]>([])
const { graphPosition } = useGraph() const { graphPosition } = useGraph()
const groupId = typebot?.groups[groupIndex].id const isDraggingOnCurrentBlock =
const isDraggingOnCurrentGroup = (draggedItem && mouseOverBlock?.id === block.id) ?? false
(draggedItem && mouseOverGroup?.id === groupId) ?? false const showPlaceholders =
const isReadOnly = block.type === LogicBlockType.CONDITION draggedItem !== undefined && block.items[0].type === draggedItem.type
const showPlaceholders = draggedItem && !isReadOnly
const isLastBlock = const isLastBlock =
typebot?.groups[groupIndex].blocks[blockIndex + 1] === undefined typebot?.groups[groupIndex].blocks[blockIndex + 1] === undefined
@@ -60,29 +53,37 @@ export const ItemNodesList = ({
useEventListener('mousemove', handleGlobalMouseMove) useEventListener('mousemove', handleGlobalMouseMove)
useEffect(() => { useEffect(() => {
if (mouseOverGroup?.id !== block.groupId) if (!showPlaceholders) return
if (mouseOverBlock?.id !== block.id) {
setExpandedPlaceholderIndex(undefined) setExpandedPlaceholderIndex(undefined)
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [mouseOverGroup?.id]) }, [mouseOverBlock?.id, showPlaceholders])
const handleMouseMoveOnGroup = (event: MouseEvent) => { const handleMouseMoveOnBlock = (event: MouseEvent) => {
if (!isDraggingOnCurrentGroup || isReadOnly) return if (!isDraggingOnCurrentBlock) return
const index = computeNearestPlaceholderIndex(event.pageY, placeholderRefs) const index = computeNearestPlaceholderIndex(event.pageY, placeholderRefs)
setExpandedPlaceholderIndex(index) setExpandedPlaceholderIndex(index)
} }
useEventListener( useEventListener(
'mousemove', 'mousemove',
handleMouseMoveOnGroup, handleMouseMoveOnBlock,
mouseOverGroup?.ref.current mouseOverBlock?.ref.current
) )
const handleMouseUpOnGroup = (e: MouseEvent) => { const handleMouseUpOnGroup = (e: MouseEvent) => {
if (
!showPlaceholders ||
!isDraggingOnCurrentBlock ||
!draggedItem ||
mouseOverBlock?.id !== block.id
)
return
setExpandedPlaceholderIndex(undefined) setExpandedPlaceholderIndex(undefined)
if (!isDraggingOnCurrentGroup) return
const itemIndex = computeNearestPlaceholderIndex(e.pageY, placeholderRefs) const itemIndex = computeNearestPlaceholderIndex(e.pageY, placeholderRefs)
e.stopPropagation() e.stopPropagation()
setDraggedItem(undefined) setDraggedItem(undefined)
createItem(draggedItem as ButtonItem, { createItem(draggedItem, {
groupIndex, groupIndex,
blockIndex, blockIndex,
itemIndex, itemIndex,
@@ -91,7 +92,7 @@ export const ItemNodesList = ({
useEventListener( useEventListener(
'mouseup', 'mouseup',
handleMouseUpOnGroup, handleMouseUpOnGroup,
mouseOverGroup?.ref.current, mouseOverBlock?.ref.current,
{ {
capture: true, capture: true,
} }
@@ -101,9 +102,9 @@ export const ItemNodesList = ({
(itemIndex: number) => (itemIndex: number) =>
( (
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates }, { absolute, relative }: { absolute: Coordinates; relative: Coordinates },
item: ButtonItem item: Item
) => { ) => {
if (!typebot || isReadOnly) return if (!typebot || block.items.length <= 1) return
placeholderRefs.current.splice(itemIndex + 1, 1) placeholderRefs.current.splice(itemIndex + 1, 1)
detachItemFromBlock({ groupIndex, blockIndex, itemIndex }) detachItemFromBlock({ groupIndex, blockIndex, itemIndex })
setPosition(absolute) setPosition(absolute)
@@ -119,13 +120,7 @@ export const ItemNodesList = ({
} }
return ( return (
<Stack <Stack flex={1} spacing={1} maxW="full" onClick={stopPropagating}>
flex={1}
spacing={1}
maxW="full"
onClick={stopPropagating}
pointerEvents={isReadOnly ? 'none' : 'all'}
>
<Flex <Flex
ref={handlePushElementRef(0)} ref={handlePushElementRef(0)}
h={showPlaceholders && expandedPlaceholderIndex === 0 ? '50px' : '2px'} h={showPlaceholders && expandedPlaceholderIndex === 0 ? '50px' : '2px'}
@@ -161,13 +156,15 @@ export const ItemNodesList = ({
py="2" py="2"
borderWidth="1px" borderWidth="1px"
borderColor="gray.300" borderColor="gray.300"
bgColor={isReadOnly ? '' : 'gray.50'} bgColor={'gray.50'}
rounded="md" rounded="md"
pos="relative" pos="relative"
align="center" align="center"
cursor={isReadOnly ? 'pointer' : 'not-allowed'} cursor="not-allowed"
> >
<Text color={isReadOnly ? 'inherit' : 'gray.500'}>Default</Text> <Text color="gray.500">
{block.type === LogicBlockType.CONDITION ? 'Else' : 'Default'}
</Text>
<SourceEndpoint <SourceEndpoint
source={{ source={{
groupId: block.groupId, groupId: block.groupId,
@@ -181,16 +178,23 @@ export const ItemNodesList = ({
{draggedItem && draggedItem.blockId === block.id && ( {draggedItem && draggedItem.blockId === block.id && (
<Portal> <Portal>
<ItemNodeOverlay <Flex
item={draggedItem} pointerEvents="none"
pos="fixed" pos="fixed"
top="0" top="0"
left="0" left="0"
style={{ style={{
transform: `translate(${position.x}px, ${position.y}px) rotate(-2deg) scale(${graphPosition.scale})`, transform: `translate(${position.x}px, ${position.y}px) rotate(-2deg) scale(${graphPosition.scale})`,
}} }}
w="220px"
transformOrigin="0 0 0" transformOrigin="0 0 0"
/> >
<ItemNode
item={draggedItem}
indices={{ groupIndex, blockIndex, itemIndex: 0 }}
connectionDisabled
/>
</Flex>
</Portal> </Portal>
)} )}
</Stack> </Stack>

View File

@@ -1,5 +1,5 @@
import { useEventListener } from '@chakra-ui/react' import { useEventListener } from '@chakra-ui/react'
import { ButtonItem, DraggableBlock, DraggableBlockType } from 'models' import { DraggableBlock, DraggableBlockType, Item } from 'models'
import { import {
createContext, createContext,
Dispatch, Dispatch,
@@ -11,7 +11,7 @@ import {
} from 'react' } from 'react'
import { Coordinates } from './GraphProvider' import { Coordinates } from './GraphProvider'
type GroupInfo = { type NodeInfo = {
id: string id: string
ref: React.MutableRefObject<HTMLDivElement | null> ref: React.MutableRefObject<HTMLDivElement | null>
} }
@@ -21,10 +21,12 @@ const graphDndContext = createContext<{
setDraggedBlockType: Dispatch<SetStateAction<DraggableBlockType | undefined>> setDraggedBlockType: Dispatch<SetStateAction<DraggableBlockType | undefined>>
draggedBlock?: DraggableBlock draggedBlock?: DraggableBlock
setDraggedBlock: Dispatch<SetStateAction<DraggableBlock | undefined>> setDraggedBlock: Dispatch<SetStateAction<DraggableBlock | undefined>>
draggedItem?: ButtonItem draggedItem?: Item
setDraggedItem: Dispatch<SetStateAction<ButtonItem | undefined>> setDraggedItem: Dispatch<SetStateAction<Item | undefined>>
mouseOverGroup?: GroupInfo mouseOverGroup?: NodeInfo
setMouseOverGroup: Dispatch<SetStateAction<GroupInfo | undefined>> setMouseOverGroup: Dispatch<SetStateAction<NodeInfo | undefined>>
mouseOverBlock?: NodeInfo
setMouseOverBlock: Dispatch<SetStateAction<NodeInfo | undefined>>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
}>({}) }>({})
@@ -36,8 +38,9 @@ export const GraphDndProvider = ({ children }: { children: ReactNode }) => {
const [draggedBlockType, setDraggedBlockType] = useState< const [draggedBlockType, setDraggedBlockType] = useState<
DraggableBlockType | undefined DraggableBlockType | undefined
>() >()
const [draggedItem, setDraggedItem] = useState<ButtonItem | undefined>() const [draggedItem, setDraggedItem] = useState<Item | undefined>()
const [mouseOverGroup, setMouseOverGroup] = useState<GroupInfo>() const [mouseOverGroup, setMouseOverGroup] = useState<NodeInfo>()
const [mouseOverBlock, setMouseOverBlock] = useState<NodeInfo>()
return ( return (
<graphDndContext.Provider <graphDndContext.Provider
@@ -50,6 +53,8 @@ export const GraphDndProvider = ({ children }: { children: ReactNode }) => {
setDraggedItem, setDraggedItem,
mouseOverGroup, mouseOverGroup,
setMouseOverGroup, setMouseOverGroup,
mouseOverBlock,
setMouseOverBlock,
}} }}
> >
{children} {children}

View File

@@ -69,6 +69,8 @@ const graphContext = createContext<{
addTargetEndpoint: (endpoint: Endpoint) => void addTargetEndpoint: (endpoint: Endpoint) => void
openedBlockId?: string openedBlockId?: string
setOpenedBlockId: Dispatch<SetStateAction<string | undefined>> setOpenedBlockId: Dispatch<SetStateAction<string | undefined>>
openedItemId?: string
setOpenedItemId: Dispatch<SetStateAction<string | undefined>>
isReadOnly: boolean isReadOnly: boolean
focusedGroupId?: string focusedGroupId?: string
setFocusedGroupId: Dispatch<SetStateAction<string | undefined>> setFocusedGroupId: Dispatch<SetStateAction<string | undefined>>
@@ -92,6 +94,7 @@ export const GraphProvider = ({
const [sourceEndpoints, setSourceEndpoints] = useState<IdMap<Endpoint>>({}) const [sourceEndpoints, setSourceEndpoints] = useState<IdMap<Endpoint>>({})
const [targetEndpoints, setTargetEndpoints] = useState<IdMap<Endpoint>>({}) const [targetEndpoints, setTargetEndpoints] = useState<IdMap<Endpoint>>({})
const [openedBlockId, setOpenedBlockId] = useState<string>() const [openedBlockId, setOpenedBlockId] = useState<string>()
const [openedItemId, setOpenedItemId] = useState<string>()
const [focusedGroupId, setFocusedGroupId] = useState<string>() const [focusedGroupId, setFocusedGroupId] = useState<string>()
const addSourceEndpoint = (endpoint: Endpoint) => { const addSourceEndpoint = (endpoint: Endpoint) => {
@@ -123,6 +126,8 @@ export const GraphProvider = ({
addTargetEndpoint, addTargetEndpoint,
openedBlockId, openedBlockId,
setOpenedBlockId, setOpenedBlockId,
openedItemId,
setOpenedItemId,
isReadOnly, isReadOnly,
focusedGroupId, focusedGroupId,
setFocusedGroupId, setFocusedGroupId,

View File

@@ -53,8 +53,7 @@
"id": "eMk84KvFM53sBxchTeackR", "id": "eMk84KvFM53sBxchTeackR",
"blocks": [ "blocks": [
{ {
"id": "s5hz7HQki66cwELvk2738MJ", "id": "sv8uvEXgYWQNMfZWcdbfyCs",
"groupId": "eMk84KvFM53sBxchTeackR",
"type": "Condition", "type": "Condition",
"items": [ "items": [
{ {
@@ -70,13 +69,7 @@
"logicalOperator": "AND" "logicalOperator": "AND"
}, },
"outgoingEdgeId": "nDjMjM11xPQF7c9Be6ukdY" "outgoingEdgeId": "nDjMjM11xPQF7c9Be6ukdY"
} },
]
},
{
"id": "sv8uvEXgYWQNMfZWcdbfyCs",
"type": "Condition",
"items": [
{ {
"id": "ijYfW38tGhCMRrCtmR3bcr", "id": "ijYfW38tGhCMRrCtmR3bcr",
"type": 1, "type": 1,

View File

@@ -13,14 +13,15 @@ export const executeCondition = (
block: ConditionBlock, block: ConditionBlock,
{ typebot: { variables } }: LogicState { typebot: { variables } }: LogicState
): EdgeId | undefined => { ): EdgeId | undefined => {
const { content } = block.items[0] const passedCondition = block.items.find((item) => {
const isConditionPassed = const { content } = item
content.logicalOperator === LogicalOperator.AND const isConditionPassed =
? content.comparisons.every(executeComparison(variables)) content.logicalOperator === LogicalOperator.AND
: content.comparisons.some(executeComparison(variables)) ? content.comparisons.every(executeComparison(variables))
return isConditionPassed : content.comparisons.some(executeComparison(variables))
? block.items[0].outgoingEdgeId return isConditionPassed
: block.outgoingEdgeId })
return passedCondition ? passedCondition.outgoingEdgeId : block.outgoingEdgeId
} }
const executeComparison = const executeComparison =

View File

@@ -8,7 +8,7 @@ export type ItemIndices = {
groupIndex: number groupIndex: number
itemIndex: number itemIndex: number
} }
const itemScema = buttonItemSchema.or(conditionItemSchema) const itemSchema = buttonItemSchema.or(conditionItemSchema)
export type ItemBase = z.infer<typeof itemBaseSchema> export type ItemBase = z.infer<typeof itemBaseSchema>
export type Item = z.infer<typeof itemScema> export type Item = z.infer<typeof itemSchema>

View File

@@ -12,7 +12,6 @@ export const toggle = () => {
? closePopup(existingPopup) ? closePopup(existingPopup)
: openPopup(existingPopup) : openPopup(existingPopup)
const existingBubble = document.querySelector('#typebot-bubble') const existingBubble = document.querySelector('#typebot-bubble')
console.log(existingBubble)
if (existingBubble) if (existingBubble)
isIframeOpened(existingBubble) isIframeOpened(existingBubble)
? closeIframe(existingBubble) ? closeIframe(existingBubble)