@@ -10,11 +10,10 @@ import React, { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
BubbleBlock,
|
||||
BubbleBlockContent,
|
||||
DraggableBlock,
|
||||
Block,
|
||||
BlockWithOptions,
|
||||
TextBubbleBlock,
|
||||
LogicBlockType,
|
||||
BlockV6,
|
||||
} from '@typebot.io/schemas'
|
||||
import {
|
||||
isBubbleBlock,
|
||||
@@ -25,7 +24,7 @@ import {
|
||||
import { BlockNodeContent } from './BlockNodeContent'
|
||||
import { BlockSettings, SettingsPopoverContent } from './SettingsPopoverContent'
|
||||
import { BlockNodeContextMenu } from './BlockNodeContextMenu'
|
||||
import { SourceEndpoint } from '../../endpoints/SourceEndpoint'
|
||||
import { BlockSourceEndpoint } from '../../endpoints/BlockSourceEndpoint'
|
||||
import { useRouter } from 'next/router'
|
||||
import { MediaBubblePopoverContent } from './MediaBubblePopoverContent'
|
||||
import { ContextMenu } from '@/components/ContextMenu'
|
||||
@@ -44,6 +43,7 @@ import { setMultipleRefs } from '@/helpers/setMultipleRefs'
|
||||
import { TargetEndpoint } from '../../endpoints/TargetEndpoint'
|
||||
import { SettingsModal } from './SettingsModal'
|
||||
import { TElement } from '@udecode/plate-common'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
|
||||
export const BlockNode = ({
|
||||
block,
|
||||
@@ -51,10 +51,10 @@ export const BlockNode = ({
|
||||
indices,
|
||||
onMouseDown,
|
||||
}: {
|
||||
block: Block
|
||||
block: BlockV6
|
||||
isConnectable: boolean
|
||||
indices: { blockIndex: number; groupIndex: number }
|
||||
onMouseDown?: (blockNodePosition: NodePosition, block: DraggableBlock) => void
|
||||
onMouseDown?: (blockNodePosition: NodePosition, block: BlockV6) => void
|
||||
}) => {
|
||||
const bg = useColorModeValue('gray.50', 'gray.850')
|
||||
const previewingBorderColor = useColorModeValue('blue.400', 'blue.300')
|
||||
@@ -78,7 +78,7 @@ export const BlockNode = ({
|
||||
openedBlockId === block.id
|
||||
)
|
||||
const [isEditing, setIsEditing] = useState<boolean>(
|
||||
isTextBubbleBlock(block) && block.content.richText.length === 0
|
||||
isTextBubbleBlock(block) && (block.content?.richText?.length ?? 0) === 0
|
||||
)
|
||||
const blockRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
@@ -87,15 +87,17 @@ export const BlockNode = ({
|
||||
previewingEdge?.to.blockId === block.id ||
|
||||
previewingBlock?.id === block.id
|
||||
|
||||
const groupId = typebot?.groups[indices.groupIndex].id
|
||||
|
||||
const onDrag = (position: NodePosition) => {
|
||||
if (block.type === 'start' || !onMouseDown) return
|
||||
if (!onMouseDown) return
|
||||
onMouseDown(position, block)
|
||||
}
|
||||
|
||||
useDragDistance({
|
||||
ref: blockRef,
|
||||
onDrag,
|
||||
isDisabled: !onMouseDown || block.type === 'start',
|
||||
isDisabled: !onMouseDown,
|
||||
})
|
||||
|
||||
const {
|
||||
@@ -110,10 +112,10 @@ export const BlockNode = ({
|
||||
|
||||
useEffect(() => {
|
||||
setIsConnecting(
|
||||
connectingIds?.target?.groupId === block.groupId &&
|
||||
connectingIds?.target?.groupId === groupId &&
|
||||
connectingIds?.target?.blockId === block.id
|
||||
)
|
||||
}, [connectingIds, block.groupId, block.id])
|
||||
}, [connectingIds, block.id, groupId])
|
||||
|
||||
const handleModalClose = () => {
|
||||
updateBlock(indices, { ...block })
|
||||
@@ -124,10 +126,10 @@ export const BlockNode = ({
|
||||
if (isReadOnly) return
|
||||
if (mouseOverBlock?.id !== block.id && blockRef.current)
|
||||
setMouseOverBlock({ id: block.id, element: blockRef.current })
|
||||
if (connectingIds)
|
||||
if (connectingIds && groupId)
|
||||
setConnectingIds({
|
||||
...connectingIds,
|
||||
target: { groupId: block.groupId, blockId: block.id },
|
||||
target: { groupId, blockId: block.id },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -147,7 +149,7 @@ export const BlockNode = ({
|
||||
}
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
setFocusedGroupId(block.groupId)
|
||||
setFocusedGroupId(groupId)
|
||||
e.stopPropagation()
|
||||
if (isTextBubbleBlock(block)) setIsEditing(true)
|
||||
setOpenedBlockId(block.id)
|
||||
@@ -187,7 +189,7 @@ export const BlockNode = ({
|
||||
return isEditing && isTextBubbleBlock(block) ? (
|
||||
<TextBubbleEditor
|
||||
id={block.id}
|
||||
initialValue={block.content.richText}
|
||||
initialValue={block.content?.richText ?? []}
|
||||
onClose={handleCloseEditor}
|
||||
/>
|
||||
) : (
|
||||
@@ -244,18 +246,18 @@ export const BlockNode = ({
|
||||
left="-34px"
|
||||
top="16px"
|
||||
blockId={block.id}
|
||||
groupId={block.groupId}
|
||||
groupId={groupId}
|
||||
/>
|
||||
)}
|
||||
{(isConnectable ||
|
||||
(pathname.endsWith('analytics') && isInputBlock(block))) &&
|
||||
hasDefaultConnector(block) &&
|
||||
block.type !== LogicBlockType.JUMP && (
|
||||
<SourceEndpoint
|
||||
<BlockSourceEndpoint
|
||||
source={{
|
||||
groupId: block.groupId,
|
||||
blockId: block.id,
|
||||
}}
|
||||
groupId={groupId}
|
||||
pos="absolute"
|
||||
right="-34px"
|
||||
bottom="10px"
|
||||
@@ -269,6 +271,7 @@ export const BlockNode = ({
|
||||
<>
|
||||
<SettingsPopoverContent
|
||||
block={block}
|
||||
groupId={groupId}
|
||||
onExpandClick={handleExpandClick}
|
||||
onBlockChange={handleBlockUpdate}
|
||||
/>
|
||||
@@ -276,6 +279,7 @@ export const BlockNode = ({
|
||||
<SettingsModal isOpen={isModalOpen} onClose={handleModalClose}>
|
||||
<BlockSettings
|
||||
block={block}
|
||||
groupId={groupId}
|
||||
onBlockChange={handleBlockUpdate}
|
||||
/>
|
||||
</SettingsModal>
|
||||
@@ -299,10 +303,10 @@ export const BlockNode = ({
|
||||
)
|
||||
}
|
||||
|
||||
const hasSettingsPopover = (block: Block): block is BlockWithOptions =>
|
||||
const hasSettingsPopover = (block: BlockV6): block is BlockWithOptions =>
|
||||
!isBubbleBlock(block) && block.type !== LogicBlockType.CONDITION
|
||||
|
||||
const isMediaBubbleBlock = (
|
||||
block: Block
|
||||
block: BlockV6
|
||||
): block is Exclude<BubbleBlock, TextBubbleBlock> =>
|
||||
isBubbleBlock(block) && !isTextBubbleBlock(block)
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import {
|
||||
Block,
|
||||
StartBlock,
|
||||
BubbleBlockType,
|
||||
InputBlockType,
|
||||
LogicBlockType,
|
||||
IntegrationBlockType,
|
||||
BlockIndices,
|
||||
} from '@typebot.io/schemas'
|
||||
import { BlockIndices, BlockV6 } from '@typebot.io/schemas'
|
||||
import { WaitNodeContent } from '@/features/blocks/logic/wait/components/WaitNodeContent'
|
||||
import { ScriptNodeContent } from '@/features/blocks/logic/script/components/ScriptNodeContent'
|
||||
import { ButtonsBlockNode } from '@/features/blocks/inputs/buttons/components/ButtonsBlockNode'
|
||||
@@ -42,15 +33,17 @@ import { ChatwootNodeBody } from '@/features/blocks/integrations/chatwoot/compon
|
||||
import { AbTestNodeBody } from '@/features/blocks/logic/abTest/components/AbTestNodeBody'
|
||||
import { PictureChoiceNode } from '@/features/blocks/inputs/pictureChoice/components/PictureChoiceNode'
|
||||
import { PixelNodeBody } from '@/features/blocks/integrations/pixel/components/PixelNodeBody'
|
||||
import { useTranslate } from '@tolgee/react'
|
||||
import { ZemanticAiNodeBody } from '@/features/blocks/integrations/zemanticAi/ZemanticAiNodeBody'
|
||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
|
||||
type Props = {
|
||||
block: Block | StartBlock
|
||||
block: BlockV6
|
||||
indices: BlockIndices
|
||||
}
|
||||
export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
||||
const { t } = useTranslate()
|
||||
switch (block.type) {
|
||||
case BubbleBlockType.TEXT: {
|
||||
return <TextBubbleContent block={block} />
|
||||
@@ -65,40 +58,19 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
||||
return <EmbedBubbleContent block={block} />
|
||||
}
|
||||
case BubbleBlockType.AUDIO: {
|
||||
return <AudioBubbleNode url={block.content.url} />
|
||||
return <AudioBubbleNode url={block.content?.url} />
|
||||
}
|
||||
case InputBlockType.TEXT: {
|
||||
return (
|
||||
<TextInputNodeContent
|
||||
variableId={block.options.variableId}
|
||||
placeholder={block.options.labels.placeholder}
|
||||
isLong={block.options.isLong}
|
||||
/>
|
||||
)
|
||||
return <TextInputNodeContent options={block.options} />
|
||||
}
|
||||
case InputBlockType.NUMBER: {
|
||||
return (
|
||||
<NumberNodeContent
|
||||
placeholder={block.options.labels.placeholder}
|
||||
variableId={block.options.variableId}
|
||||
/>
|
||||
)
|
||||
return <NumberNodeContent options={block.options} />
|
||||
}
|
||||
case InputBlockType.EMAIL: {
|
||||
return (
|
||||
<EmailInputNodeContent
|
||||
placeholder={block.options.labels.placeholder}
|
||||
variableId={block.options.variableId}
|
||||
/>
|
||||
)
|
||||
return <EmailInputNodeContent options={block.options} />
|
||||
}
|
||||
case InputBlockType.URL: {
|
||||
return (
|
||||
<UrlNodeContent
|
||||
placeholder={block.options.labels.placeholder}
|
||||
variableId={block.options.variableId}
|
||||
/>
|
||||
)
|
||||
return <UrlNodeContent options={block.options} />
|
||||
}
|
||||
case InputBlockType.CHOICE: {
|
||||
return <ButtonsBlockNode block={block} indices={indices} />
|
||||
@@ -107,26 +79,16 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
||||
return <PictureChoiceNode block={block} indices={indices} />
|
||||
}
|
||||
case InputBlockType.PHONE: {
|
||||
return (
|
||||
<PhoneNodeContent
|
||||
placeholder={block.options.labels.placeholder}
|
||||
variableId={block.options.variableId}
|
||||
/>
|
||||
)
|
||||
return <PhoneNodeContent options={block.options} />
|
||||
}
|
||||
case InputBlockType.DATE: {
|
||||
return <DateNodeContent variableId={block.options.variableId} />
|
||||
return <DateNodeContent variableId={block.options?.variableId} />
|
||||
}
|
||||
case InputBlockType.PAYMENT: {
|
||||
return <PaymentInputContent block={block} />
|
||||
}
|
||||
case InputBlockType.RATING: {
|
||||
return (
|
||||
<RatingInputContent
|
||||
block={block}
|
||||
variableId={block.options.variableId}
|
||||
/>
|
||||
)
|
||||
return <RatingInputContent block={block} />
|
||||
}
|
||||
case InputBlockType.FILE: {
|
||||
return <FileInputContent options={block.options} />
|
||||
@@ -135,15 +97,10 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
||||
return <SetVariableContent block={block} />
|
||||
}
|
||||
case LogicBlockType.REDIRECT: {
|
||||
return <RedirectNodeContent url={block.options.url} />
|
||||
return <RedirectNodeContent url={block.options?.url} />
|
||||
}
|
||||
case LogicBlockType.SCRIPT: {
|
||||
return (
|
||||
<ScriptNodeContent
|
||||
name={block.options.name}
|
||||
content={block.options.content}
|
||||
/>
|
||||
)
|
||||
return <ScriptNodeContent options={block.options} />
|
||||
}
|
||||
case LogicBlockType.WAIT: {
|
||||
return <WaitNodeContent options={block.options} />
|
||||
@@ -185,9 +142,9 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
||||
case IntegrationBlockType.OPEN_AI: {
|
||||
return (
|
||||
<OpenAINodeBody
|
||||
task={block.options.task}
|
||||
task={block.options?.task}
|
||||
responseMapping={
|
||||
'responseMapping' in block.options
|
||||
block.options && 'responseMapping' in block.options
|
||||
? block.options.responseMapping
|
||||
: []
|
||||
}
|
||||
@@ -200,8 +157,5 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
||||
case IntegrationBlockType.ZEMANTIC_AI: {
|
||||
return <ZemanticAiNodeBody options={block.options} />
|
||||
}
|
||||
case 'start': {
|
||||
return <Text>{t('editor.blocks.start.text')}</Text>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { BlockIcon } from '@/features/editor/components/BlockIcon'
|
||||
import { StackProps, HStack, useColorModeValue } from '@chakra-ui/react'
|
||||
import { StartBlock, Block, BlockIndices } from '@typebot.io/schemas'
|
||||
import { BlockIndices, BlockV6 } from '@typebot.io/schemas'
|
||||
import { BlockNodeContent } from './BlockNodeContent'
|
||||
|
||||
export const BlockNodeOverlay = ({
|
||||
block,
|
||||
indices,
|
||||
...props
|
||||
}: { block: Block | StartBlock; indices: BlockIndices } & StackProps) => {
|
||||
}: { block: BlockV6; indices: BlockIndices } & StackProps) => {
|
||||
return (
|
||||
<HStack
|
||||
p="3"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEventListener, Stack, Portal } from '@chakra-ui/react'
|
||||
import { DraggableBlock, DraggableBlockType, Block } from '@typebot.io/schemas'
|
||||
import { BlockV6 } from '@typebot.io/schemas'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { BlockNode } from './BlockNode'
|
||||
@@ -14,19 +14,11 @@ import { useGraph } from '@/features/graph/providers/GraphProvider'
|
||||
import { Coordinates } from '@dnd-kit/utilities'
|
||||
|
||||
type Props = {
|
||||
groupId: string
|
||||
blocks: Block[]
|
||||
blocks: BlockV6[]
|
||||
groupIndex: number
|
||||
groupRef: React.MutableRefObject<HTMLDivElement | null>
|
||||
isStartGroup: boolean
|
||||
}
|
||||
export const BlockNodesList = ({
|
||||
groupId,
|
||||
blocks,
|
||||
groupIndex,
|
||||
groupRef,
|
||||
isStartGroup,
|
||||
}: Props) => {
|
||||
export const BlockNodesList = ({ blocks, groupIndex, groupRef }: Props) => {
|
||||
const {
|
||||
draggedBlock,
|
||||
setDraggedBlock,
|
||||
@@ -48,10 +40,10 @@ export const BlockNodesList = ({
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
const groupId = typebot?.groups[groupIndex].id
|
||||
const isDraggingOnCurrentGroup =
|
||||
(draggedBlock || draggedBlockType) && mouseOverGroup?.id === groupId
|
||||
const showSortPlaceholders =
|
||||
!isStartGroup && isDefined(draggedBlock || draggedBlockType)
|
||||
const showSortPlaceholders = isDefined(draggedBlock || draggedBlockType)
|
||||
|
||||
useEffect(() => {
|
||||
if (mouseOverGroup?.id !== groupId) setExpandedPlaceholderIndex(undefined)
|
||||
@@ -75,14 +67,13 @@ export const BlockNodesList = ({
|
||||
|
||||
const handleMouseUpOnGroup = (e: MouseEvent) => {
|
||||
setExpandedPlaceholderIndex(undefined)
|
||||
if (!isDraggingOnCurrentGroup) return
|
||||
if (!isDraggingOnCurrentGroup || !groupId) return
|
||||
const blockIndex = computeNearestPlaceholderIndex(
|
||||
e.clientY,
|
||||
placeholderRefs
|
||||
)
|
||||
createBlock(
|
||||
groupId,
|
||||
(draggedBlock || draggedBlockType) as DraggableBlock | DraggableBlockType,
|
||||
(draggedBlock || draggedBlockType) as BlockV6 | BlockV6['type'],
|
||||
{
|
||||
groupIndex,
|
||||
blockIndex,
|
||||
@@ -96,16 +87,16 @@ export const BlockNodesList = ({
|
||||
(blockIndex: number) =>
|
||||
(
|
||||
{ relative, absolute }: { absolute: Coordinates; relative: Coordinates },
|
||||
block: DraggableBlock
|
||||
block: BlockV6
|
||||
) => {
|
||||
if (isReadOnly) return
|
||||
if (isReadOnly || !groupId) return
|
||||
placeholderRefs.current.splice(blockIndex + 1, 1)
|
||||
setMousePositionInElement(relative)
|
||||
setPosition({
|
||||
x: absolute.x - relative.x,
|
||||
y: absolute.y - relative.y,
|
||||
})
|
||||
setDraggedBlock(block)
|
||||
setDraggedBlock({ ...block, groupId })
|
||||
detachBlockFromGroup({ groupIndex, blockIndex })
|
||||
}
|
||||
|
||||
@@ -124,7 +115,7 @@ export const BlockNodesList = ({
|
||||
<Stack
|
||||
spacing={1}
|
||||
transition="none"
|
||||
pointerEvents={isReadOnly || isStartGroup ? 'none' : 'auto'}
|
||||
pointerEvents={isReadOnly ? 'none' : 'auto'}
|
||||
>
|
||||
<PlaceholderNode
|
||||
isVisible={showSortPlaceholders}
|
||||
|
||||
@@ -12,9 +12,9 @@ import {
|
||||
import {
|
||||
BubbleBlock,
|
||||
BubbleBlockContent,
|
||||
BubbleBlockType,
|
||||
TextBubbleBlock,
|
||||
} from '@typebot.io/schemas'
|
||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||
import { useRef } from 'react'
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -9,14 +9,7 @@ import {
|
||||
SlideFade,
|
||||
Flex,
|
||||
} from '@chakra-ui/react'
|
||||
import {
|
||||
InputBlockType,
|
||||
IntegrationBlockType,
|
||||
LogicBlockType,
|
||||
Block,
|
||||
BlockOptions,
|
||||
BlockWithOptions,
|
||||
} from '@typebot.io/schemas'
|
||||
import { Block, BlockOptions, BlockWithOptions } from '@typebot.io/schemas'
|
||||
import { useRef, useState } from 'react'
|
||||
import { WaitSettings } from '@/features/blocks/logic/wait/components/WaitSettings'
|
||||
import { ScriptSettings } from '@/features/blocks/logic/script/components/ScriptSettings'
|
||||
@@ -48,9 +41,13 @@ import { PictureChoiceSettings } from '@/features/blocks/inputs/pictureChoice/co
|
||||
import { SettingsHoverBar } from './SettingsHoverBar'
|
||||
import { PixelSettings } from '@/features/blocks/integrations/pixel/components/PixelSettings'
|
||||
import { ZemanticAiSettings } from '@/features/blocks/integrations/zemanticAi/ZemanticAiSettings'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
|
||||
type Props = {
|
||||
block: BlockWithOptions
|
||||
groupId: string | undefined
|
||||
onExpandClick: () => void
|
||||
onBlockChange: (updates: Partial<Block>) => void
|
||||
}
|
||||
@@ -105,11 +102,13 @@ export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
|
||||
|
||||
export const BlockSettings = ({
|
||||
block,
|
||||
groupId,
|
||||
onBlockChange,
|
||||
}: {
|
||||
block: BlockWithOptions
|
||||
groupId: string | undefined
|
||||
onBlockChange: (block: Partial<Block>) => void
|
||||
}): JSX.Element => {
|
||||
}): JSX.Element | null => {
|
||||
const updateOptions = (options: BlockOptions) => {
|
||||
onBlockChange({ options } as Partial<Block>)
|
||||
}
|
||||
@@ -241,12 +240,14 @@ export const BlockSettings = ({
|
||||
)
|
||||
}
|
||||
case LogicBlockType.JUMP: {
|
||||
return (
|
||||
return groupId ? (
|
||||
<JumpSettings
|
||||
groupId={block.groupId}
|
||||
groupId={groupId}
|
||||
options={block.options}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
case LogicBlockType.AB_TEST: {
|
||||
@@ -320,5 +321,7 @@ export const BlockSettings = ({
|
||||
<ZemanticAiSettings block={block} onOptionsChange={updateOptions} />
|
||||
)
|
||||
}
|
||||
case LogicBlockType.CONDITION:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { InfoIcon, PlayIcon, TrashIcon } from '@/components/icons'
|
||||
import {
|
||||
HStack,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
useClipboard,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react'
|
||||
|
||||
type Props = {
|
||||
eventId: string
|
||||
onPlayClick: () => void
|
||||
onDeleteClick?: () => void
|
||||
}
|
||||
|
||||
export const EventFocusToolbar = ({
|
||||
eventId,
|
||||
onPlayClick,
|
||||
onDeleteClick,
|
||||
}: Props) => {
|
||||
const { hasCopied, onCopy } = useClipboard(eventId)
|
||||
|
||||
return (
|
||||
<HStack
|
||||
rounded="md"
|
||||
spacing={0}
|
||||
borderWidth="1px"
|
||||
bgColor={useColorModeValue('white', 'gray.800')}
|
||||
shadow="md"
|
||||
>
|
||||
<IconButton
|
||||
icon={<PlayIcon />}
|
||||
borderRightWidth="1px"
|
||||
borderRightRadius="none"
|
||||
aria-label={'Preview bot from this group'}
|
||||
variant="ghost"
|
||||
onClick={onPlayClick}
|
||||
size="sm"
|
||||
/>
|
||||
<Tooltip
|
||||
label={hasCopied ? 'Copied!' : eventId}
|
||||
closeOnClick={false}
|
||||
placement="top"
|
||||
>
|
||||
<IconButton
|
||||
icon={<InfoIcon />}
|
||||
borderRightWidth="1px"
|
||||
borderRightRadius="none"
|
||||
borderLeftRadius="none"
|
||||
aria-label={'Show group info'}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onCopy}
|
||||
/>
|
||||
</Tooltip>
|
||||
{onDeleteClick ? (
|
||||
<IconButton
|
||||
aria-label="Delete"
|
||||
borderLeftRadius="none"
|
||||
icon={<TrashIcon />}
|
||||
onClick={onDeleteClick}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
/>
|
||||
) : null}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import { SlideFade, Stack, useColorModeValue } from '@chakra-ui/react'
|
||||
import React, { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { EventNodeContextMenu } from './EventNodeContextMenu'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { ContextMenu } from '@/components/ContextMenu'
|
||||
import { useDrag } from '@use-gesture/react'
|
||||
import { EventFocusToolbar } from './EventFocusToolbar'
|
||||
import { useOutsideClick } from '@/hooks/useOutsideClick'
|
||||
import {
|
||||
RightPanel,
|
||||
useEditor,
|
||||
} from '@/features/editor/providers/EditorProvider'
|
||||
import { useGraph } from '@/features/graph/providers/GraphProvider'
|
||||
import { useEventsCoordinates } from '@/features/graph/providers/EventsCoordinateProvider'
|
||||
import { setMultipleRefs } from '@/helpers/setMultipleRefs'
|
||||
import { Coordinates } from '@/features/graph/types'
|
||||
import { TEvent } from '@typebot.io/schemas'
|
||||
import { EventNodeContent } from './EventNodeContent'
|
||||
import { EventSourceEndpoint } from '../../endpoints/EventSourceEndpoint'
|
||||
import { eventWidth } from '@/features/graph/constants'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
|
||||
type Props = {
|
||||
event: TEvent
|
||||
eventIndex: number
|
||||
}
|
||||
|
||||
export const EventNode = ({ event, eventIndex }: Props) => {
|
||||
const { updateEventCoordinates } = useEventsCoordinates()
|
||||
|
||||
const handleEventDrag = useCallback(
|
||||
(newCoord: Coordinates) => {
|
||||
updateEventCoordinates(event.id, newCoord)
|
||||
},
|
||||
[event.id, updateEventCoordinates]
|
||||
)
|
||||
|
||||
return (
|
||||
<DraggableEventNode
|
||||
event={event}
|
||||
eventIndex={eventIndex}
|
||||
onEventDrag={handleEventDrag}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const NonMemoizedDraggableEventNode = ({
|
||||
event,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
eventIndex,
|
||||
onEventDrag,
|
||||
}: Props & { onEventDrag: (newCoord: Coordinates) => void }) => {
|
||||
const elementBgColor = useColorModeValue('white', 'gray.900')
|
||||
const previewingBorderColor = useColorModeValue('blue.400', 'blue.300')
|
||||
const { previewingEdge, isReadOnly, graphPosition } = useGraph()
|
||||
const { updateEvent } = useTypebot()
|
||||
const { setRightPanel, setStartPreviewAtEvent } = useEditor()
|
||||
|
||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||
const [currentCoordinates, setCurrentCoordinates] = useState(
|
||||
event.graphCoordinates
|
||||
)
|
||||
|
||||
const isPreviewing = previewingEdge
|
||||
? 'eventId' in previewingEdge.from
|
||||
? previewingEdge.from.eventId === event.id
|
||||
: false
|
||||
: false
|
||||
|
||||
const eventRef = useRef<HTMLDivElement | null>(null)
|
||||
const [debouncedEventPosition] = useDebounce(currentCoordinates, 100)
|
||||
const [isFocused, setIsFocused] = useState(false)
|
||||
|
||||
useOutsideClick({
|
||||
handler: () => setIsFocused(false),
|
||||
ref: eventRef,
|
||||
capture: true,
|
||||
isEnabled: isFocused,
|
||||
})
|
||||
|
||||
// When the event is moved from external action (e.g. undo/redo), update the current coordinates
|
||||
useEffect(() => {
|
||||
setCurrentCoordinates({
|
||||
x: event.graphCoordinates.x,
|
||||
y: event.graphCoordinates.y,
|
||||
})
|
||||
}, [event.graphCoordinates.x, event.graphCoordinates.y])
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentCoordinates || isReadOnly) return
|
||||
if (
|
||||
currentCoordinates?.x === event.graphCoordinates.x &&
|
||||
currentCoordinates.y === event.graphCoordinates.y
|
||||
)
|
||||
return
|
||||
updateEvent(eventIndex, { graphCoordinates: currentCoordinates })
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedEventPosition])
|
||||
|
||||
const startPreviewAtThisEvent = () => {
|
||||
setStartPreviewAtEvent(event.id)
|
||||
setRightPanel(RightPanel.PREVIEW)
|
||||
}
|
||||
|
||||
useDrag(
|
||||
({ first, last, offset: [offsetX, offsetY], event, target }) => {
|
||||
event.stopPropagation()
|
||||
if (
|
||||
(target as HTMLElement)
|
||||
.closest('.prevent-event-drag')
|
||||
?.classList.contains('prevent-event-drag')
|
||||
)
|
||||
return
|
||||
|
||||
if (first) {
|
||||
setIsFocused(true)
|
||||
setIsMouseDown(true)
|
||||
}
|
||||
if (last) {
|
||||
setIsMouseDown(false)
|
||||
}
|
||||
const newCoord = {
|
||||
x: Number((offsetX / graphPosition.scale).toFixed(2)),
|
||||
y: Number((offsetY / graphPosition.scale).toFixed(2)),
|
||||
}
|
||||
setCurrentCoordinates(newCoord)
|
||||
onEventDrag(newCoord)
|
||||
},
|
||||
{
|
||||
target: eventRef,
|
||||
pointer: { keys: false },
|
||||
from: () => [
|
||||
currentCoordinates.x * graphPosition.scale,
|
||||
currentCoordinates.y * graphPosition.scale,
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<ContextMenu<HTMLDivElement>
|
||||
renderMenu={() => <EventNodeContextMenu />}
|
||||
isDisabled={isReadOnly || event.type === 'start'}
|
||||
>
|
||||
{(ref, isContextMenuOpened) => (
|
||||
<Stack
|
||||
ref={setMultipleRefs([ref, eventRef])}
|
||||
id={`event-${event.id}`}
|
||||
data-testid="event"
|
||||
py="2"
|
||||
pl="3"
|
||||
pr="3"
|
||||
w={eventWidth}
|
||||
rounded="xl"
|
||||
bg={elementBgColor}
|
||||
borderWidth="1px"
|
||||
fontWeight="semibold"
|
||||
borderColor={
|
||||
isContextMenuOpened || isPreviewing || isFocused
|
||||
? previewingBorderColor
|
||||
: elementBgColor
|
||||
}
|
||||
transition="border 300ms, box-shadow 200ms"
|
||||
pos="absolute"
|
||||
style={{
|
||||
transform: `translate(${currentCoordinates?.x ?? 0}px, ${
|
||||
currentCoordinates?.y ?? 0
|
||||
}px)`,
|
||||
touchAction: 'none',
|
||||
}}
|
||||
cursor={isMouseDown ? 'grabbing' : 'pointer'}
|
||||
shadow="md"
|
||||
_hover={{ shadow: 'lg' }}
|
||||
zIndex={isFocused ? 10 : 1}
|
||||
>
|
||||
<EventNodeContent event={event} />
|
||||
<EventSourceEndpoint
|
||||
source={{
|
||||
eventId: event.id,
|
||||
}}
|
||||
pos="absolute"
|
||||
right="-19px"
|
||||
bottom="4px"
|
||||
isHidden={false}
|
||||
/>
|
||||
{!isReadOnly && (
|
||||
<SlideFade
|
||||
in={isFocused}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-45px',
|
||||
right: 0,
|
||||
}}
|
||||
unmountOnExit
|
||||
>
|
||||
<EventFocusToolbar
|
||||
eventId={event.id}
|
||||
onPlayClick={startPreviewAtThisEvent}
|
||||
onDeleteClick={event.type !== 'start' ? () => {} : undefined}
|
||||
/>
|
||||
</SlideFade>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</ContextMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export const DraggableEventNode = memo(NonMemoizedDraggableEventNode)
|
||||
@@ -0,0 +1,14 @@
|
||||
import { StartEventNode } from '@/features/events/start/StartEventNode'
|
||||
import { TEvent } from '@typebot.io/schemas'
|
||||
|
||||
type Props = {
|
||||
event: TEvent
|
||||
}
|
||||
export const EventNodeContent = ({ event }: Props) => {
|
||||
switch (event.type) {
|
||||
case 'start':
|
||||
return <StartEventNode />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { MenuList, MenuItem } from '@chakra-ui/react'
|
||||
import { CopyIcon, TrashIcon } from '@/components/icons'
|
||||
|
||||
type Props = {
|
||||
onDuplicateClick?: () => void
|
||||
onDeleteClick?: () => void
|
||||
}
|
||||
export const EventNodeContextMenu = ({
|
||||
onDuplicateClick,
|
||||
onDeleteClick,
|
||||
}: Props) => (
|
||||
<MenuList>
|
||||
{onDuplicateClick && (
|
||||
<MenuItem icon={<CopyIcon />} onClick={onDuplicateClick}>
|
||||
Duplicate
|
||||
</MenuItem>
|
||||
)}
|
||||
{onDeleteClick && (
|
||||
<MenuItem icon={<TrashIcon />} onClick={onDeleteClick}>
|
||||
Delete
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuList>
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
export { EventNode } from './EventNode'
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react'
|
||||
import React, { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Group } from '@typebot.io/schemas'
|
||||
import { GroupV6 } from '@typebot.io/schemas'
|
||||
import { BlockNodesList } from '../block/BlockNodesList'
|
||||
import { isDefined, isEmpty, isNotDefined } from '@typebot.io/lib'
|
||||
import { isEmpty, isNotDefined } from '@typebot.io/lib'
|
||||
import { GroupNodeContextMenu } from './GroupNodeContextMenu'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { ContextMenu } from '@/components/ContextMenu'
|
||||
@@ -26,9 +26,10 @@ import { useGraph } from '@/features/graph/providers/GraphProvider'
|
||||
import { useGroupsCoordinates } from '@/features/graph/providers/GroupsCoordinateProvider'
|
||||
import { setMultipleRefs } from '@/helpers/setMultipleRefs'
|
||||
import { Coordinates } from '@/features/graph/types'
|
||||
import { groupWidth } from '@/features/graph/constants'
|
||||
|
||||
type Props = {
|
||||
group: Group
|
||||
group: GroupV6
|
||||
groupIndex: number
|
||||
}
|
||||
|
||||
@@ -81,12 +82,11 @@ const NonMemoizedDraggableGroupNode = ({
|
||||
|
||||
const isPreviewing =
|
||||
previewingBlock?.groupId === group.id ||
|
||||
previewingEdge?.from.groupId === group.id ||
|
||||
(previewingEdge?.to.groupId === group.id &&
|
||||
isNotDefined(previewingEdge.to.blockId))
|
||||
|
||||
const isStartGroup =
|
||||
isDefined(group.blocks[0]) && group.blocks[0].type === 'start'
|
||||
(previewingEdge &&
|
||||
(('groupId' in previewingEdge.from &&
|
||||
previewingEdge.from.groupId === group.id) ||
|
||||
(previewingEdge.to.groupId === group.id &&
|
||||
isNotDefined(previewingEdge.to.blockId))))
|
||||
|
||||
const groupRef = useRef<HTMLDivElement | null>(null)
|
||||
const [debouncedGroupPosition] = useDebounce(currentCoordinates, 100)
|
||||
@@ -135,7 +135,7 @@ const NonMemoizedDraggableGroupNode = ({
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (isReadOnly) return
|
||||
if (mouseOverGroup?.id !== group.id && !isStartGroup && groupRef.current)
|
||||
if (mouseOverGroup?.id !== group.id && groupRef.current)
|
||||
setMouseOverGroup({ id: group.id, element: groupRef.current })
|
||||
if (connectingIds)
|
||||
setConnectingIds({ ...connectingIds, target: { groupId: group.id } })
|
||||
@@ -189,7 +189,7 @@ const NonMemoizedDraggableGroupNode = ({
|
||||
return (
|
||||
<ContextMenu<HTMLDivElement>
|
||||
renderMenu={() => <GroupNodeContextMenu groupIndex={groupIndex} />}
|
||||
isDisabled={isReadOnly || isStartGroup}
|
||||
isDisabled={isReadOnly}
|
||||
>
|
||||
{(ref, isContextMenuOpened) => (
|
||||
<Stack
|
||||
@@ -205,7 +205,7 @@ const NonMemoizedDraggableGroupNode = ({
|
||||
? previewingBorderColor
|
||||
: borderColor
|
||||
}
|
||||
w="300px"
|
||||
w={groupWidth}
|
||||
transition="border 300ms, box-shadow 200ms"
|
||||
pos="absolute"
|
||||
style={{
|
||||
@@ -227,7 +227,7 @@ const NonMemoizedDraggableGroupNode = ({
|
||||
onChange={setGroupTitle}
|
||||
onSubmit={handleTitleSubmit}
|
||||
fontWeight="semibold"
|
||||
pointerEvents={isReadOnly || isStartGroup ? 'none' : 'auto'}
|
||||
pointerEvents={isReadOnly ? 'none' : 'auto'}
|
||||
pr="8"
|
||||
>
|
||||
<EditablePreview
|
||||
@@ -251,14 +251,12 @@ const NonMemoizedDraggableGroupNode = ({
|
||||
</Editable>
|
||||
{typebot && (
|
||||
<BlockNodesList
|
||||
groupId={group.id}
|
||||
blocks={group.blocks}
|
||||
groupIndex={groupIndex}
|
||||
groupRef={ref}
|
||||
isStartGroup={isStartGroup}
|
||||
/>
|
||||
)}
|
||||
{!isReadOnly && !isStartGroup && (
|
||||
{!isReadOnly && (
|
||||
<SlideFade
|
||||
in={isFocused}
|
||||
style={{
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import { Flex, useColorModeValue, Stack } from '@chakra-ui/react'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import {
|
||||
ChoiceInputBlock,
|
||||
Item,
|
||||
ItemIndices,
|
||||
ItemType,
|
||||
} from '@typebot.io/schemas'
|
||||
import { BlockWithItems, Item, ItemIndices } from '@typebot.io/schemas'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { SourceEndpoint } from '../../endpoints/SourceEndpoint'
|
||||
import { BlockSourceEndpoint } from '../../endpoints/BlockSourceEndpoint'
|
||||
import { ItemNodeContent } from './ItemNodeContent'
|
||||
import { ItemNodeContextMenu } from './ItemNodeContextMenu'
|
||||
import { ContextMenu } from '@/components/ContextMenu'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { Coordinates } from '@/features/graph/types'
|
||||
import {
|
||||
DraggabbleItem,
|
||||
DraggableItem,
|
||||
NodePosition,
|
||||
useDragDistance,
|
||||
} from '@/features/graph/providers/GraphDndProvider'
|
||||
@@ -22,19 +17,22 @@ import { useGraph } from '@/features/graph/providers/GraphProvider'
|
||||
import { setMultipleRefs } from '@/helpers/setMultipleRefs'
|
||||
import { ConditionContent } from '@/features/blocks/logic/condition/components/ConditionContent'
|
||||
import { useRouter } from 'next/router'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
|
||||
type Props = {
|
||||
item: Item
|
||||
block: BlockWithItems
|
||||
indices: ItemIndices
|
||||
onMouseDown?: (
|
||||
blockNodePosition: { absolute: Coordinates; relative: Coordinates },
|
||||
item: DraggabbleItem
|
||||
item: DraggableItem
|
||||
) => void
|
||||
connectionDisabled?: boolean
|
||||
}
|
||||
|
||||
export const ItemNode = ({
|
||||
item,
|
||||
block,
|
||||
indices,
|
||||
onMouseDown,
|
||||
connectionDisabled,
|
||||
@@ -47,18 +45,21 @@ export const ItemNode = ({
|
||||
const { pathname } = useRouter()
|
||||
const [isMouseOver, setIsMouseOver] = useState(false)
|
||||
const itemRef = useRef<HTMLDivElement | null>(null)
|
||||
const isPreviewing = previewingEdge?.from.itemId === item.id
|
||||
const isPreviewing =
|
||||
previewingEdge &&
|
||||
'itemId' in previewingEdge.from &&
|
||||
previewingEdge.from.itemId === item.id
|
||||
const isConnectable =
|
||||
isDefined(typebot) &&
|
||||
!connectionDisabled &&
|
||||
!(
|
||||
typebot.groups[indices.groupIndex].blocks[indices.blockIndex] as
|
||||
| ChoiceInputBlock
|
||||
| undefined
|
||||
)?.options?.isMultipleChoice
|
||||
block.options &&
|
||||
'isMultipleChoice' in block.options &&
|
||||
block.options.isMultipleChoice
|
||||
)
|
||||
const onDrag = (position: NodePosition) => {
|
||||
if (!onMouseDown || item.type === ItemType.AB_TEST) return
|
||||
onMouseDown(position, item)
|
||||
if (!onMouseDown || block.type === LogicBlockType.AB_TEST) return
|
||||
onMouseDown(position, { ...item, type: block.type })
|
||||
}
|
||||
useDragDistance({
|
||||
ref: itemRef,
|
||||
@@ -108,20 +109,18 @@ export const ItemNode = ({
|
||||
w="full"
|
||||
>
|
||||
<ItemNodeContent
|
||||
blockType={block.type}
|
||||
item={item}
|
||||
isMouseOver={isMouseOver}
|
||||
indices={indices}
|
||||
/>
|
||||
{typebot && (isConnectable || pathname.endsWith('analytics')) && (
|
||||
<SourceEndpoint
|
||||
<BlockSourceEndpoint
|
||||
source={{
|
||||
groupId: typebot.groups[indices.groupIndex].id,
|
||||
blockId:
|
||||
typebot.groups[indices.groupIndex]?.blocks[
|
||||
indices.blockIndex
|
||||
]?.id,
|
||||
blockId: block.id,
|
||||
itemId: item.id,
|
||||
}}
|
||||
groupId={typebot.groups[indices.groupIndex].id}
|
||||
pos="absolute"
|
||||
right="-49px"
|
||||
bottom="9px"
|
||||
|
||||
@@ -1,31 +1,41 @@
|
||||
import { ButtonsItemNode } from '@/features/blocks/inputs/buttons/components/ButtonsItemNode'
|
||||
import { PictureChoiceItemNode } from '@/features/blocks/inputs/pictureChoice/components/PictureChoiceItemNode'
|
||||
import { ConditionItemNode } from '@/features/blocks/logic/condition/components/ConditionItemNode'
|
||||
import { Item, ItemIndices, ItemType } from '@typebot.io/schemas'
|
||||
import {
|
||||
BlockWithItems,
|
||||
ButtonItem,
|
||||
ConditionItem,
|
||||
Item,
|
||||
ItemIndices,
|
||||
} from '@typebot.io/schemas'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
item: Item
|
||||
blockType: BlockWithItems['type']
|
||||
indices: ItemIndices
|
||||
isMouseOver: boolean
|
||||
}
|
||||
|
||||
export const ItemNodeContent = ({
|
||||
item,
|
||||
blockType,
|
||||
indices,
|
||||
isMouseOver,
|
||||
}: Props): JSX.Element => {
|
||||
switch (item.type) {
|
||||
case ItemType.BUTTON:
|
||||
switch (blockType) {
|
||||
case InputBlockType.CHOICE:
|
||||
return (
|
||||
<ButtonsItemNode
|
||||
key={`${item.id}-${item.content}`}
|
||||
item={item}
|
||||
item={item as ButtonItem}
|
||||
key={`${item.id}-${(item as ButtonItem).content}`}
|
||||
isMouseOver={isMouseOver}
|
||||
indices={indices}
|
||||
/>
|
||||
)
|
||||
case ItemType.PICTURE_CHOICE:
|
||||
case InputBlockType.PICTURE_CHOICE:
|
||||
return (
|
||||
<PictureChoiceItemNode
|
||||
item={item}
|
||||
@@ -33,15 +43,15 @@ export const ItemNodeContent = ({
|
||||
indices={indices}
|
||||
/>
|
||||
)
|
||||
case ItemType.CONDITION:
|
||||
case LogicBlockType.CONDITION:
|
||||
return (
|
||||
<ConditionItemNode
|
||||
item={item}
|
||||
item={item as ConditionItem}
|
||||
isMouseOver={isMouseOver}
|
||||
indices={indices}
|
||||
/>
|
||||
)
|
||||
case ItemType.AB_TEST:
|
||||
case LogicBlockType.AB_TEST:
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,7 @@ import {
|
||||
useEventListener,
|
||||
} from '@chakra-ui/react'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import {
|
||||
BlockIndices,
|
||||
BlockWithItems,
|
||||
LogicBlockType,
|
||||
} from '@typebot.io/schemas'
|
||||
import { BlockIndices, BlockWithItems } from '@typebot.io/schemas'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { ItemNode } from './ItemNode'
|
||||
import { PlaceholderNode } from '../PlaceholderNode'
|
||||
@@ -19,11 +15,13 @@ import { isDefined } from '@typebot.io/lib'
|
||||
import {
|
||||
useBlockDnd,
|
||||
computeNearestPlaceholderIndex,
|
||||
DraggabbleItem,
|
||||
DraggableItem,
|
||||
} from '@/features/graph/providers/GraphDndProvider'
|
||||
import { useGraph } from '@/features/graph/providers/GraphProvider'
|
||||
import { Coordinates } from '@dnd-kit/utilities'
|
||||
import { SourceEndpoint } from '../../endpoints/SourceEndpoint'
|
||||
import { BlockSourceEndpoint } from '../../endpoints/BlockSourceEndpoint'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
|
||||
type Props = {
|
||||
block: BlockWithItems
|
||||
@@ -41,12 +39,18 @@ export const ItemNodesList = ({
|
||||
const isDraggingOnCurrentBlock =
|
||||
(draggedItem && mouseOverBlock?.id === block.id) ?? false
|
||||
const showPlaceholders =
|
||||
draggedItem !== undefined && block.items.at(0)?.type === draggedItem.type
|
||||
draggedItem !== undefined && block.type === draggedItem.type
|
||||
|
||||
const isLastBlock =
|
||||
isDefined(typebot) &&
|
||||
typebot.groups[groupIndex]?.blocks?.[blockIndex + 1] === undefined
|
||||
|
||||
const someChoiceItemsAreNotConnected =
|
||||
block.type === InputBlockType.CHOICE ||
|
||||
block.type === InputBlockType.PICTURE_CHOICE
|
||||
? block.items.some((item) => item.outgoingEdgeId === undefined)
|
||||
: true
|
||||
|
||||
const [position, setPosition] = useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -57,7 +61,7 @@ export const ItemNodesList = ({
|
||||
>()
|
||||
|
||||
const handleGlobalMouseMove = (event: MouseEvent) => {
|
||||
if (!draggedItem || draggedItem.blockId !== block.id) return
|
||||
if (!draggedItem) return
|
||||
const { clientX, clientY } = event
|
||||
setPosition({
|
||||
...position,
|
||||
@@ -107,7 +111,7 @@ export const ItemNodesList = ({
|
||||
(itemIndex: number) =>
|
||||
(
|
||||
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
|
||||
item: DraggabbleItem
|
||||
item: DraggableItem
|
||||
) => {
|
||||
if (!typebot || block.items.length <= 1) return
|
||||
placeholderRefs.current.splice(itemIndex + 1, 1)
|
||||
@@ -116,7 +120,6 @@ export const ItemNodesList = ({
|
||||
setRelativeCoordinates(relative)
|
||||
setDraggedItem({
|
||||
...item,
|
||||
blockId: block.id,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -138,6 +141,7 @@ export const ItemNodesList = ({
|
||||
<Stack key={item.id} spacing={1}>
|
||||
<ItemNode
|
||||
item={item}
|
||||
block={block}
|
||||
indices={{ groupIndex, blockIndex, itemIndex: idx }}
|
||||
onMouseDown={handleBlockMouseDown(idx)}
|
||||
/>
|
||||
@@ -148,9 +152,14 @@ export const ItemNodesList = ({
|
||||
/>
|
||||
</Stack>
|
||||
))}
|
||||
{isLastBlock && <DefaultItemNode block={block} />}
|
||||
{isLastBlock && someChoiceItemsAreNotConnected && (
|
||||
<DefaultItemNode
|
||||
block={block}
|
||||
groupId={typebot.groups[groupIndex].id}
|
||||
/>
|
||||
)}
|
||||
|
||||
{draggedItem && draggedItem.blockId === block.id && (
|
||||
{draggedItem && (
|
||||
<Portal>
|
||||
<Flex
|
||||
pointerEvents="none"
|
||||
@@ -165,6 +174,7 @@ export const ItemNodesList = ({
|
||||
>
|
||||
<ItemNode
|
||||
item={draggedItem}
|
||||
block={block}
|
||||
indices={{ groupIndex, blockIndex, itemIndex: 0 }}
|
||||
connectionDisabled
|
||||
/>
|
||||
@@ -175,7 +185,13 @@ export const ItemNodesList = ({
|
||||
)
|
||||
}
|
||||
|
||||
const DefaultItemNode = ({ block }: { block: BlockWithItems }) => {
|
||||
const DefaultItemNode = ({
|
||||
block,
|
||||
groupId,
|
||||
}: {
|
||||
block: BlockWithItems
|
||||
groupId: string
|
||||
}) => {
|
||||
return (
|
||||
<Flex
|
||||
px="4"
|
||||
@@ -191,11 +207,11 @@ const DefaultItemNode = ({ block }: { block: BlockWithItems }) => {
|
||||
<Text color="gray.500">
|
||||
{block.type === LogicBlockType.CONDITION ? 'Else' : 'Default'}
|
||||
</Text>
|
||||
<SourceEndpoint
|
||||
<BlockSourceEndpoint
|
||||
source={{
|
||||
groupId: block.groupId,
|
||||
blockId: block.id,
|
||||
}}
|
||||
groupId={groupId}
|
||||
pos="absolute"
|
||||
right="-49px"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user