@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user