♻️ Introduce typebot v6 with events (#1013)

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

View File

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

View File

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

View File

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

View File

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