Skip to content

Commit

Permalink
♻️ Introduce typebot v6 with events
Browse files Browse the repository at this point in the history
Closes #885
  • Loading branch information
baptisteArno committed Nov 8, 2023
1 parent 68e4fc7 commit c46d9fc
Show file tree
Hide file tree
Showing 145 changed files with 3,638 additions and 0 deletions.
60 changes: 60 additions & 0 deletions apps/builder/src/features/analytics/api/getTotalAnswers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { canReadTypebots } from '@/helpers/databaseRules'
import { totalAnswersSchema } from '@typebot.io/schemas/features/analytics'
import { parseGroups } from '@typebot.io/schemas'
import { isInputBlock } from '@typebot.io/lib'

export const getTotalAnswers = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/typebots/{typebotId}/analytics/totalAnswersInBlocks',
protect: true,
summary: 'List total answers in blocks',
tags: ['Analytics'],
},
})
.input(
z.object({
typebotId: z.string(),
})
)
.output(z.object({ totalAnswers: z.array(totalAnswersSchema) }))
.query(async ({ input: { typebotId }, ctx: { user } }) => {
const typebot = await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
select: { publishedTypebot: true },
})
if (!typebot?.publishedTypebot)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Published typebot not found',
})

const totalAnswersPerBlock = await prisma.answer.groupBy({
by: ['blockId'],
where: {
result: {
typebotId: typebot.publishedTypebot.typebotId,
},
blockId: {
in: parseGroups(typebot.publishedTypebot.groups, {
typebotVersion: typebot.publishedTypebot.version,
}).flatMap((group) =>
group.blocks.filter(isInputBlock).map((block) => block.id)
),
},
},
_count: { _all: true },
})

return {
totalAnswers: totalAnswersPerBlock.map((a) => ({
blockId: a.blockId,
total: a._count._all,
})),
}
})
55 changes: 55 additions & 0 deletions apps/builder/src/features/analytics/api/getTotalVisitedEdges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { canReadTypebots } from '@/helpers/databaseRules'
import { totalVisitedEdgesSchema } from '@typebot.io/schemas'

export const getTotalVisitedEdges = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/typebots/{typebotId}/analytics/totalVisitedEdges',
protect: true,
summary: 'List total edges used in results',
tags: ['Analytics'],
},
})
.input(
z.object({
typebotId: z.string(),
})
)
.output(
z.object({
totalVisitedEdges: z.array(totalVisitedEdgesSchema),
})
)
.query(async ({ input: { typebotId }, ctx: { user } }) => {
const typebot = await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
select: { id: true },
})
if (!typebot?.id)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Published typebot not found',
})

const edges = await prisma.visitedEdge.groupBy({
by: ['edgeId'],
where: {
result: {
typebotId: typebot.id,
},
},
_count: { resultId: true },
})

return {
totalVisitedEdges: edges.map((e) => ({
edgeId: e.edgeId,
total: e._count.resultId,
})),
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { isInputBlock, isNotDefined } from '@typebot.io/lib'
import { PublicTypebotV6 } from '@typebot.io/schemas'
import {
TotalAnswers,
TotalVisitedEdges,
} from '@typebot.io/schemas/features/analytics'

export const computeTotalUsersAtBlock = (
currentBlockId: string,
{
publishedTypebot,
totalVisitedEdges,
totalAnswers,
}: {
publishedTypebot: PublicTypebotV6
totalVisitedEdges: TotalVisitedEdges[]
totalAnswers: TotalAnswers[]
}
): number => {
let totalUsers = 0
const currentGroup = publishedTypebot.groups.find((group) =>
group.blocks.find((block) => block.id === currentBlockId)
)
if (!currentGroup) return 0
const currentBlockIndex = currentGroup.blocks.findIndex(
(block) => block.id === currentBlockId
)
const previousBlocks = currentGroup.blocks.slice(0, currentBlockIndex + 1)
for (const block of previousBlocks.reverse()) {
if (currentBlockId !== block.id && isInputBlock(block))
return totalAnswers.find((t) => t.blockId === block.id)?.total ?? 0
const incomingEdges = publishedTypebot.edges.filter(
(edge) => edge.to.blockId === block.id
)
if (!incomingEdges.length) continue
totalUsers += incomingEdges.reduce(
(acc, incomingEdge) =>
acc +
(totalVisitedEdges.find(
(totalEdge) => totalEdge.edgeId === incomingEdge.id
)?.total ?? 0),
0
)
}
const edgesConnectedToGroup = publishedTypebot.edges.filter(
(edge) =>
edge.to.groupId === currentGroup.id && isNotDefined(edge.to.blockId)
)

totalUsers += edgesConnectedToGroup.reduce(
(acc, connectedEdge) =>
acc +
(totalVisitedEdges.find(
(totalEdge) => totalEdge.edgeId === connectedEdge.id
)?.total ?? 0),
0
)

return totalUsers
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { byId } from '@typebot.io/lib'
import { PublicTypebotV6 } from '@typebot.io/schemas'
import { TotalAnswers } from '@typebot.io/schemas/features/analytics'

export const getTotalAnswersAtBlock = (
currentBlockId: string,
{
publishedTypebot,
totalAnswers,
}: {
publishedTypebot: PublicTypebotV6
totalAnswers: TotalAnswers[]
}
): number => {
const block = publishedTypebot.groups
.flatMap((g) => g.blocks)
.find(byId(currentBlockId))
if (!block) throw new Error(`Block ${currentBlockId} not found`)
return totalAnswers.find((t) => t.blockId === block.id)?.total ?? 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { produce } from 'immer'
import { TEvent } from '@typebot.io/schemas'
import { SetTypebot } from '../TypebotProvider'

export type EventsActions = {
updateEvent: (
eventIndex: number,
updates: Partial<Omit<TEvent, 'id'>>
) => void
}

const eventsActions = (setTypebot: SetTypebot): EventsActions => ({
updateEvent: (eventIndex: number, updates: Partial<Omit<TEvent, 'id'>>) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
const event = typebot.events[eventIndex]
typebot.events[eventIndex] = { ...event, ...updates }
})
),
})

