@@ -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 -
|
||||
|
||||
@@ -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} />
|
||||
))}
|
||||
|
||||
@@ -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,
|
||||
])
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
])
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export const TargetEndpoint = ({
|
||||
blockId,
|
||||
...props
|
||||
}: BoxProps & {
|
||||
groupId: string
|
||||
groupId?: string
|
||||
blockId: string
|
||||
}) => {
|
||||
const { setTargetEnpointYOffset: addTargetEndpoint } = useEndpoints()
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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>()
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user