@ -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')
|
@ -0,0 +1,3 @@
|
||||
export * from './ButtonsItemNode'
|
||||
export * from './ButtonsIcon'
|
||||
export * from './ButtonsOptionsForm'
|
@ -1,3 +1 @@
|
||||
export { ButtonsOptionsForm } from './components/ButtonsOptionsForm'
|
||||
export { ButtonNodeContent } from './components/ButtonNodeContent'
|
||||
export { ButtonsInputIcon } from './components/ButtonsInputIcon'
|
||||
export * from './components'
|
||||
|
@ -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<ConditionItem>) => 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) =>
|
@ -0,0 +1 @@
|
||||
export * from './ConditionItemForm'
|
@ -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 '!='
|
||||
}
|
||||
}
|
@ -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 '!='
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export { ConditionSettingsBody } from './ConditonSettingsBody'
|
@ -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'
|
||||
|
@ -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<ButtonItem, 'id'>,
|
||||
indices: ItemIndices
|
||||
) => void
|
||||
createItem: (item: Item | Omit<Item, 'id'>, indices: ItemIndices) => void
|
||||
updateItem: (indices: ItemIndices, updates: Partial<Omit<Item, 'id'>>) => 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<ButtonItem, 'id'>,
|
||||
item: Item | Omit<Item, 'id'>,
|
||||
{ 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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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<Block>) => void
|
||||
@ -88,18 +85,14 @@ export const BlockSettings = ({
|
||||
block,
|
||||
onBlockChange,
|
||||
}: {
|
||||
block: BlockWithOptions | ConditionBlock
|
||||
block: BlockWithOptions
|
||||
webhook?: Webhook
|
||||
onBlockChange: (block: Partial<Block>) => void
|
||||
}): JSX.Element => {
|
||||
const handleOptionsChange = (options: BlockOptions) => {
|
||||
onBlockChange({ options } as Partial<Block>)
|
||||
}
|
||||
const handleItemChange = (updates: Partial<ConditionItem>) => {
|
||||
onBlockChange({
|
||||
items: [{ ...(block as ConditionBlock).items[0], ...updates }],
|
||||
} as Partial<Block>)
|
||||
}
|
||||
|
||||
switch (block.type) {
|
||||
case InputBlockType.TEXT: {
|
||||
return (
|
||||
@ -189,11 +182,6 @@ export const BlockSettings = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
case LogicBlockType.CONDITION: {
|
||||
return (
|
||||
<ConditionSettingsBody block={block} onItemChange={handleItemChange} />
|
||||
)
|
||||
}
|
||||
case LogicBlockType.REDIRECT: {
|
||||
return (
|
||||
<RedirectSettings
|
||||
|
@ -6,13 +6,7 @@ import {
|
||||
useDragDistance,
|
||||
} from '../../../providers'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import {
|
||||
ButtonItem,
|
||||
ChoiceInputBlock,
|
||||
Item,
|
||||
ItemIndices,
|
||||
ItemType,
|
||||
} from 'models'
|
||||
import { ChoiceInputBlock, Item, ItemIndices } from 'models'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { SourceEndpoint } from '../../Endpoints/SourceEndpoint'
|
||||
import { ItemNodeContent } from './ItemNodeContent'
|
||||
@ -25,30 +19,37 @@ type Props = {
|
||||
indices: ItemIndices
|
||||
onMouseDown?: (
|
||||
blockNodePosition: { absolute: Coordinates; relative: Coordinates },
|
||||
item: ButtonItem
|
||||
item: Item
|
||||
) => 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<HTMLDivElement | null>(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"
|
||||
>
|
||||
<Flex
|
||||
align="center"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
shadow="sm"
|
||||
_hover={isReadOnly ? {} : { shadow: 'md' }}
|
||||
_hover={{ shadow: 'md' }}
|
||||
transition="box-shadow 200ms, border-color 200ms"
|
||||
rounded="md"
|
||||
borderWidth={isOpened || isPreviewing ? '2px' : '1px'}
|
||||
borderColor={isOpened || isPreviewing ? 'blue.400' : 'gray.100'}
|
||||
margin={isOpened || isPreviewing ? '-1px' : 0}
|
||||
pointerEvents={isReadOnly ? 'none' : 'all'}
|
||||
bgColor="white"
|
||||
w="full"
|
||||
>
|
||||
|
@ -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 (
|
||||
<ButtonNodeContent
|
||||
<ButtonsItemNode
|
||||
item={item}
|
||||
isMouseOver={isMouseOver}
|
||||
indices={indices}
|
||||
/>
|
||||
)
|
||||
case ItemType.CONDITION:
|
||||
return <ConditionNodeContent item={item} />
|
||||
return (
|
||||
<ConditionItemNode
|
||||
item={item}
|
||||
isMouseOver={isMouseOver}
|
||||
indices={indices}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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<HTMLDivElement[]>([])
|
||||
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 (
|
||||
<Stack
|
||||
flex={1}
|
||||
spacing={1}
|
||||
maxW="full"
|
||||
onClick={stopPropagating}
|
||||
pointerEvents={isReadOnly ? 'none' : 'all'}
|
||||
>
|
||||
<Stack flex={1} spacing={1} maxW="full" onClick={stopPropagating}>
|
||||
<Flex
|
||||
ref={handlePushElementRef(0)}
|
||||
h={showPlaceholders && expandedPlaceholderIndex === 0 ? '50px' : '2px'}
|
||||
@ -161,13 +156,15 @@ export const ItemNodesList = ({
|
||||
py="2"
|
||||
borderWidth="1px"
|
||||
borderColor="gray.300"
|
||||
bgColor={isReadOnly ? '' : 'gray.50'}
|
||||
bgColor={'gray.50'}
|
||||
rounded="md"
|
||||
pos="relative"
|
||||
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
|
||||
source={{
|
||||
groupId: block.groupId,
|
||||
@ -181,16 +178,23 @@ export const ItemNodesList = ({
|
||||
|
||||
{draggedItem && draggedItem.blockId === block.id && (
|
||||
<Portal>
|
||||
<ItemNodeOverlay
|
||||
item={draggedItem}
|
||||
<Flex
|
||||
pointerEvents="none"
|
||||
pos="fixed"
|
||||
top="0"
|
||||
left="0"
|
||||
style={{
|
||||
transform: `translate(${position.x}px, ${position.y}px) rotate(-2deg) scale(${graphPosition.scale})`,
|
||||
}}
|
||||
w="220px"
|
||||
transformOrigin="0 0 0"
|
||||
/>
|
||||
>
|
||||
<ItemNode
|
||||
item={draggedItem}
|
||||
indices={{ groupIndex, blockIndex, itemIndex: 0 }}
|
||||
connectionDisabled
|
||||
/>
|
||||
</Flex>
|
||||
</Portal>
|
||||
)}
|
||||
</Stack>
|
||||
|
@ -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<HTMLDivElement | null>
|
||||
}
|
||||
@ -21,10 +21,12 @@ const graphDndContext = createContext<{
|
||||
setDraggedBlockType: Dispatch<SetStateAction<DraggableBlockType | undefined>>
|
||||
draggedBlock?: DraggableBlock
|
||||
setDraggedBlock: Dispatch<SetStateAction<DraggableBlock | undefined>>
|
||||
draggedItem?: ButtonItem
|
||||
setDraggedItem: Dispatch<SetStateAction<ButtonItem | undefined>>
|
||||
mouseOverGroup?: GroupInfo
|
||||
setMouseOverGroup: Dispatch<SetStateAction<GroupInfo | undefined>>
|
||||
draggedItem?: Item
|
||||
setDraggedItem: Dispatch<SetStateAction<Item | undefined>>
|
||||
mouseOverGroup?: NodeInfo
|
||||
setMouseOverGroup: Dispatch<SetStateAction<NodeInfo | undefined>>
|
||||
mouseOverBlock?: NodeInfo
|
||||
setMouseOverBlock: Dispatch<SetStateAction<NodeInfo | undefined>>
|
||||
// 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<ButtonItem | undefined>()
|
||||
const [mouseOverGroup, setMouseOverGroup] = useState<GroupInfo>()
|
||||
const [draggedItem, setDraggedItem] = useState<Item | undefined>()
|
||||
const [mouseOverGroup, setMouseOverGroup] = useState<NodeInfo>()
|
||||
const [mouseOverBlock, setMouseOverBlock] = useState<NodeInfo>()
|
||||
|
||||
return (
|
||||
<graphDndContext.Provider
|
||||
@ -50,6 +53,8 @@ export const GraphDndProvider = ({ children }: { children: ReactNode }) => {
|
||||
setDraggedItem,
|
||||
mouseOverGroup,
|
||||
setMouseOverGroup,
|
||||
mouseOverBlock,
|
||||
setMouseOverBlock,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -69,6 +69,8 @@ const graphContext = createContext<{
|
||||
addTargetEndpoint: (endpoint: Endpoint) => void
|
||||
openedBlockId?: string
|
||||
setOpenedBlockId: Dispatch<SetStateAction<string | undefined>>
|
||||
openedItemId?: string
|
||||
setOpenedItemId: Dispatch<SetStateAction<string | undefined>>
|
||||
isReadOnly: boolean
|
||||
focusedGroupId?: string
|
||||
setFocusedGroupId: Dispatch<SetStateAction<string | undefined>>
|
||||
@ -92,6 +94,7 @@ export const GraphProvider = ({
|
||||
const [sourceEndpoints, setSourceEndpoints] = useState<IdMap<Endpoint>>({})
|
||||
const [targetEndpoints, setTargetEndpoints] = useState<IdMap<Endpoint>>({})
|
||||
const [openedBlockId, setOpenedBlockId] = useState<string>()
|
||||
const [openedItemId, setOpenedItemId] = useState<string>()
|
||||
const [focusedGroupId, setFocusedGroupId] = useState<string>()
|
||||
|
||||
const addSourceEndpoint = (endpoint: Endpoint) => {
|
||||
@ -123,6 +126,8 @@ export const GraphProvider = ({
|
||||
addTargetEndpoint,
|
||||
openedBlockId,
|
||||
setOpenedBlockId,
|
||||
openedItemId,
|
||||
setOpenedItemId,
|
||||
isReadOnly,
|
||||
focusedGroupId,
|
||||
setFocusedGroupId,
|
||||
|
@ -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,
|
||||
|
@ -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 =
|
||||
|
@ -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<typeof itemBaseSchema>
|
||||
export type Item = z.infer<typeof itemScema>
|
||||
export type Item = z.infer<typeof itemSchema>
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user