export { eventsActions }
11 changes: 11 additions & 0 deletions apps/builder/src/features/events/start/StartEventNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { FlagIcon } from '@/components/icons'
import { HStack, Text } from '@chakra-ui/react'

export const StartEventNode = () => {
return (
<HStack spacing={3}>
<FlagIcon />
<Text>Start</Text>
</HStack>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import {
BoxProps,
Flex,
useColorModeValue,
useEventListener,
} from '@chakra-ui/react'
import { BlockSource } from '@typebot.io/schemas'
import React, {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useEndpoints } from '../../providers/EndpointsProvider'
import { useGraph } from '../../providers/GraphProvider'
import { useGroupsCoordinates } from '../../providers/GroupsCoordinateProvider'

const endpointHeight = 32

export const BlockSourceEndpoint = ({
source,
groupId,
isHidden,
...props
}: BoxProps & {
source: BlockSource
groupId?: string
isHidden?: boolean
}) => {
const id = source.itemId ?? source.blockId
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 { groupsCoordinates } = useGroupsCoordinates()
const ref = useRef<HTMLDivElement | null>(null)
const [groupHeight, setGroupHeight] = useState<number>()
const [groupTransformProp, setGroupTransformProp] = useState<string>()

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 group height and position changes
// eslint-disable-next-line react-hooks/exhaustive-deps
[graphPosition.scale, graphPosition.y, groupHeight, groupTransformProp]
)

useLayoutEffect(() => {
const resizeObserver = new ResizeObserver((entries) => {
setGroupHeight(entries[0].contentRect.height)
})
const groupElement = document.getElementById(`group-${groupId}`)
if (!groupElement) return
resizeObserver.observe(groupElement)
return () => {
resizeObserver.disconnect()
}
}, [groupId])

useLayoutEffect(() => {
const mutationObserver = new MutationObserver((entries) => {
setGroupTransformProp((entries[0].target as HTMLElement).style.transform)
})
const groupElement = document.getElementById(`group-${groupId}`)
if (!groupElement) return
mutationObserver.observe(groupElement, {
attributes: true,
attributeFilter: ['style'],
})
return () => {
mutationObserver.disconnect()
}
}, [groupId])

useEffect(() => {
if (!endpointY) return
setSourceEndpointYOffset?.({
id,
y: endpointY,
})
}, [setSourceEndpointYOffset, endpointY, id])

useEffect(
() => () => {
deleteSourceEndpointYOffset?.(id)
},
[deleteSourceEndpointYOffset, id]
)

useEventListener(
'pointerdown',
(e) => {
e.stopPropagation()
if (groupId) setConnectingIds({ source: { ...source, groupId } })
},
ref.current
)

useEventListener(
'mousedown',
(e) => {
e.stopPropagation()
},
ref.current
)

if (!groupsCoordinates) return <></>
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 &&
'blockId' in previewingEdge.from &&
previewingEdge.from.blockId === source.blockId &&
previewingEdge.from.itemId === source.itemId
? connectedColor
: color
}
/>
</Flex>
</Flex>
)
}
Loading

0 comments on commit c46d9fc

Please sign in to comment.