♻️ Introduce typebot v6 with events (#1013)

Closes #885
This commit is contained in:
Baptiste Arnaud
2023-11-08 15:34:16 +01:00
committed by GitHub
parent 68e4fc71fb
commit 35300eaf34
634 changed files with 58971 additions and 31449 deletions

View File

@@ -1,7 +1,7 @@
import { Flex, FlexProps, useEventListener } from '@chakra-ui/react'
import React, { useRef, useMemo, useEffect, useState } from 'react'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { DraggableBlockType, PublicTypebot, Typebot } from '@typebot.io/schemas'
import { BlockV6, PublicTypebotV6, TypebotV6 } from '@typebot.io/schemas'
import { useDebounce } from 'use-debounce'
import GraphElements from './GraphElements'
import { createId } from '@paralleldrive/cuid2'
@@ -10,12 +10,15 @@ import { ZoomButtons } from './ZoomButtons'
import { useGesture } from '@use-gesture/react'
import { GraphNavigation } from '@typebot.io/prisma'
import { headerHeight } from '@/features/editor/constants'
import { graphPositionDefaultValue, blockWidth } from '../constants'
import { graphPositionDefaultValue, groupWidth } from '../constants'
import { useBlockDnd } from '../providers/GraphDndProvider'
import { useGraph } from '../providers/GraphProvider'
import { useGroupsCoordinates } from '../providers/GroupsCoordinateProvider'
import { Coordinates } from '../types'
import { TotalAnswersInBlock } from '@typebot.io/schemas/features/analytics'
import {
TotalAnswers,
TotalVisitedEdges,
} from '@typebot.io/schemas/features/analytics'
const maxScale = 2
const minScale = 0.3
@@ -23,12 +26,14 @@ const zoomButtonsScaleBlock = 0.2
export const Graph = ({
typebot,
totalAnswersInBlocks,
totalAnswers,
totalVisitedEdges,
onUnlockProPlanClick,
...props
}: {
typebot: Typebot | PublicTypebot
totalAnswersInBlocks?: TotalAnswersInBlock[]
typebot: TypebotV6 | PublicTypebotV6
totalVisitedEdges?: TotalVisitedEdges[]
totalAnswers?: TotalAnswers[]
onUnlockProPlanClick?: () => void
} & FlexProps) => {
const {
@@ -52,7 +57,7 @@ export const Graph = ({
const { updateGroupCoordinates } = useGroupsCoordinates()
const [graphPosition, setGraphPosition] = useState(
graphPositionDefaultValue(
typebot.groups.at(0)?.graphCoordinates ?? { x: 0, y: 0 }
typebot.events[0].graphCoordinates ?? { x: 0, y: 0 }
)
)
const [debouncedGraphPosition] = useDebounce(graphPosition, 200)
@@ -100,7 +105,7 @@ export const Graph = ({
createGroup({
id,
...coordinates,
block: draggedBlock ?? (draggedBlockType as DraggableBlockType),
block: draggedBlock ?? (draggedBlockType as BlockV6['type']),
indices: { groupIndex: typebot.groups.length, blockIndex: 0 },
})
setDraggedBlock(undefined)
@@ -254,7 +259,9 @@ export const Graph = ({
<GraphElements
edges={typebot.edges}
groups={typebot.groups}
totalAnswersInBlocks={totalAnswersInBlocks}
events={typebot.events}
totalAnswers={totalAnswers}
totalVisitedEdges={totalVisitedEdges}
onUnlockProPlanClick={onUnlockProPlanClick}
/>
</Flex>
@@ -270,7 +277,7 @@ const projectMouse = (
x:
(mouseCoordinates.x -
graphPosition.x -
blockWidth / (3 / graphPosition.scale)) /
groupWidth / (3 / graphPosition.scale)) /
graphPosition.scale,
y:
(mouseCoordinates.y -

View File

@@ -1,29 +1,49 @@
import { Edge, Group } from '@typebot.io/schemas'
import { TotalAnswersInBlock } from '@typebot.io/schemas/features/analytics'
import { Edge, GroupV6, TEvent } from '@typebot.io/schemas'
import {
TotalAnswers,
TotalVisitedEdges,
} from '@typebot.io/schemas/features/analytics'
import React, { memo } from 'react'
import { EndpointsProvider } from '../providers/EndpointsProvider'
import { Edges } from './edges/Edges'
import { GroupNode } from './nodes/group'
import { isInputBlock } from '@typebot.io/lib'
import { EventNode } from './nodes/event'
type Props = {
edges: Edge[]
groups: Group[]
totalAnswersInBlocks?: TotalAnswersInBlock[]
groups: GroupV6[]
events: TEvent[]
totalVisitedEdges?: TotalVisitedEdges[]
totalAnswers?: TotalAnswers[]
onUnlockProPlanClick?: () => void
}
const GroupNodes = ({
edges,
groups,
totalAnswersInBlocks,
events,
totalVisitedEdges,
totalAnswers,
onUnlockProPlanClick,
}: Props) => {
const inputBlockIds = groups
.flatMap((g) => g.blocks)
.filter(isInputBlock)
.map((b) => b.id)
return (
<EndpointsProvider>
<Edges
edges={edges}
totalAnswersInBlocks={totalAnswersInBlocks}
groups={groups}
totalAnswers={totalAnswers}
totalVisitedEdges={totalVisitedEdges}
inputBlockIds={inputBlockIds}
onUnlockProPlanClick={onUnlockProPlanClick}
/>
{events.map((event, idx) => (
<EventNode event={event} key={event.id} eventIndex={idx} />
))}
{groups.map((group, idx) => (
<GroupNode group={group} groupIndex={idx} key={group.id} />
))}

View File

@@ -10,6 +10,8 @@ import { computeEdgePathToMouse } from '../../helpers/computeEdgePathToMouth'
import { useGraph } from '../../providers/GraphProvider'
import { useGroupsCoordinates } from '../../providers/GroupsCoordinateProvider'
import { ConnectingIds } from '../../types'
import { useEventsCoordinates } from '../../providers/EventsCoordinateProvider'
import { eventWidth, groupWidth } from '../../constants'
export const DrawingEdge = () => {
const { graphPosition, setConnectingIds, connectingIds } = useGraph()
@@ -18,18 +20,25 @@ export const DrawingEdge = () => {
targetEndpointYOffsets: targetEndpoints,
} = useEndpoints()
const { groupsCoordinates } = useGroupsCoordinates()
const { eventsCoordinates } = useEventsCoordinates()
const { createEdge } = useTypebot()
const [mousePosition, setMousePosition] = useState<Coordinates | null>(null)
const sourceGroupCoordinates =
groupsCoordinates && groupsCoordinates[connectingIds?.source.groupId ?? '']
const sourceElementCoordinates = connectingIds
? 'eventId' in connectingIds.source
? eventsCoordinates[connectingIds?.source.eventId]
: groupsCoordinates[connectingIds?.source.groupId ?? '']
: undefined
const targetGroupCoordinates =
groupsCoordinates && groupsCoordinates[connectingIds?.target?.groupId ?? '']
const sourceTop = useMemo(() => {
if (!connectingIds) return 0
const endpointId =
connectingIds.source.itemId ?? connectingIds.source.blockId
'eventId' in connectingIds.source
? connectingIds.source.eventId
: connectingIds.source.itemId ?? connectingIds.source.blockId
return sourceEndpoints.get(endpointId)?.y
}, [connectingIds, sourceEndpoints])
@@ -40,27 +49,38 @@ export const DrawingEdge = () => {
}, [connectingIds, targetEndpoints])
const path = useMemo(() => {
if (!sourceTop || !sourceGroupCoordinates || !mousePosition) return ``
if (
!sourceTop ||
!sourceElementCoordinates ||
!mousePosition ||
!connectingIds?.source
)
return ``
return targetGroupCoordinates
? computeConnectingEdgePath({
sourceGroupCoordinates,
sourceGroupCoordinates: sourceElementCoordinates,
targetGroupCoordinates,
elementWidth:
'eventId' in connectingIds.source ? eventWidth : groupWidth,
sourceTop,
targetTop,
graphScale: graphPosition.scale,
})
: computeEdgePathToMouse({
sourceGroupCoordinates,
sourceGroupCoordinates: sourceElementCoordinates,
mousePosition,
sourceTop,
elementWidth:
'eventId' in connectingIds.source ? eventWidth : groupWidth,
})
}, [
sourceTop,
sourceGroupCoordinates,
targetGroupCoordinates,
targetTop,
sourceElementCoordinates,
mousePosition,
targetGroupCoordinates,
connectingIds?.source,
targetTop,
graphPosition.scale,
])

View File

@@ -14,9 +14,14 @@ import { useGroupsCoordinates } from '../../providers/GroupsCoordinateProvider'
import { hasProPerks } from '@/features/billing/helpers/hasProPerks'
import { computeDropOffPath } from '../../helpers/computeDropOffPath'
import { computeSourceCoordinates } from '../../helpers/computeSourceCoordinates'
import { TotalAnswersInBlock } from '@typebot.io/schemas/features/analytics'
import { computePreviousTotalAnswers } from '@/features/analytics/helpers/computePreviousTotalAnswers'
import { blockHasItems } from '@typebot.io/lib'
import {
TotalAnswers,
TotalVisitedEdges,
} from '@typebot.io/schemas/features/analytics'
import { computeTotalUsersAtBlock } from '@/features/analytics/helpers/computeTotalUsersAtBlock'
import { blockHasItems, byId } from '@typebot.io/lib'
import { groupWidth } from '../../constants'
import { getTotalAnswersAtBlock } from '@/features/analytics/helpers/getTotalAnswersAtBlock'
export const dropOffBoxDimensions = {
width: 100,
@@ -30,13 +35,15 @@ const dropOffSegmentMaxWidth = 20
export const dropOffStubLength = 30
type Props = {
totalAnswersInBlocks: TotalAnswersInBlock[]
blockId: string
totalVisitedEdges: TotalVisitedEdges[]
totalAnswers: TotalAnswers[]
onUnlockProPlanClick?: () => void
}
export const DropOffEdge = ({
totalAnswersInBlocks,
totalVisitedEdges,
totalAnswers,
blockId,
onUnlockProPlanClick,
}: Props) => {
@@ -48,86 +55,72 @@ export const DropOffEdge = ({
const { groupsCoordinates } = useGroupsCoordinates()
const { sourceEndpointYOffsets: sourceEndpoints } = useEndpoints()
const { publishedTypebot } = useTypebot()
const currentBlock = useMemo(
const currentBlockId = useMemo(
() =>
totalAnswersInBlocks.reduce<TotalAnswersInBlock | undefined>(
(block, totalAnswersInBlock) => {
if (totalAnswersInBlock.blockId === blockId) {
return block
? { ...block, total: block.total + totalAnswersInBlock.total }
: totalAnswersInBlock
}
return block
},
undefined
),
[blockId, totalAnswersInBlocks]
publishedTypebot?.groups.flatMap((g) => g.blocks)?.find(byId(blockId))
?.id,
[blockId, publishedTypebot?.groups]
)
const isWorkspaceProPlan = hasProPerks(workspace)
const { totalDroppedUser, dropOffRate } = useMemo(() => {
if (!publishedTypebot || currentBlock?.total === undefined)
return { previousTotal: undefined, dropOffRate: undefined }
const totalAnswers = currentBlock.total
const previousTotal = computePreviousTotalAnswers(
if (!publishedTypebot || !currentBlockId) return {}
const totalUsersAtBlock = computeTotalUsersAtBlock(currentBlockId, {
publishedTypebot,
currentBlock.blockId,
totalAnswersInBlocks
)
if (previousTotal === 0)
return { previousTotal: undefined, dropOffRate: undefined }
const totalDroppedUser = previousTotal - totalAnswers
totalVisitedEdges,
totalAnswers,
})
const totalBlockReplies = getTotalAnswersAtBlock(currentBlockId, {
publishedTypebot,
totalAnswers,
})
if (totalUsersAtBlock === 0) return {}
const totalDroppedUser = totalUsersAtBlock - totalBlockReplies
return {
totalDroppedUser,
dropOffRate: Math.round((totalDroppedUser / previousTotal) * 100),
dropOffRate: Math.round((totalDroppedUser / totalUsersAtBlock) * 100),
}
}, [
currentBlock?.blockId,
currentBlock?.total,
publishedTypebot,
totalAnswersInBlocks,
])
}, [currentBlockId, publishedTypebot, totalAnswers, totalVisitedEdges])
const sourceTop = useMemo(() => {
const blockTop = currentBlock?.blockId
? sourceEndpoints.get(currentBlock.blockId)?.y
const blockTop = currentBlockId
? sourceEndpoints.get(currentBlockId)?.y
: undefined
if (blockTop) return blockTop
const block = publishedTypebot?.groups
.flatMap((group) => group.blocks)
.find((block) => block.id === currentBlock?.blockId)
.find((block) => block.id === currentBlockId)
if (!block || !blockHasItems(block)) return 0
const itemId = block.items.at(-1)?.id
if (!itemId) return 0
return sourceEndpoints.get(itemId)?.y
}, [currentBlock?.blockId, publishedTypebot?.groups, sourceEndpoints])
}, [currentBlockId, publishedTypebot?.groups, sourceEndpoints])
const endpointCoordinates = useMemo(() => {
const groupId = publishedTypebot?.groups.find((group) =>
group.blocks.some((block) => block.id === currentBlock?.blockId)
group.blocks.some((block) => block.id === currentBlockId)
)?.id
if (!groupId) return undefined
const coordinates = groupsCoordinates[groupId]
if (!coordinates) return undefined
return computeSourceCoordinates(coordinates, sourceTop ?? 0)
}, [
publishedTypebot?.groups,
groupsCoordinates,
sourceTop,
currentBlock?.blockId,
])
return computeSourceCoordinates({
sourcePosition: coordinates,
sourceTop: sourceTop ?? 0,
elementWidth: groupWidth,
})
}, [publishedTypebot?.groups, groupsCoordinates, sourceTop, currentBlockId])
const isLastBlock = useMemo(() => {
if (!publishedTypebot) return false
const lastBlock = publishedTypebot.groups
.find((group) =>
group.blocks.some((block) => block.id === currentBlock?.blockId)
group.blocks.some((block) => block.id === currentBlockId)
)
?.blocks.at(-1)
return lastBlock?.id === currentBlock?.blockId
}, [publishedTypebot, currentBlock?.blockId])
return lastBlock?.id === currentBlockId
}, [publishedTypebot, currentBlockId])
if (!endpointCoordinates) return null

View File

@@ -9,34 +9,42 @@ import { getAnchorsPosition } from '../../helpers/getAnchorsPosition'
import { useGraph } from '../../providers/GraphProvider'
import { useGroupsCoordinates } from '../../providers/GroupsCoordinateProvider'
import { EdgeMenu } from './EdgeMenu'
import { useEventsCoordinates } from '../../providers/EventsCoordinateProvider'
import { eventWidth, groupWidth } from '../../constants'
type Props = {
edge: EdgeProps
fromGroupId: string | undefined
}
export const Edge = ({ edge }: Props) => {
export const Edge = ({ edge, fromGroupId }: Props) => {
const isDark = useColorMode().colorMode === 'dark'
const { deleteEdge } = useTypebot()
const { previewingEdge, graphPosition, isReadOnly, setPreviewingEdge } =
useGraph()
const { sourceEndpointYOffsets, targetEndpointYOffsets } = useEndpoints()
const { groupsCoordinates } = useGroupsCoordinates()
const { eventsCoordinates } = useEventsCoordinates()
const [isMouseOver, setIsMouseOver] = useState(false)
const { isOpen, onOpen, onClose } = useDisclosure()
const [edgeMenuPosition, setEdgeMenuPosition] = useState({ x: 0, y: 0 })
const isPreviewing = isMouseOver || previewingEdge?.id === edge.id
const sourceGroupCoordinates =
groupsCoordinates && groupsCoordinates[edge.from.groupId]
const targetGroupCoordinates =
groupsCoordinates && groupsCoordinates[edge.to.groupId]
const sourceElementCoordinates =
'eventId' in edge.from
? eventsCoordinates[edge.from.eventId]
: groupsCoordinates[fromGroupId as string]
const targetGroupCoordinates = groupsCoordinates[edge.to.groupId]
const sourceTop = useMemo(() => {
const endpointId = edge?.from.itemId ?? edge?.from.blockId
const endpointId =
'eventId' in edge.from
? edge.from.eventId
: edge?.from.itemId ?? edge?.from.blockId
if (!endpointId) return
return sourceEndpointYOffsets.get(endpointId)?.y
}, [edge?.from.itemId, edge?.from.blockId, sourceEndpointYOffsets])
}, [edge.from, sourceEndpointYOffsets])
const targetTop = useMemo(
() =>
@@ -47,20 +55,22 @@ export const Edge = ({ edge }: Props) => {
)
const path = useMemo(() => {
if (!sourceGroupCoordinates || !targetGroupCoordinates || !sourceTop)
if (!sourceElementCoordinates || !targetGroupCoordinates || !sourceTop)
return ``
const anchorsPosition = getAnchorsPosition({
sourceGroupCoordinates,
sourceGroupCoordinates: sourceElementCoordinates,
targetGroupCoordinates,
elementWidth: 'eventId' in edge.from ? eventWidth : groupWidth,
sourceTop,
targetTop,
graphScale: graphPosition.scale,
})
return computeEdgePath(anchorsPosition)
}, [
sourceGroupCoordinates,
sourceElementCoordinates,
targetGroupCoordinates,
sourceTop,
edge.from,
targetTop,
graphPosition.scale,
])

View File

@@ -1,34 +1,33 @@
import { chakra, useColorMode } from '@chakra-ui/react'
import { colors } from '@/lib/theme'
import { Edge as EdgeProps } from '@typebot.io/schemas'
import React, { useMemo } from 'react'
import { BlockSource, Edge as EdgeProps, GroupV6 } from '@typebot.io/schemas'
import React from 'react'
import { DrawingEdge } from './DrawingEdge'
import { DropOffEdge } from './DropOffEdge'
import { Edge } from './Edge'
import { TotalAnswersInBlock } from '@typebot.io/schemas/features/analytics'
import {
TotalAnswers,
TotalVisitedEdges,
} from '@typebot.io/schemas/features/analytics'
type Props = {
edges: EdgeProps[]
totalAnswersInBlocks?: TotalAnswersInBlock[]
groups: GroupV6[]
inputBlockIds: string[]
totalVisitedEdges?: TotalVisitedEdges[]
totalAnswers?: TotalAnswers[]
onUnlockProPlanClick?: () => void
}
export const Edges = ({
edges,
totalAnswersInBlocks,
groups,
inputBlockIds,
totalVisitedEdges,
totalAnswers,
onUnlockProPlanClick,
}: Props) => {
const isDark = useColorMode().colorMode === 'dark'
const uniqueBlockIds = useMemo(
() => [
...new Set(
totalAnswersInBlocks?.map(
(totalAnswersInBlock) => totalAnswersInBlock.blockId
)
),
],
[totalAnswersInBlocks]
)
return (
<chakra.svg
width="full"
@@ -41,19 +40,31 @@ export const Edges = ({
>
<DrawingEdge />
{edges.map((edge) => (
<Edge key={edge.id} edge={edge} />
<Edge
key={edge.id}
edge={edge}
fromGroupId={
'blockId' in edge.from
? groups.find((g) =>
g.blocks.some(
(b) => b.id === (edge.from as BlockSource).blockId
)
)?.id
: undefined
}
/>
))}
{totalAnswersInBlocks &&
uniqueBlockIds
?.slice(1)
.map((blockId) => (
<DropOffEdge
key={blockId}
blockId={blockId}
totalAnswersInBlocks={totalAnswersInBlocks}
onUnlockProPlanClick={onUnlockProPlanClick}
/>
))}
{totalVisitedEdges &&
totalAnswers &&
inputBlockIds.map((blockId) => (
<DropOffEdge
key={blockId}
blockId={blockId}
totalVisitedEdges={totalVisitedEdges}
totalAnswers={totalAnswers}
onUnlockProPlanClick={onUnlockProPlanClick}
/>
))}
<marker
id={'arrow'}
refX="8"

View File

@@ -4,7 +4,7 @@ import {
useColorModeValue,
useEventListener,
} from '@chakra-ui/react'
import { Source } from '@typebot.io/schemas'
import { BlockSource } from '@typebot.io/schemas'
import React, {
useEffect,
useLayoutEffect,
@@ -18,12 +18,14 @@ import { useGroupsCoordinates } from '../../providers/GroupsCoordinateProvider'
const endpointHeight = 32
export const SourceEndpoint = ({
export const BlockSourceEndpoint = ({
source,
groupId,
isHidden,
...props
}: BoxProps & {
source: Source
source: BlockSource
groupId?: string
isHidden?: boolean
}) => {
const id = source.itemId ?? source.blockId
@@ -59,19 +61,19 @@ export const SourceEndpoint = ({
const resizeObserver = new ResizeObserver((entries) => {
setGroupHeight(entries[0].contentRect.height)
})
const groupElement = document.getElementById(`group-${source.groupId}`)
const groupElement = document.getElementById(`group-${groupId}`)
if (!groupElement) return
resizeObserver.observe(groupElement)
return () => {
resizeObserver.disconnect()
}
}, [source.groupId])
}, [groupId])
useLayoutEffect(() => {
const mutationObserver = new MutationObserver((entries) => {
setGroupTransformProp((entries[0].target as HTMLElement).style.transform)
})
const groupElement = document.getElementById(`group-${source.groupId}`)
const groupElement = document.getElementById(`group-${groupId}`)
if (!groupElement) return
mutationObserver.observe(groupElement, {
attributes: true,
@@ -80,7 +82,7 @@ export const SourceEndpoint = ({
return () => {
mutationObserver.disconnect()
}
}, [source.groupId])
}, [groupId])
useEffect(() => {
if (!endpointY) return
@@ -101,7 +103,7 @@ export const SourceEndpoint = ({
'pointerdown',
(e) => {
e.stopPropagation()
setConnectingIds({ source })
if (groupId) setConnectingIds({ source: { ...source, groupId } })
},
ref.current
)
@@ -142,6 +144,7 @@ export const SourceEndpoint = ({
shadow={`sm`}
borderColor={
previewingEdge &&
'blockId' in previewingEdge.from &&
previewingEdge.from.blockId === source.blockId &&
previewingEdge.from.itemId === source.itemId
? connectedColor

View File

@@ -0,0 +1,138 @@
import {
BoxProps,
Flex,
useColorModeValue,
useEventListener,
} from '@chakra-ui/react'
import React, {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useEndpoints } from '../../providers/EndpointsProvider'
import { useGraph } from '../../providers/GraphProvider'
import { TEventSource } from '@typebot.io/schemas'
import { isNotDefined } from '@typebot.io/lib'
const endpointHeight = 32
export const EventSourceEndpoint = ({
source,
isHidden,
...props
}: BoxProps & {
source: TEventSource
isHidden?: boolean
}) => {
const color = useColorModeValue('blue.200', 'blue.100')
const connectedColor = useColorModeValue('blue.300', 'blue.200')
const bg = useColorModeValue('gray.100', 'gray.700')
const { setConnectingIds, previewingEdge, graphPosition } = useGraph()
const { setSourceEndpointYOffset, deleteSourceEndpointYOffset } =
useEndpoints()
const [eventTransformProp, setEventTransformProp] = useState<string>()
const ref = useRef<HTMLDivElement | null>(null)
const endpointY = useMemo(
() =>
ref.current
? Number(
(
(ref.current?.getBoundingClientRect().y +
(endpointHeight * graphPosition.scale) / 2 -
graphPosition.y) /
graphPosition.scale
).toFixed(2)
)
: undefined,
// We need to force recompute whenever the event node position changes
// eslint-disable-next-line react-hooks/exhaustive-deps
[graphPosition.scale, graphPosition.y, eventTransformProp]
)
useLayoutEffect(() => {
const mutationObserver = new MutationObserver((entries) => {
setEventTransformProp((entries[0].target as HTMLElement).style.transform)
})
const groupElement = document.getElementById(`event-${source.eventId}`)
if (!groupElement) return
mutationObserver.observe(groupElement, {
attributes: true,
attributeFilter: ['style'],
})
return () => {
mutationObserver.disconnect()
}
}, [source.eventId])
useEffect(() => {
if (isNotDefined(endpointY)) return
setSourceEndpointYOffset?.({
id: source.eventId,
y: endpointY,
})
}, [endpointY, setSourceEndpointYOffset, source.eventId])
useEffect(
() => () => {
deleteSourceEndpointYOffset?.(source.eventId)
},
[deleteSourceEndpointYOffset, source.eventId]
)
useEventListener(
'pointerdown',
(e) => {
e.stopPropagation()
setConnectingIds({ source })
},
ref.current
)
useEventListener(
'mousedown',
(e) => {
e.stopPropagation()
},
ref.current
)
return (
<Flex
ref={ref}
data-testid="endpoint"
boxSize="32px"
rounded="full"
cursor="copy"
justify="center"
align="center"
pointerEvents="all"
visibility={isHidden ? 'hidden' : 'visible'}
{...props}
>
<Flex
boxSize="20px"
justify="center"
align="center"
bg={bg}
rounded="full"
>
<Flex
boxSize="13px"
rounded="full"
borderWidth="3.5px"
shadow={`sm`}
borderColor={
previewingEdge &&
'eventId' in previewingEdge.from &&
previewingEdge.from.eventId === source.eventId
? connectedColor
: color
}
/>
</Flex>
</Flex>
)
}

View File

@@ -16,7 +16,7 @@ export const TargetEndpoint = ({
blockId,
...props
}: BoxProps & {
groupId: string
groupId?: string
blockId: string
}) => {
const { setTargetEnpointYOffset: addTargetEndpoint } = useEndpoints()

View File

@@ -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)

View File

@@ -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>
}
}
}

View File

@@ -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"

View File

@@ -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}

View File

@@ -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 = {

View File

@@ -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
}
}

View File

@@ -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>
)
}

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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>
)

View File

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

View File

@@ -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={{

View File

@@ -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"

View File

@@ -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 <></>
}
}

View File

@@ -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"
/>

View File

@@ -1,21 +1,22 @@
import { Coordinates } from './types'
export const stubLength = 20
export const blockWidth = 300
export const blockAnchorsOffset = {
export const groupWidth = 300
export const groupAnchorsOffset = {
left: {
x: 0,
y: 20,
y: stubLength,
},
top: {
x: blockWidth / 2,
x: groupWidth / 2,
y: 0,
},
right: {
x: blockWidth,
y: 20,
x: groupWidth,
y: stubLength,
},
}
export const eventWidth = 200
export const graphPositionDefaultValue = (
firstGroupCoordinates: Coordinates

View File

@@ -7,6 +7,7 @@ import {
export const computeConnectingEdgePath = ({
sourceGroupCoordinates,
targetGroupCoordinates,
elementWidth,
sourceTop,
targetTop,
graphScale,
@@ -14,6 +15,7 @@ export const computeConnectingEdgePath = ({
const anchorsPosition = getAnchorsPosition({
sourceGroupCoordinates,
targetGroupCoordinates,
elementWidth,
sourceTop,
targetTop,
graphScale,

View File

@@ -1,5 +1,5 @@
import { roundCorners } from 'svg-round-corners'
import { blockWidth, pathRadius } from '../constants'
import { pathRadius } from '../constants'
import { Coordinates } from '../types'
import { computeThreeSegments } from './segments'
@@ -7,20 +7,22 @@ export const computeEdgePathToMouse = ({
sourceGroupCoordinates,
mousePosition,
sourceTop,
elementWidth,
}: {
sourceGroupCoordinates: Coordinates
mousePosition: Coordinates
sourceTop: number
elementWidth: number
}): string => {
const sourcePosition = {
x:
mousePosition.x - sourceGroupCoordinates.x > blockWidth / 2
? sourceGroupCoordinates.x + blockWidth
mousePosition.x - sourceGroupCoordinates.x > elementWidth / 2
? sourceGroupCoordinates.x + elementWidth
: sourceGroupCoordinates.x,
y: sourceTop,
}
const sourceType =
mousePosition.x - sourceGroupCoordinates.x > blockWidth / 2
mousePosition.x - sourceGroupCoordinates.x > elementWidth / 2
? 'right'
: 'left'
const segments = computeThreeSegments(

View File

@@ -1,10 +1,16 @@
import { blockWidth } from '../constants'
import { Coordinates } from '../types'
export const computeSourceCoordinates = (
sourcePosition: Coordinates,
type Props = {
sourcePosition: Coordinates
sourceTop: number
) => ({
x: sourcePosition.x + blockWidth,
elementWidth: number
}
export const computeSourceCoordinates = ({
sourcePosition,
sourceTop,
elementWidth,
}: Props) => ({
x: sourcePosition.x + elementWidth,
y: sourceTop,
})

View File

@@ -1,10 +1,11 @@
import { blockAnchorsOffset, blockWidth, stubLength } from '../constants'
import { groupAnchorsOffset, stubLength } from '../constants'
import { AnchorsPositionProps, Coordinates } from '../types'
import { computeSourceCoordinates } from './computeSourceCoordinates'
export type GetAnchorsPositionProps = {
sourceGroupCoordinates: Coordinates
targetGroupCoordinates: Coordinates
elementWidth: number
sourceTop: number
targetTop?: number
graphScale: number
@@ -13,53 +14,63 @@ export type GetAnchorsPositionProps = {
export const getAnchorsPosition = ({
sourceGroupCoordinates,
targetGroupCoordinates,
elementWidth,
sourceTop,
targetTop,
}: GetAnchorsPositionProps): AnchorsPositionProps => {
const sourcePosition = computeSourceCoordinates(
sourceGroupCoordinates,
sourceTop
)
const sourcePosition = computeSourceCoordinates({
sourcePosition: sourceGroupCoordinates,
elementWidth,
sourceTop,
})
let sourceType: 'right' | 'left' = 'right'
if (sourceGroupCoordinates.x > targetGroupCoordinates.x) {
sourcePosition.x = sourceGroupCoordinates.x
sourceType = 'left'
}
const { targetPosition, totalSegments } = computeGroupTargetPosition(
sourceGroupCoordinates,
targetGroupCoordinates,
sourcePosition.y,
targetTop
)
const { targetPosition, totalSegments } = computeGroupTargetPosition({
sourceGroupPosition: sourceGroupCoordinates,
targetGroupPosition: targetGroupCoordinates,
elementWidth,
sourceOffsetY: sourceTop,
targetOffsetY: targetTop,
})
return { sourcePosition, targetPosition, sourceType, totalSegments }
}
const computeGroupTargetPosition = (
sourceGroupPosition: Coordinates,
targetGroupPosition: Coordinates,
sourceOffsetY: number,
const computeGroupTargetPosition = ({
sourceGroupPosition,
targetGroupPosition,
elementWidth,
sourceOffsetY,
targetOffsetY,
}: {
sourceGroupPosition: Coordinates
targetGroupPosition: Coordinates
elementWidth: number
sourceOffsetY: number
targetOffsetY?: number
): { targetPosition: Coordinates; totalSegments: number } => {
}): { targetPosition: Coordinates; totalSegments: number } => {
const isTargetGroupBelow =
targetGroupPosition.y > sourceOffsetY &&
targetGroupPosition.x < sourceGroupPosition.x + blockWidth + stubLength &&
targetGroupPosition.x > sourceGroupPosition.x - blockWidth - stubLength
targetGroupPosition.x < sourceGroupPosition.x + elementWidth + stubLength &&
targetGroupPosition.x > sourceGroupPosition.x - elementWidth - stubLength
const isTargetGroupToTheRight = targetGroupPosition.x < sourceGroupPosition.x
const isTargettingGroup = !targetOffsetY
if (isTargetGroupBelow && isTargettingGroup) {
const isExterior =
targetGroupPosition.x <
sourceGroupPosition.x - blockWidth / 2 - stubLength ||
sourceGroupPosition.x - elementWidth / 2 - stubLength ||
targetGroupPosition.x >
sourceGroupPosition.x + blockWidth / 2 + stubLength
sourceGroupPosition.x + elementWidth / 2 + stubLength
const targetPosition = parseGroupAnchorPosition(targetGroupPosition, 'top')
return { totalSegments: isExterior ? 2 : 4, targetPosition }
} else {
const isExterior =
targetGroupPosition.x < sourceGroupPosition.x - blockWidth ||
targetGroupPosition.x > sourceGroupPosition.x + blockWidth
targetGroupPosition.x < sourceGroupPosition.x - elementWidth ||
targetGroupPosition.x > sourceGroupPosition.x + elementWidth
const targetPosition = parseGroupAnchorPosition(
targetGroupPosition,
isTargetGroupToTheRight ? 'right' : 'left',
@@ -77,18 +88,18 @@ const parseGroupAnchorPosition = (
switch (anchor) {
case 'left':
return {
x: blockPosition.x + blockAnchorsOffset.left.x,
y: targetOffsetY ?? blockPosition.y + blockAnchorsOffset.left.y,
x: blockPosition.x + groupAnchorsOffset.left.x,
y: targetOffsetY ?? blockPosition.y + groupAnchorsOffset.left.y,
}
case 'top':
return {
x: blockPosition.x + blockAnchorsOffset.top.x,
y: blockPosition.y + blockAnchorsOffset.top.y,
x: blockPosition.x + groupAnchorsOffset.top.x,
y: blockPosition.y + groupAnchorsOffset.top.y,
}
case 'right':
return {
x: blockPosition.x + blockAnchorsOffset.right.x,
y: targetOffsetY ?? blockPosition.y + blockAnchorsOffset.right.y,
x: blockPosition.x + groupAnchorsOffset.right.x,
y: targetOffsetY ?? blockPosition.y + groupAnchorsOffset.right.y,
}
}
}

View File

@@ -1,9 +1,7 @@
import {
BlockWithOptions,
InputBlockType,
IntegrationBlockType,
LogicBlockType,
} from '@typebot.io/schemas'
import { BlockWithOptions } from '@typebot.io/schemas'
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'
export const getHelpDocUrl = (blockType: BlockWithOptions['type']): string => {
switch (blockType) {
@@ -65,5 +63,7 @@ export const getHelpDocUrl = (blockType: BlockWithOptions['type']): string => {
return 'https://docs.typebot.io/editor/blocks/integrations/pixel'
case IntegrationBlockType.ZEMANTIC_AI:
return 'https://docs.typebot.io/editor/blocks/integrations/zemantic-ai'
case LogicBlockType.CONDITION:
return 'https://docs.typebot.io/editor/blocks/logic/condition'
}
}

View File

@@ -0,0 +1,59 @@
import {
ReactNode,
useState,
useEffect,
useContext,
createContext,
useCallback,
} from 'react'
import { Coordinates, CoordinatesMap } from '../types'
import { TypebotV6 } from '@typebot.io/schemas'
const eventsCoordinatesContext = createContext<{
eventsCoordinates: CoordinatesMap
updateEventCoordinates: (groupId: string, newCoord: Coordinates) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})
export const EventsCoordinatesProvider = ({
children,
events,
}: {
children: ReactNode
events: TypebotV6['events'][number][]
isReadOnly?: boolean
}) => {
const [eventsCoordinates, setEventsCoordinates] = useState<CoordinatesMap>({})
useEffect(() => {
setEventsCoordinates(
events.reduce(
(coords, group) => ({
...coords,
[group.id]: group.graphCoordinates,
}),
{}
)
)
}, [events])
const updateEventCoordinates = useCallback(
(groupId: string, newCoord: Coordinates) =>
setEventsCoordinates((eventsCoordinates) => ({
...eventsCoordinates,
[groupId]: newCoord,
})),
[]
)
return (
<eventsCoordinatesContext.Provider
value={{ eventsCoordinates, updateEventCoordinates }}
>
{children}
</eventsCoordinatesContext.Provider>
)
}
export const useEventsCoordinates = () => useContext(eventsCoordinatesContext)

View File

@@ -1,9 +1,9 @@
import { useEventListener } from '@chakra-ui/react'
import {
AbTestBlock,
DraggableBlock,
DraggableBlockType,
Item,
BlockV6,
BlockWithItems,
ItemV6,
} from '@typebot.io/schemas'
import {
createContext,
@@ -17,21 +17,31 @@ import {
useState,
} from 'react'
import { Coordinates } from '../types'
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
type NodeElement = {
id: string
element: HTMLDivElement
}
export type DraggabbleItem = Exclude<Item, AbTestBlock['items'][number]>
export type BlockWithCreatableItems = Exclude<
BlockWithItems,
{ type: LogicBlockType.AB_TEST }
>
export type DraggableItem = Exclude<ItemV6, AbTestBlock['items'][number]> & {
type: BlockWithCreatableItems['type']
}
const graphDndContext = createContext<{
draggedBlockType?: DraggableBlockType
setDraggedBlockType: Dispatch<SetStateAction<DraggableBlockType | undefined>>
draggedBlock?: DraggableBlock
setDraggedBlock: Dispatch<SetStateAction<DraggableBlock | undefined>>
draggedItem?: DraggabbleItem
setDraggedItem: Dispatch<SetStateAction<DraggabbleItem | undefined>>
draggedBlockType?: BlockV6['type']
setDraggedBlockType: Dispatch<SetStateAction<BlockV6['type'] | undefined>>
draggedBlock?: BlockV6 & { groupId: string }
setDraggedBlock: Dispatch<
SetStateAction<(BlockV6 & { groupId: string }) | undefined>
>
draggedItem?: DraggableItem
setDraggedItem: Dispatch<SetStateAction<DraggableItem | undefined>>
mouseOverGroup?: NodeElement
setMouseOverGroup: (node: NodeElement | undefined) => void
mouseOverBlock?: NodeElement
@@ -43,11 +53,13 @@ const graphDndContext = createContext<{
export type NodePosition = { absolute: Coordinates; relative: Coordinates }
export const GraphDndProvider = ({ children }: { children: ReactNode }) => {
const [draggedBlock, setDraggedBlock] = useState<DraggableBlock>()
const [draggedBlockType, setDraggedBlockType] = useState<
DraggableBlockType | undefined
const [draggedBlock, setDraggedBlock] = useState<
BlockV6 & { groupId: string }
>()
const [draggedItem, setDraggedItem] = useState<DraggabbleItem | undefined>()
const [draggedBlockType, setDraggedBlockType] = useState<
BlockV6['type'] | undefined
>()
const [draggedItem, setDraggedItem] = useState<DraggableItem | undefined>()
const [mouseOverGroup, _setMouseOverGroup] = useState<NodeElement>()
const [mouseOverBlock, _setMouseOverBlock] = useState<NodeElement>()

View File

@@ -7,10 +7,10 @@ import {
createContext,
useCallback,
} from 'react'
import { Coordinates, GroupsCoordinates } from '../types'
import { Coordinates, CoordinatesMap } from '../types'
const groupsCoordinatesContext = createContext<{
groupsCoordinates: GroupsCoordinates
groupsCoordinates: CoordinatesMap
updateGroupCoordinates: (groupId: string, newCoord: Coordinates) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
@@ -24,9 +24,7 @@ export const GroupsCoordinatesProvider = ({
groups: Group[]
isReadOnly?: boolean
}) => {
const [groupsCoordinates, setGroupsCoordinates] = useState<GroupsCoordinates>(
{}
)
const [groupsCoordinates, setGroupsCoordinates] = useState<CoordinatesMap>({})
useEffect(() => {
setGroupsCoordinates(

View File

@@ -1,4 +1,11 @@
import { Group, Block, Source, IdMap, Target } from '@typebot.io/schemas'
import {
Group,
Block,
IdMap,
Target,
BlockSource,
TEventSource,
} from '@typebot.io/schemas'
export type Coordinates = { x: number; y: number }
@@ -13,11 +20,11 @@ export type Node = Omit<Group, 'blocks'> & {
}
export type ConnectingIds = {
source: Source
source: TEventSource | (BlockSource & { groupId: string })
target?: Target
}
export type GroupsCoordinates = IdMap<Coordinates>
export type CoordinatesMap = IdMap<Coordinates>
export type AnchorsPositionProps = {
sourcePosition: Coordinates