diff --git a/apps/builder/src/features/blocks/inputs/buttons/components/ButtonsInputIcon.tsx b/apps/builder/src/features/blocks/inputs/buttons/components/ButtonsIcon.tsx similarity index 100% rename from apps/builder/src/features/blocks/inputs/buttons/components/ButtonsInputIcon.tsx rename to apps/builder/src/features/blocks/inputs/buttons/components/ButtonsIcon.tsx diff --git a/apps/builder/src/features/blocks/inputs/buttons/components/ButtonNodeContent.tsx b/apps/builder/src/features/blocks/inputs/buttons/components/ButtonsItemNode.tsx similarity index 96% rename from apps/builder/src/features/blocks/inputs/buttons/components/ButtonNodeContent.tsx rename to apps/builder/src/features/blocks/inputs/buttons/components/ButtonsItemNode.tsx index 44a2bd433c..00a0e13ee9 100644 --- a/apps/builder/src/features/blocks/inputs/buttons/components/ButtonNodeContent.tsx +++ b/apps/builder/src/features/blocks/inputs/buttons/components/ButtonsItemNode.tsx @@ -18,7 +18,7 @@ type Props = { isMouseOver: boolean } -export const ButtonNodeContent = ({ item, indices, isMouseOver }: Props) => { +export const ButtonsItemNode = ({ item, indices, isMouseOver }: Props) => { const { deleteItem, updateItem, createItem } = useTypebot() const [initialContent] = useState(item.content ?? '') const [itemValue, setItemValue] = useState(item.content ?? 'Click to edit') diff --git a/apps/builder/src/features/blocks/inputs/buttons/components/index.ts b/apps/builder/src/features/blocks/inputs/buttons/components/index.ts new file mode 100644 index 0000000000..eb5b2eaabc --- /dev/null +++ b/apps/builder/src/features/blocks/inputs/buttons/components/index.ts @@ -0,0 +1,3 @@ +export * from './ButtonsItemNode' +export * from './ButtonsIcon' +export * from './ButtonsOptionsForm' diff --git a/apps/builder/src/features/blocks/inputs/buttons/index.ts b/apps/builder/src/features/blocks/inputs/buttons/index.ts index 68a09e1003..cb64ac1b52 100644 --- a/apps/builder/src/features/blocks/inputs/buttons/index.ts +++ b/apps/builder/src/features/blocks/inputs/buttons/index.ts @@ -1,3 +1 @@ -export { ButtonsOptionsForm } from './components/ButtonsOptionsForm' -export { ButtonNodeContent } from './components/ButtonNodeContent' -export { ButtonsInputIcon } from './components/ButtonsInputIcon' +export * from './components' diff --git a/apps/builder/src/features/blocks/logic/condition/components/ConditionSettingsBody/ComparisonsItem.tsx b/apps/builder/src/features/blocks/logic/condition/components/ConditionItemForm/ComparisonItem.tsx similarity index 100% rename from apps/builder/src/features/blocks/logic/condition/components/ConditionSettingsBody/ComparisonsItem.tsx rename to apps/builder/src/features/blocks/logic/condition/components/ConditionItemForm/ComparisonItem.tsx diff --git a/apps/builder/src/features/blocks/logic/condition/components/ConditionSettingsBody/ConditonSettingsBody.tsx b/apps/builder/src/features/blocks/logic/condition/components/ConditionItemForm/ConditionItemForm.tsx similarity index 74% rename from apps/builder/src/features/blocks/logic/condition/components/ConditionSettingsBody/ConditonSettingsBody.tsx rename to apps/builder/src/features/blocks/logic/condition/components/ConditionItemForm/ConditionItemForm.tsx index 4993d7048f..fdefc1827c 100644 --- a/apps/builder/src/features/blocks/logic/condition/components/ConditionSettingsBody/ConditonSettingsBody.tsx +++ b/apps/builder/src/features/blocks/logic/condition/components/ConditionItemForm/ConditionItemForm.tsx @@ -1,26 +1,16 @@ import { Flex } from '@chakra-ui/react' import { DropdownList } from '@/components/DropdownList' -import { - Comparison, - ConditionItem, - ConditionBlock, - LogicalOperator, -} from 'models' +import { Comparison, ConditionItem, LogicalOperator } from 'models' import React from 'react' -import { ComparisonItem } from './ComparisonsItem' +import { ComparisonItem } from './ComparisonItem' import { TableList } from '@/components/TableList' -type ConditionSettingsBodyProps = { - block: ConditionBlock +type Props = { + itemContent: ConditionItem['content'] onItemChange: (updates: Partial) => void } -export const ConditionSettingsBody = ({ - block, - onItemChange, -}: ConditionSettingsBodyProps) => { - const itemContent = block.items[0].content - +export const ConditionItemForm = ({ itemContent, onItemChange }: Props) => { const handleComparisonsChange = (comparisons: Comparison[]) => onItemChange({ content: { ...itemContent, comparisons } }) const handleLogicalOperatorChange = (logicalOperator: LogicalOperator) => diff --git a/apps/builder/src/features/blocks/logic/condition/components/ConditionItemForm/index.tsx b/apps/builder/src/features/blocks/logic/condition/components/ConditionItemForm/index.tsx new file mode 100644 index 0000000000..768d58624c --- /dev/null +++ b/apps/builder/src/features/blocks/logic/condition/components/ConditionItemForm/index.tsx @@ -0,0 +1 @@ +export * from './ConditionItemForm' diff --git a/apps/builder/src/features/blocks/logic/condition/components/ConditionItemNode.tsx b/apps/builder/src/features/blocks/logic/condition/components/ConditionItemNode.tsx new file mode 100644 index 0000000000..89445986af --- /dev/null +++ b/apps/builder/src/features/blocks/logic/condition/components/ConditionItemNode.tsx @@ -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(null) + + const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation() + + const openPopover = () => { + setOpenedItemId(item.id) + } + + const handleItemChange = (updates: Partial) => { + 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 ( + + + + {item.content.comparisons.length === 0 || + comparisonIsEmpty(item.content.comparisons[0]) ? ( + Configure... + ) : ( + + {item.content.comparisons.map((comparison, idx) => { + const variable = typebot?.variables.find( + byId(comparison.variableId) + ) + return ( + + {idx > 0 && ( + {item.content.logicalOperator ?? ''} + )} + {variable?.name && ( + + {variable.name} + + )} + {comparison.comparisonOperator && ( + + {parseComparisonOperatorSymbol( + comparison.comparisonOperator + )} + + )} + {comparison?.value && ( + + {comparison.value} + + )} + + ) + })} + + )} + + } + size="xs" + shadow="md" + colorScheme="gray" + onClick={handlePlusClick} + /> + + + + + + + + + + + + + ) +} + +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 '!=' + } +} diff --git a/apps/builder/src/features/blocks/logic/condition/components/ConditionNodeContent.tsx b/apps/builder/src/features/blocks/logic/condition/components/ConditionNodeContent.tsx deleted file mode 100644 index cdba55d265..0000000000 --- a/apps/builder/src/features/blocks/logic/condition/components/ConditionNodeContent.tsx +++ /dev/null @@ -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 ( - - {item.content.comparisons.length === 0 || - comparisonIsEmpty(item.content.comparisons[0]) ? ( - Configure... - ) : ( - - {item.content.comparisons.map((comparison, idx) => { - const variable = typebot?.variables.find( - byId(comparison.variableId) - ) - return ( - - {idx > 0 && {item.content.logicalOperator ?? ''}} - {variable?.name && ( - - {variable.name} - - )} - {comparison.comparisonOperator && ( - - {parseComparisonOperatorSymbol( - comparison.comparisonOperator - )} - - )} - {comparison?.value && ( - - {comparison.value} - - )} - - ) - })} - - )} - - ) -} - -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 '!=' - } -} diff --git a/apps/builder/src/features/blocks/logic/condition/components/ConditionSettingsBody/index.tsx b/apps/builder/src/features/blocks/logic/condition/components/ConditionSettingsBody/index.tsx deleted file mode 100644 index 30946bfaa6..0000000000 --- a/apps/builder/src/features/blocks/logic/condition/components/ConditionSettingsBody/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { ConditionSettingsBody } from './ConditonSettingsBody' diff --git a/apps/builder/src/features/blocks/logic/condition/index.ts b/apps/builder/src/features/blocks/logic/condition/index.ts index 166577a9b5..42ee1370d8 100644 --- a/apps/builder/src/features/blocks/logic/condition/index.ts +++ b/apps/builder/src/features/blocks/logic/condition/index.ts @@ -1,3 +1,2 @@ -export { ConditionSettingsBody } from './components/ConditionSettingsBody' -export { ConditionNodeContent } from './components/ConditionNodeContent' -export { ConditionIcon } from './components/ConditionIcon' +export * from './components/ConditionItemNode' +export * from './components/ConditionIcon' diff --git a/apps/builder/src/features/editor/providers/TypebotProvider/actions/items.ts b/apps/builder/src/features/editor/providers/TypebotProvider/actions/items.ts index 430054ab5b..6fe565566c 100644 --- a/apps/builder/src/features/editor/providers/TypebotProvider/actions/items.ts +++ b/apps/builder/src/features/editor/providers/TypebotProvider/actions/items.ts @@ -1,9 +1,9 @@ import { ItemIndices, Item, - InputBlockType, BlockWithItems, - ButtonItem, + defaultConditionContent, + ItemType, } from 'models' import { SetTypebot } from '../TypebotProvider' import produce from 'immer' @@ -12,10 +12,7 @@ import { byId, blockHasItems } from 'utils' import cuid from 'cuid' export type ItemsActions = { - createItem: ( - item: ButtonItem | Omit, - indices: ItemIndices - ) => void + createItem: (item: Item | Omit, indices: ItemIndices) => void updateItem: (indices: ItemIndices, updates: Partial>) => void detachItemFromBlock: (indices: ItemIndices) => void deleteItem: (indices: ItemIndices) => void @@ -23,18 +20,23 @@ export type ItemsActions = { const itemsAction = (setTypebot: SetTypebot): ItemsActions => ({ createItem: ( - item: ButtonItem | Omit, + item: Item | Omit, { groupIndex, blockIndex, itemIndex }: ItemIndices ) => setTypebot((typebot) => produce(typebot, (typebot) => { - const block = typebot.groups[groupIndex].blocks[blockIndex] - if (block.type !== InputBlockType.CHOICE) return + const block = typebot.groups[groupIndex].blocks[ + blockIndex + ] as BlockWithItems + const newItem = { - ...item, - blockId: block.id, id: 'id' in item ? item.id : cuid(), - } + content: + item.type === ItemType.CONDITION + ? defaultConditionContent + : undefined, + ...item, + } as Item if (item.outgoingEdgeId) { const edgeIndex = typebot.edges.findIndex(byId(item.outgoingEdgeId)) edgeIndex !== -1 diff --git a/apps/builder/src/features/graph/components/Graph.tsx b/apps/builder/src/features/graph/components/Graph.tsx index 5917a069ae..6780b63390 100644 --- a/apps/builder/src/features/graph/components/Graph.tsx +++ b/apps/builder/src/features/graph/components/Graph.tsx @@ -48,6 +48,7 @@ export const Graph = ({ const { setGraphPosition: setGlobalGraphPosition, setOpenedBlockId, + setOpenedItemId, setPreviewingEdge, connectingIds, } = useGraph() @@ -126,6 +127,7 @@ export const Graph = ({ const handleClick = () => { setOpenedBlockId(undefined) + setOpenedItemId(undefined) setPreviewingEdge(undefined) } diff --git a/apps/builder/src/features/graph/components/Nodes/BlockNode/BlockNode.tsx b/apps/builder/src/features/graph/components/Nodes/BlockNode/BlockNode.tsx index e2894c0aca..03c80daadc 100644 --- a/apps/builder/src/features/graph/components/Nodes/BlockNode/BlockNode.tsx +++ b/apps/builder/src/features/graph/components/Nodes/BlockNode/BlockNode.tsx @@ -9,12 +9,12 @@ import React, { useEffect, useRef, useState } from 'react' import { BubbleBlock, BubbleBlockContent, - ConditionBlock, DraggableBlock, Block, BlockWithOptions, TextBubbleContent, TextBubbleBlock, + LogicBlockType, } from 'models' import { isBubbleBlock, isTextBubbleBlock } from 'utils' import { BlockNodeContent } from './BlockNodeContent/BlockNodeContent' @@ -28,7 +28,12 @@ import { BlockSettings } from './SettingsPopoverContent/SettingsPopoverContent' import { TextBubbleEditor } from '../../../../blocks/bubbles/textBubble/components/TextBubbleEditor' import { TargetEndpoint } from '../../Endpoints' import { MediaBubblePopoverContent } from './MediaBubblePopoverContent' -import { NodePosition, useDragDistance, useGraph } from '../../../providers' +import { + NodePosition, + useBlockDnd, + useDragDistance, + useGraph, +} from '../../../providers' import { ContextMenu } from '@/components/ContextMenu' import { setMultipleRefs } from '@/utils/helpers' import { hasDefaultConnector } from '../../../utils' @@ -53,6 +58,7 @@ export const BlockNode = ({ setFocusedGroupId, previewingEdge, } = useGraph() + const { mouseOverBlock, setMouseOverBlock, draggedItem } = useBlockDnd() const { typebot, updateBlock } = useTypebot() const [isConnecting, setIsConnecting] = useState(false) const [isPopoverOpened, setIsPopoverOpened] = useState( @@ -99,6 +105,8 @@ export const BlockNode = ({ } const handleMouseEnter = () => { + if (draggedItem !== undefined) + setMouseOverBlock({ id: block.id, ref: blockRef }) if (connectingIds) setConnectingIds({ ...connectingIds, @@ -107,6 +115,7 @@ export const BlockNode = ({ } const handleMouseLeave = () => { + if (mouseOverBlock) setMouseOverBlock(undefined) if (connectingIds?.target) setConnectingIds({ ...connectingIds, @@ -238,9 +247,8 @@ export const BlockNode = ({ ) } -const hasSettingsPopover = ( - block: Block -): block is BlockWithOptions | ConditionBlock => !isBubbleBlock(block) +const hasSettingsPopover = (block: Block): block is BlockWithOptions => + !isBubbleBlock(block) && block.type !== LogicBlockType.CONDITION const isMediaBubbleBlock = ( block: Block diff --git a/apps/builder/src/features/graph/components/Nodes/BlockNode/SettingsPopoverContent/SettingsPopoverContent.tsx b/apps/builder/src/features/graph/components/Nodes/BlockNode/SettingsPopoverContent/SettingsPopoverContent.tsx index a55948bda7..3b9f94f087 100644 --- a/apps/builder/src/features/graph/components/Nodes/BlockNode/SettingsPopoverContent/SettingsPopoverContent.tsx +++ b/apps/builder/src/features/graph/components/Nodes/BlockNode/SettingsPopoverContent/SettingsPopoverContent.tsx @@ -8,8 +8,6 @@ import { } from '@chakra-ui/react' import { ExpandIcon } from '@/components/icons' import { - ConditionItem, - ConditionBlock, InputBlockType, IntegrationBlockType, LogicBlockType, @@ -34,7 +32,6 @@ import { SendEmailSettings } from '@/features/blocks/integrations/sendEmail' import { WebhookSettings } from '@/features/blocks/integrations/webhook' import { ZapierSettings } from '@/features/blocks/integrations/zapier' import { CodeSettings } from '@/features/blocks/logic/code' -import { ConditionSettingsBody } from '@/features/blocks/logic/condition' import { RedirectSettings } from '@/features/blocks/logic/redirect' import { SetVariableSettings } from '@/features/blocks/logic/setVariable' 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' type Props = { - block: BlockWithOptions | ConditionBlock + block: BlockWithOptions webhook?: Webhook onExpandClick: () => void onBlockChange: (updates: Partial) => void @@ -88,18 +85,14 @@ export const BlockSettings = ({ block, onBlockChange, }: { - block: BlockWithOptions | ConditionBlock + block: BlockWithOptions webhook?: Webhook onBlockChange: (block: Partial) => void }): JSX.Element => { const handleOptionsChange = (options: BlockOptions) => { onBlockChange({ options } as Partial) } - const handleItemChange = (updates: Partial) => { - onBlockChange({ - items: [{ ...(block as ConditionBlock).items[0], ...updates }], - } as Partial) - } + switch (block.type) { case InputBlockType.TEXT: { return ( @@ -189,11 +182,6 @@ export const BlockSettings = ({ /> ) } - case LogicBlockType.CONDITION: { - return ( - - ) - } case LogicBlockType.REDIRECT: { return ( void + connectionDisabled?: boolean } -export const ItemNode = ({ item, indices, onMouseDown }: Props) => { +export const ItemNode = ({ + item, + indices, + onMouseDown, + connectionDisabled, +}: Props) => { const { typebot } = useTypebot() const { previewingEdge } = useGraph() const [isMouseOver, setIsMouseOver] = useState(false) const itemRef = useRef(null) const isPreviewing = previewingEdge?.from.itemId === item.id - const isConnectable = !( - typebot?.groups[indices.groupIndex].blocks[ - indices.blockIndex - ] as ChoiceInputBlock - )?.options?.isMultipleChoice - const isReadOnly = item.type === ItemType.CONDITION + const isConnectable = + !connectionDisabled && + !( + typebot?.groups[indices.groupIndex].blocks[ + indices.blockIndex + ] as ChoiceInputBlock + )?.options?.isMultipleChoice const onDrag = (position: NodePosition) => { - if (!onMouseDown || item.type !== ItemType.BUTTON) return + if (!onMouseDown) return onMouseDown(position, item) } useDragDistance({ ref: itemRef, onDrag, - isDisabled: !onMouseDown || item.type !== ItemType.BUTTON, + isDisabled: !onMouseDown, }) const handleMouseEnter = () => setIsMouseOver(true) @@ -63,19 +64,19 @@ export const ItemNode = ({ item, indices, onMouseDown }: Props) => { data-testid="item" pos="relative" ref={setMultipleRefs([ref, itemRef])} + w="full" > diff --git a/apps/builder/src/features/graph/components/Nodes/ItemNode/ItemNodeContent/ItemNodeContent.tsx b/apps/builder/src/features/graph/components/Nodes/ItemNode/ItemNodeContent/ItemNodeContent.tsx index c984707c6e..6a0b4af0e7 100644 --- a/apps/builder/src/features/graph/components/Nodes/ItemNode/ItemNodeContent/ItemNodeContent.tsx +++ b/apps/builder/src/features/graph/components/Nodes/ItemNode/ItemNodeContent/ItemNodeContent.tsx @@ -1,5 +1,5 @@ -import { ButtonNodeContent } from '@/features/blocks/inputs/buttons' -import { ConditionNodeContent } from '@/features/blocks/logic/condition' +import { ButtonsItemNode } from '@/features/blocks/inputs/buttons' +import { ConditionItemNode } from '@/features/blocks/logic/condition' import { Item, ItemIndices, ItemType } from 'models' import React from 'react' @@ -13,13 +13,19 @@ export const ItemNodeContent = ({ item, indices, isMouseOver }: Props) => { switch (item.type) { case ItemType.BUTTON: return ( - ) case ItemType.CONDITION: - return + return ( + + ) } } diff --git a/apps/builder/src/features/graph/components/Nodes/ItemNode/ItemNodesList.tsx b/apps/builder/src/features/graph/components/Nodes/ItemNode/ItemNodesList.tsx index e80d20cc7a..a0423b291e 100644 --- a/apps/builder/src/features/graph/components/Nodes/ItemNode/ItemNodesList.tsx +++ b/apps/builder/src/features/graph/components/Nodes/ItemNode/ItemNodesList.tsx @@ -6,16 +6,10 @@ import { useGraph, } from '../../../providers' import { useTypebot } from '@/features/editor' -import { - ButtonItem, - BlockIndices, - BlockWithItems, - LogicBlockType, -} from 'models' +import { BlockIndices, BlockWithItems, LogicBlockType, Item } from 'models' import React, { useEffect, useRef, useState } from 'react' import { ItemNode } from './ItemNode' import { SourceEndpoint } from '../../Endpoints' -import { ItemNodeOverlay } from './ItemNodeOverlay' type Props = { block: BlockWithItems @@ -27,14 +21,13 @@ export const ItemNodesList = ({ indices: { groupIndex, blockIndex }, }: Props) => { const { typebot, createItem, detachItemFromBlock } = useTypebot() - const { draggedItem, setDraggedItem, mouseOverGroup } = useBlockDnd() + const { draggedItem, setDraggedItem, mouseOverBlock } = useBlockDnd() const placeholderRefs = useRef([]) const { graphPosition } = useGraph() - const groupId = typebot?.groups[groupIndex].id - const isDraggingOnCurrentGroup = - (draggedItem && mouseOverGroup?.id === groupId) ?? false - const isReadOnly = block.type === LogicBlockType.CONDITION - const showPlaceholders = draggedItem && !isReadOnly + const isDraggingOnCurrentBlock = + (draggedItem && mouseOverBlock?.id === block.id) ?? false + const showPlaceholders = + draggedItem !== undefined && block.items[0].type === draggedItem.type const isLastBlock = typebot?.groups[groupIndex].blocks[blockIndex + 1] === undefined @@ -60,29 +53,37 @@ export const ItemNodesList = ({ useEventListener('mousemove', handleGlobalMouseMove) useEffect(() => { - if (mouseOverGroup?.id !== block.groupId) + if (!showPlaceholders) return + if (mouseOverBlock?.id !== block.id) { setExpandedPlaceholderIndex(undefined) + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [mouseOverGroup?.id]) + }, [mouseOverBlock?.id, showPlaceholders]) - const handleMouseMoveOnGroup = (event: MouseEvent) => { - if (!isDraggingOnCurrentGroup || isReadOnly) return + const handleMouseMoveOnBlock = (event: MouseEvent) => { + if (!isDraggingOnCurrentBlock) return const index = computeNearestPlaceholderIndex(event.pageY, placeholderRefs) setExpandedPlaceholderIndex(index) } useEventListener( 'mousemove', - handleMouseMoveOnGroup, - mouseOverGroup?.ref.current + handleMouseMoveOnBlock, + mouseOverBlock?.ref.current ) const handleMouseUpOnGroup = (e: MouseEvent) => { + if ( + !showPlaceholders || + !isDraggingOnCurrentBlock || + !draggedItem || + mouseOverBlock?.id !== block.id + ) + return setExpandedPlaceholderIndex(undefined) - if (!isDraggingOnCurrentGroup) return const itemIndex = computeNearestPlaceholderIndex(e.pageY, placeholderRefs) e.stopPropagation() setDraggedItem(undefined) - createItem(draggedItem as ButtonItem, { + createItem(draggedItem, { groupIndex, blockIndex, itemIndex, @@ -91,7 +92,7 @@ export const ItemNodesList = ({ useEventListener( 'mouseup', handleMouseUpOnGroup, - mouseOverGroup?.ref.current, + mouseOverBlock?.ref.current, { capture: true, } @@ -101,9 +102,9 @@ export const ItemNodesList = ({ (itemIndex: number) => ( { 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) detachItemFromBlock({ groupIndex, blockIndex, itemIndex }) setPosition(absolute) @@ -119,13 +120,7 @@ export const ItemNodesList = ({ } return ( - + - Default + + {block.type === LogicBlockType.CONDITION ? 'Else' : 'Default'} + - + > + + )} diff --git a/apps/builder/src/features/graph/providers/GraphDndProvider.tsx b/apps/builder/src/features/graph/providers/GraphDndProvider.tsx index 9e14563ff9..a147e0a467 100644 --- a/apps/builder/src/features/graph/providers/GraphDndProvider.tsx +++ b/apps/builder/src/features/graph/providers/GraphDndProvider.tsx @@ -1,5 +1,5 @@ import { useEventListener } from '@chakra-ui/react' -import { ButtonItem, DraggableBlock, DraggableBlockType } from 'models' +import { DraggableBlock, DraggableBlockType, Item } from 'models' import { createContext, Dispatch, @@ -11,7 +11,7 @@ import { } from 'react' import { Coordinates } from './GraphProvider' -type GroupInfo = { +type NodeInfo = { id: string ref: React.MutableRefObject } @@ -21,10 +21,12 @@ const graphDndContext = createContext<{ setDraggedBlockType: Dispatch> draggedBlock?: DraggableBlock setDraggedBlock: Dispatch> - draggedItem?: ButtonItem - setDraggedItem: Dispatch> - mouseOverGroup?: GroupInfo - setMouseOverGroup: Dispatch> + draggedItem?: Item + setDraggedItem: Dispatch> + mouseOverGroup?: NodeInfo + setMouseOverGroup: Dispatch> + mouseOverBlock?: NodeInfo + setMouseOverBlock: Dispatch> // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore }>({}) @@ -36,8 +38,9 @@ export const GraphDndProvider = ({ children }: { children: ReactNode }) => { const [draggedBlockType, setDraggedBlockType] = useState< DraggableBlockType | undefined >() - const [draggedItem, setDraggedItem] = useState() - const [mouseOverGroup, setMouseOverGroup] = useState() + const [draggedItem, setDraggedItem] = useState() + const [mouseOverGroup, setMouseOverGroup] = useState() + const [mouseOverBlock, setMouseOverBlock] = useState() return ( { setDraggedItem, mouseOverGroup, setMouseOverGroup, + mouseOverBlock, + setMouseOverBlock, }} > {children} diff --git a/apps/builder/src/features/graph/providers/GraphProvider.tsx b/apps/builder/src/features/graph/providers/GraphProvider.tsx index ba00a84e80..11bda3fb84 100644 --- a/apps/builder/src/features/graph/providers/GraphProvider.tsx +++ b/apps/builder/src/features/graph/providers/GraphProvider.tsx @@ -69,6 +69,8 @@ const graphContext = createContext<{ addTargetEndpoint: (endpoint: Endpoint) => void openedBlockId?: string setOpenedBlockId: Dispatch> + openedItemId?: string + setOpenedItemId: Dispatch> isReadOnly: boolean focusedGroupId?: string setFocusedGroupId: Dispatch> @@ -92,6 +94,7 @@ export const GraphProvider = ({ const [sourceEndpoints, setSourceEndpoints] = useState>({}) const [targetEndpoints, setTargetEndpoints] = useState>({}) const [openedBlockId, setOpenedBlockId] = useState() + const [openedItemId, setOpenedItemId] = useState() const [focusedGroupId, setFocusedGroupId] = useState() const addSourceEndpoint = (endpoint: Endpoint) => { @@ -123,6 +126,8 @@ export const GraphProvider = ({ addTargetEndpoint, openedBlockId, setOpenedBlockId, + openedItemId, + setOpenedItemId, isReadOnly, focusedGroupId, setFocusedGroupId, diff --git a/apps/builder/src/test/assets/typebots/logic/condition.json b/apps/builder/src/test/assets/typebots/logic/condition.json index 8eac8501ce..f1561459eb 100644 --- a/apps/builder/src/test/assets/typebots/logic/condition.json +++ b/apps/builder/src/test/assets/typebots/logic/condition.json @@ -53,8 +53,7 @@ "id": "eMk84KvFM53sBxchTeackR", "blocks": [ { - "id": "s5hz7HQki66cwELvk2738MJ", - "groupId": "eMk84KvFM53sBxchTeackR", + "id": "sv8uvEXgYWQNMfZWcdbfyCs", "type": "Condition", "items": [ { @@ -70,13 +69,7 @@ "logicalOperator": "AND" }, "outgoingEdgeId": "nDjMjM11xPQF7c9Be6ukdY" - } - ] - }, - { - "id": "sv8uvEXgYWQNMfZWcdbfyCs", - "type": "Condition", - "items": [ + }, { "id": "ijYfW38tGhCMRrCtmR3bcr", "type": 1, diff --git a/packages/bot-engine/src/features/blocks/logic/condition/utils/executeCondition.ts b/packages/bot-engine/src/features/blocks/logic/condition/utils/executeCondition.ts index 985e66cc05..2d4fd45e48 100644 --- a/packages/bot-engine/src/features/blocks/logic/condition/utils/executeCondition.ts +++ b/packages/bot-engine/src/features/blocks/logic/condition/utils/executeCondition.ts @@ -13,14 +13,15 @@ export const executeCondition = ( block: ConditionBlock, { typebot: { variables } }: LogicState ): EdgeId | undefined => { - const { content } = block.items[0] - const isConditionPassed = - content.logicalOperator === LogicalOperator.AND - ? content.comparisons.every(executeComparison(variables)) - : content.comparisons.some(executeComparison(variables)) - return isConditionPassed - ? block.items[0].outgoingEdgeId - : block.outgoingEdgeId + const passedCondition = block.items.find((item) => { + const { content } = item + const isConditionPassed = + content.logicalOperator === LogicalOperator.AND + ? content.comparisons.every(executeComparison(variables)) + : content.comparisons.some(executeComparison(variables)) + return isConditionPassed + }) + return passedCondition ? passedCondition.outgoingEdgeId : block.outgoingEdgeId } const executeComparison = diff --git a/packages/models/src/features/blocks/item.ts b/packages/models/src/features/blocks/item.ts index dae4d68399..729862f7be 100644 --- a/packages/models/src/features/blocks/item.ts +++ b/packages/models/src/features/blocks/item.ts @@ -8,7 +8,7 @@ export type ItemIndices = { groupIndex: number itemIndex: number } -const itemScema = buttonItemSchema.or(conditionItemSchema) +const itemSchema = buttonItemSchema.or(conditionItemSchema) export type ItemBase = z.infer -export type Item = z.infer +export type Item = z.infer diff --git a/packages/typebot-js/src/commands/toggle.ts b/packages/typebot-js/src/commands/toggle.ts index 5e2deb2655..bec7f48eab 100644 --- a/packages/typebot-js/src/commands/toggle.ts +++ b/packages/typebot-js/src/commands/toggle.ts @@ -12,7 +12,6 @@ export const toggle = () => { ? closePopup(existingPopup) : openPopup(existingPopup) const existingBubble = document.querySelector('#typebot-bubble') - console.log(existingBubble) if (existingBubble) isIframeOpened(existingBubble) ? closeIframe(existingBubble)