@ -0,0 +1,137 @@
|
||||
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/schemas/helpers'
|
||||
import { defaultTimeFilter, timeFilterValues } from '../constants'
|
||||
import {
|
||||
parseFromDateFromTimeFilter,
|
||||
parseToDateFromTimeFilter,
|
||||
} from '../helpers/parseDateFromTimeFilter'
|
||||
|
||||
export const getInDepthAnalyticsData = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/v1/typebots/{typebotId}/analytics/inDepthData',
|
||||
protect: true,
|
||||
summary:
|
||||
'List total answers in blocks and off-default paths visited edges',
|
||||
tags: ['Analytics'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
typebotId: z.string(),
|
||||
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
|
||||
timeZone: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
totalAnswers: z.array(totalAnswersSchema),
|
||||
offDefaultPathVisitedEdges: z.array(
|
||||
z.object({ edgeId: z.string(), total: z.number() })
|
||||
),
|
||||
})
|
||||
)
|
||||
.query(
|
||||
async ({ input: { typebotId, timeFilter, timeZone }, 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 fromDate = parseFromDateFromTimeFilter(timeFilter, timeZone)
|
||||
const toDate = parseToDateFromTimeFilter(timeFilter, timeZone)
|
||||
|
||||
const totalAnswersPerBlock = await prisma.answer.groupBy({
|
||||
by: ['blockId', 'resultId'],
|
||||
where: {
|
||||
result: {
|
||||
typebotId: typebot.publishedTypebot.typebotId,
|
||||
createdAt: fromDate
|
||||
? {
|
||||
gte: fromDate,
|
||||
lte: toDate ?? undefined,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
blockId: {
|
||||
in: parseGroups(typebot.publishedTypebot.groups, {
|
||||
typebotVersion: typebot.publishedTypebot.version,
|
||||
}).flatMap((group) =>
|
||||
group.blocks.filter(isInputBlock).map((block) => block.id)
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const totalAnswersV2PerBlock = await prisma.answerV2.groupBy({
|
||||
by: ['blockId', 'resultId'],
|
||||
where: {
|
||||
result: {
|
||||
typebotId: typebot.publishedTypebot.typebotId,
|
||||
createdAt: fromDate
|
||||
? {
|
||||
gte: fromDate,
|
||||
lte: toDate ?? undefined,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
blockId: {
|
||||
in: parseGroups(typebot.publishedTypebot.groups, {
|
||||
typebotVersion: typebot.publishedTypebot.version,
|
||||
}).flatMap((group) =>
|
||||
group.blocks.filter(isInputBlock).map((block) => block.id)
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const uniqueCounts = totalAnswersPerBlock
|
||||
.concat(totalAnswersV2PerBlock)
|
||||
.reduce<{
|
||||
[key: string]: Set<string>
|
||||
}>((acc, { blockId, resultId }) => {
|
||||
acc[blockId] = acc[blockId] || new Set()
|
||||
acc[blockId].add(resultId)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const offDefaultPathVisitedEdges = await prisma.visitedEdge.groupBy({
|
||||
by: ['edgeId'],
|
||||
where: {
|
||||
result: {
|
||||
typebotId: typebot.publishedTypebot.typebotId,
|
||||
createdAt: fromDate
|
||||
? {
|
||||
gte: fromDate,
|
||||
lte: toDate ?? undefined,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
_count: { resultId: true },
|
||||
})
|
||||
|
||||
return {
|
||||
totalAnswers: Object.keys(uniqueCounts).map((blockId) => ({
|
||||
blockId,
|
||||
total: uniqueCounts[blockId].size,
|
||||
})),
|
||||
offDefaultPathVisitedEdges: offDefaultPathVisitedEdges.map((e) => ({
|
||||
edgeId: e.edgeId,
|
||||
total: e._count.resultId,
|
||||
})),
|
||||
}
|
||||
}
|
||||
)
|
@ -1,78 +0,0 @@
|
||||
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/schemas/helpers'
|
||||
import { defaultTimeFilter, timeFilterValues } from '../constants'
|
||||
import {
|
||||
parseFromDateFromTimeFilter,
|
||||
parseToDateFromTimeFilter,
|
||||
} from '../helpers/parseDateFromTimeFilter'
|
||||
|
||||
export const getTotalAnswers = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/v1/typebots/{typebotId}/analytics/totalAnswersInBlocks',
|
||||
protect: true,
|
||||
summary: 'List total answers in blocks',
|
||||
tags: ['Analytics'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
typebotId: z.string(),
|
||||
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
|
||||
timeZone: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.output(z.object({ totalAnswers: z.array(totalAnswersSchema) }))
|
||||
.query(
|
||||
async ({ input: { typebotId, timeFilter, timeZone }, 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 fromDate = parseFromDateFromTimeFilter(timeFilter, timeZone)
|
||||
const toDate = parseToDateFromTimeFilter(timeFilter, timeZone)
|
||||
|
||||
const totalAnswersPerBlock = await prisma.answer.groupBy({
|
||||
by: ['blockId'],
|
||||
where: {
|
||||
result: {
|
||||
typebotId: typebot.publishedTypebot.typebotId,
|
||||
createdAt: fromDate
|
||||
? {
|
||||
gte: fromDate,
|
||||
lte: toDate ?? undefined,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
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,
|
||||
})),
|
||||
}
|
||||
}
|
||||
)
|
@ -1,10 +1,8 @@
|
||||
import { router } from '@/helpers/server/trpc'
|
||||
import { getTotalAnswers } from './getTotalAnswers'
|
||||
import { getTotalVisitedEdges } from './getTotalVisitedEdges'
|
||||
import { getStats } from './getStats'
|
||||
import { getInDepthAnalyticsData } from './getInDepthAnalyticsData'
|
||||
|
||||
export const analyticsRouter = router({
|
||||
getTotalAnswers,
|
||||
getTotalVisitedEdges,
|
||||
getInDepthAnalyticsData,
|
||||
getStats,
|
||||
})
|
||||
|
@ -5,8 +5,14 @@ import {
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { Stats } from '@typebot.io/schemas'
|
||||
import React from 'react'
|
||||
import {
|
||||
Edge,
|
||||
GroupV6,
|
||||
Stats,
|
||||
TotalAnswers,
|
||||
TotalVisitedEdges,
|
||||
} from '@typebot.io/schemas'
|
||||
import React, { useMemo } from 'react'
|
||||
import { StatsCards } from './StatsCards'
|
||||
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
|
||||
import { Graph } from '@/features/graph/components/Graph'
|
||||
@ -16,6 +22,7 @@ import { trpc } from '@/lib/trpc'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { EventsCoordinatesProvider } from '@/features/graph/providers/EventsCoordinateProvider'
|
||||
import { timeFilterValues } from '../constants'
|
||||
import { blockHasItems, isInputBlock } from '@typebot.io/schemas/helpers'
|
||||
|
||||
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
|
||||
@ -33,7 +40,7 @@ export const AnalyticsGraphContainer = ({
|
||||
const { t } = useTranslate()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const { typebot, publishedTypebot } = useTypebot()
|
||||
const { data } = trpc.analytics.getTotalAnswers.useQuery(
|
||||
const { data } = trpc.analytics.getInDepthAnalyticsData.useQuery(
|
||||
{
|
||||
typebotId: typebot?.id as string,
|
||||
timeFilter,
|
||||
@ -42,14 +49,36 @@ export const AnalyticsGraphContainer = ({
|
||||
{ enabled: isDefined(publishedTypebot) }
|
||||
)
|
||||
|
||||
const { data: edgesData } = trpc.analytics.getTotalVisitedEdges.useQuery(
|
||||
{
|
||||
typebotId: typebot?.id as string,
|
||||
timeFilter,
|
||||
timeZone,
|
||||
},
|
||||
{ enabled: isDefined(publishedTypebot) }
|
||||
)
|
||||
const totalVisitedEdges = useMemo(() => {
|
||||
if (
|
||||
!publishedTypebot?.edges ||
|
||||
!publishedTypebot.groups ||
|
||||
!publishedTypebot.events ||
|
||||
!data?.totalAnswers ||
|
||||
!stats?.totalViews
|
||||
)
|
||||
return
|
||||
const firstEdgeId = publishedTypebot.events[0].outgoingEdgeId
|
||||
if (!firstEdgeId) return
|
||||
return populateEdgesWithVisitData({
|
||||
edgeId: firstEdgeId,
|
||||
edges: publishedTypebot.edges,
|
||||
groups: publishedTypebot.groups,
|
||||
currentTotalUsers: stats.totalViews,
|
||||
totalVisitedEdges: data.offDefaultPathVisitedEdges
|
||||
? [...data.offDefaultPathVisitedEdges]
|
||||
: [],
|
||||
totalAnswers: data.totalAnswers,
|
||||
edgeVisitHistory: [],
|
||||
})
|
||||
}, [
|
||||
data?.offDefaultPathVisitedEdges,
|
||||
data?.totalAnswers,
|
||||
publishedTypebot?.edges,
|
||||
publishedTypebot?.groups,
|
||||
publishedTypebot?.events,
|
||||
stats?.totalViews,
|
||||
])
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@ -73,7 +102,7 @@ export const AnalyticsGraphContainer = ({
|
||||
typebot={publishedTypebot}
|
||||
onUnlockProPlanClick={onOpen}
|
||||
totalAnswers={data?.totalAnswers}
|
||||
totalVisitedEdges={edgesData?.totalVisitedEdges}
|
||||
totalVisitedEdges={totalVisitedEdges}
|
||||
/>
|
||||
</EventsCoordinatesProvider>
|
||||
</GraphProvider>
|
||||
@ -102,3 +131,72 @@ export const AnalyticsGraphContainer = ({
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
const populateEdgesWithVisitData = ({
|
||||
edgeId,
|
||||
edges,
|
||||
groups,
|
||||
currentTotalUsers,
|
||||
totalVisitedEdges,
|
||||
totalAnswers,
|
||||
edgeVisitHistory,
|
||||
}: {
|
||||
edgeId: string
|
||||
edges: Edge[]
|
||||
groups: GroupV6[]
|
||||
currentTotalUsers: number
|
||||
totalVisitedEdges: TotalVisitedEdges[]
|
||||
totalAnswers: TotalAnswers[]
|
||||
edgeVisitHistory: string[]
|
||||
}): TotalVisitedEdges[] => {
|
||||
if (edgeVisitHistory.find((e) => e === edgeId)) return totalVisitedEdges
|
||||
totalVisitedEdges.push({
|
||||
edgeId,
|
||||
total: currentTotalUsers,
|
||||
})
|
||||
edgeVisitHistory.push(edgeId)
|
||||
const edge = edges.find((edge) => edge.id === edgeId)
|
||||
if (!edge) return totalVisitedEdges
|
||||
const group = groups.find((group) => edge?.to.groupId === group.id)
|
||||
if (!group) return totalVisitedEdges
|
||||
for (const block of edge.to.blockId
|
||||
? group.blocks.slice(
|
||||
group.blocks.findIndex((b) => b.id === edge.to.blockId)
|
||||
)
|
||||
: group.blocks) {
|
||||
if (blockHasItems(block)) {
|
||||
for (const item of block.items) {
|
||||
if (item.outgoingEdgeId) {
|
||||
totalVisitedEdges = populateEdgesWithVisitData({
|
||||
edgeId: item.outgoingEdgeId,
|
||||
edges,
|
||||
groups,
|
||||
currentTotalUsers:
|
||||
totalVisitedEdges.find(
|
||||
(tve) => tve.edgeId === item.outgoingEdgeId
|
||||
)?.total ?? 0,
|
||||
totalVisitedEdges,
|
||||
totalAnswers,
|
||||
edgeVisitHistory,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (block.outgoingEdgeId) {
|
||||
const totalUsers = isInputBlock(block)
|
||||
? totalAnswers.find((a) => a.blockId === block.id)?.total
|
||||
: currentTotalUsers
|
||||
totalVisitedEdges = populateEdgesWithVisitData({
|
||||
edgeId: block.outgoingEdgeId,
|
||||
edges,
|
||||
groups,
|
||||
currentTotalUsers: totalUsers ?? 0,
|
||||
totalVisitedEdges,
|
||||
totalAnswers,
|
||||
edgeVisitHistory,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return totalVisitedEdges
|
||||
}
|
||||
|
@ -71,6 +71,7 @@ const Expression = ({
|
||||
case 'Result ID':
|
||||
case 'Moment of the day':
|
||||
case 'Environment name':
|
||||
case 'Transcript':
|
||||
case 'Yesterday': {
|
||||
return (
|
||||
<Text as="span">
|
||||
|
@ -8,11 +8,13 @@ import { WhatsAppLogo } from '@/components/logos/WhatsAppLogo'
|
||||
import {
|
||||
defaultSetVariableOptions,
|
||||
hiddenTypes,
|
||||
sessionOnlySetVariableOptions,
|
||||
valueTypes,
|
||||
} from '@typebot.io/schemas/features/blocks/logic/setVariable/constants'
|
||||
import { TextInput } from '@/components/inputs'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { isInputBlock } from '@typebot.io/schemas/helpers'
|
||||
|
||||
type Props = {
|
||||
options: SetVariableBlock['options']
|
||||
@ -48,6 +50,20 @@ export const SetVariableSettings = ({ options, onOptionsChange }: Props) => {
|
||||
})
|
||||
}
|
||||
|
||||
const isSessionOnly =
|
||||
options?.type &&
|
||||
sessionOnlySetVariableOptions.includes(
|
||||
options.type as (typeof sessionOnlySetVariableOptions)[number]
|
||||
)
|
||||
|
||||
const isLinkedToAnswer =
|
||||
options?.variableId &&
|
||||
typebot?.groups.some((g) =>
|
||||
g.blocks.some(
|
||||
(b) => isInputBlock(b) && b.options?.variableId === options.variableId
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
@ -80,7 +96,7 @@ export const SetVariableSettings = ({ options, onOptionsChange }: Props) => {
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{selectedVariable && (
|
||||
{selectedVariable && !isSessionOnly && !isLinkedToAnswer && (
|
||||
<SwitchWithLabel
|
||||
key={selectedVariable.id}
|
||||
label="Save in results?"
|
||||
@ -244,6 +260,7 @@ const SetVariableValue = ({
|
||||
case 'Today':
|
||||
case 'Result ID':
|
||||
case 'Empty':
|
||||
case 'Transcript':
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,11 @@ import { importTypebotInDatabase } from '@typebot.io/playwright/databaseActions'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
|
||||
const typebotId = createId()
|
||||
test.describe.configure({ mode: 'parallel' })
|
||||
|
||||
test.describe('Set variable block', () => {
|
||||
test('its configuration should work', async ({ page }) => {
|
||||
const typebotId = createId()
|
||||
await importTypebotInDatabase(
|
||||
getTestAsset('typebots/logic/setVariable.json'),
|
||||
{
|
||||
@ -70,4 +71,47 @@ test.describe('Set variable block', () => {
|
||||
page.locator('typebot-standard').locator('text=Addition: 366000')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('Transcription variable setting should work in preview', async ({
|
||||
page,
|
||||
}) => {
|
||||
const typebotId = createId()
|
||||
await importTypebotInDatabase(
|
||||
getTestAsset('typebots/logic/setVariable2.json'),
|
||||
{
|
||||
id: typebotId,
|
||||
}
|
||||
)
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.getByText('Transcription =').click()
|
||||
await expect(page.getByText('Save in results?')).toBeVisible()
|
||||
await page.locator('input[type="text"]').click()
|
||||
await page.getByRole('menuitem', { name: 'Transcript' }).click()
|
||||
await expect(page.getByText('Save in results?')).toBeHidden()
|
||||
await expect(page.getByText('System.Transcript')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Test' }).click()
|
||||
await page.getByRole('button', { name: 'There is a bug 🐛' }).click()
|
||||
await page.getByTestId('textarea').fill('Hello!!')
|
||||
await page.getByTestId('input').getByRole('button').click()
|
||||
await page
|
||||
.locator('typebot-standard')
|
||||
.getByRole('button', { name: 'Restart' })
|
||||
.click()
|
||||
await page.getByRole('button', { name: 'I have a question 💭' }).click()
|
||||
await page.getByTestId('textarea').fill('How are you?')
|
||||
await page.getByTestId('input').getByRole('button').click()
|
||||
await page.getByRole('button', { name: 'Transcription' }).click()
|
||||
|
||||
await expect(
|
||||
page.getByText('Assistant: "Hey friend 👋 How').first()
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
page.getByText(
|
||||
'Assistant: "https://media0.giphy.com/media/rhgwg4qBu97ISgbfni/giphy-downsized.gif?cid=fe3852a3wimy48e55djt23j44uto7gdlu8ksytylafisvr0q&rid=giphy-downsized.gif&ct=g"'
|
||||
)
|
||||
).toBeVisible()
|
||||
await expect(page.getByText('User: "How are you?"')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
@ -43,7 +43,7 @@ test('should be configurable', async ({ page }) => {
|
||||
await page.click('[aria-label="Close"]')
|
||||
await page.click('text=Jump to Group #2 in My link typebot 2')
|
||||
await page.getByTestId('selected-item-label').nth(1).click({ force: true })
|
||||
await page.click('button >> text=Start')
|
||||
await page.getByLabel('Clear').click()
|
||||
|
||||
await page.click('text=Test')
|
||||
await page.locator('typebot-standard').locator('input').fill('Hello there!')
|
||||
@ -53,7 +53,7 @@ test('should be configurable', async ({ page }) => {
|
||||
).toBeVisible()
|
||||
|
||||
await page.click('[aria-label="Close"]')
|
||||
await page.click('text=Jump to Start in My link typebot 2')
|
||||
await page.click('text=Jump in My link typebot 2')
|
||||
await page.waitForTimeout(1000)
|
||||
await page.getByTestId('selected-item-label').first().click({ force: true })
|
||||
await page.click('button >> text=Current typebot')
|
||||
|
@ -61,58 +61,6 @@ test('Edges connection should work', async ({ page }) => {
|
||||
const total = await page.locator('[data-testid="edge"]').count()
|
||||
expect(total).toBe(1)
|
||||
})
|
||||
test('Drag and drop blocks and items should work', async ({ page }) => {
|
||||
const typebotId = createId()
|
||||
await importTypebotInDatabase(
|
||||
getTestAsset('typebots/editor/buttonsDnd.json'),
|
||||
{
|
||||
id: typebotId,
|
||||
}
|
||||
)
|
||||
|
||||
// Blocks dnd
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await expect(page.locator('[data-testid="block"] >> nth=0')).toHaveText(
|
||||
'Hello!'
|
||||
)
|
||||
await page.dragAndDrop('text=Hello', '[data-testid="block"] >> nth=2', {
|
||||
targetPosition: { x: 100, y: 0 },
|
||||
})
|
||||
await expect(page.locator('[data-testid="block"] >> nth=1')).toHaveText(
|
||||
'Hello!'
|
||||
)
|
||||
await page.dragAndDrop('text=Hello', 'text=Group #2')
|
||||
await expect(page.locator('[data-testid="block"] >> nth=2')).toHaveText(
|
||||
'Hello!'
|
||||
)
|
||||
|
||||
// Items dnd
|
||||
await expect(page.locator('[data-testid="item"] >> nth=0')).toHaveText(
|
||||
'Item 1'
|
||||
)
|
||||
await page.dragAndDrop('text=Item 1', 'text=Item 3')
|
||||
await expect(page.locator('[data-testid="item"] >> nth=2')).toHaveText(
|
||||
'Item 1'
|
||||
)
|
||||
await expect(page.locator('[data-testid="item"] >> nth=1')).toHaveText(
|
||||
'Item 3'
|
||||
)
|
||||
await page.dragAndDrop('text=Item 3', 'text=Item 2-3')
|
||||
await expect(page.locator('[data-testid="item"] >> nth=7')).toHaveText(
|
||||
'Item 3'
|
||||
)
|
||||
|
||||
await expect(page.locator('[data-testid="item"] >> nth=2')).toHaveText(
|
||||
'Name=John'
|
||||
)
|
||||
await page.dragAndDrop(
|
||||
'[data-testid="item"] >> nth=2',
|
||||
'[data-testid="item"] >> nth=3'
|
||||
)
|
||||
await expect(page.locator('[data-testid="item"] >> nth=3')).toHaveText(
|
||||
'Name=John'
|
||||
)
|
||||
})
|
||||
|
||||
test('Rename and icon change should work', async ({ page }) => {
|
||||
const typebotId = createId()
|
||||
|
@ -110,10 +110,14 @@ export const useUndo = <T extends { updatedAt: Date }>(
|
||||
|
||||
const setUpdateDate = useCallback(
|
||||
(updatedAt: Date) => {
|
||||
set((current) => ({
|
||||
...current,
|
||||
updatedAt,
|
||||
}))
|
||||
set((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
updatedAt,
|
||||
}
|
||||
: current
|
||||
)
|
||||
},
|
||||
[set]
|
||||
)
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
TotalVisitedEdges,
|
||||
} from '@typebot.io/schemas/features/analytics'
|
||||
import { computeTotalUsersAtBlock } from '@/features/analytics/helpers/computeTotalUsersAtBlock'
|
||||
import { byId } from '@typebot.io/lib'
|
||||
import { byId, isNotDefined } from '@typebot.io/lib'
|
||||
import { blockHasItems } from '@typebot.io/schemas/helpers'
|
||||
import { groupWidth } from '../../constants'
|
||||
import { getTotalAnswersAtBlock } from '@/features/analytics/helpers/getTotalAnswersAtBlock'
|
||||
@ -130,7 +130,7 @@ export const DropOffEdge = ({
|
||||
return lastBlock?.id === currentBlockId
|
||||
}, [publishedTypebot, currentBlockId])
|
||||
|
||||
if (!endpointCoordinates) return null
|
||||
if (!endpointCoordinates || isNotDefined(dropOffRate)) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -22,7 +22,7 @@ import { FormEvent, useState } from 'react'
|
||||
import { headerHeight } from '../../editor/constants'
|
||||
import { useDrag } from '@use-gesture/react'
|
||||
import { ResizeHandle } from './ResizeHandle'
|
||||
import { Variable } from '@typebot.io/schemas'
|
||||
import { InputBlock, SetVariableBlock, Variable } from '@typebot.io/schemas'
|
||||
import {
|
||||
CheckIcon,
|
||||
MoreHorizontalIcon,
|
||||
@ -32,6 +32,9 @@ import {
|
||||
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
||||
import { isNotEmpty } from '@typebot.io/lib'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { isInputBlock } from '@typebot.io/schemas/helpers'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
import { sessionOnlySetVariableOptions } from '@typebot.io/schemas/features/blocks/logic/setVariable/constants'
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
@ -70,6 +73,14 @@ export const VariablesDrawer = ({ onClose }: Props) => {
|
||||
})
|
||||
}
|
||||
|
||||
const setVariableAndInputBlocks =
|
||||
typebot?.groups.flatMap(
|
||||
(g) =>
|
||||
g.blocks.filter(
|
||||
(b) => b.type === LogicBlockType.SET_VARIABLE || isInputBlock(b)
|
||||
) as (InputBlock | SetVariableBlock)[]
|
||||
) ?? []
|
||||
|
||||
return (
|
||||
<Flex
|
||||
pos="absolute"
|
||||
@ -132,6 +143,7 @@ export const VariablesDrawer = ({ onClose }: Props) => {
|
||||
variable={variable}
|
||||
onChange={(changes) => updateVariable(variable.id, changes)}
|
||||
onDelete={() => deleteVariable(variable.id)}
|
||||
setVariableAndInputBlocks={setVariableAndInputBlocks}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
@ -144,58 +156,76 @@ const VariableItem = ({
|
||||
variable,
|
||||
onChange,
|
||||
onDelete,
|
||||
setVariableAndInputBlocks,
|
||||
}: {
|
||||
variable: Variable
|
||||
onChange: (variable: Partial<Variable>) => void
|
||||
onDelete: () => void
|
||||
}) => (
|
||||
<HStack justifyContent="space-between">
|
||||
<Editable
|
||||
defaultValue={variable.name}
|
||||
onSubmit={(name) => onChange({ name })}
|
||||
>
|
||||
<EditablePreview
|
||||
px="2"
|
||||
noOfLines={1}
|
||||
cursor="text"
|
||||
_hover={{
|
||||
bg: useColorModeValue('gray.100', 'gray.700'),
|
||||
}}
|
||||
/>
|
||||
<EditableInput ml="1" pl="1" />
|
||||
</Editable>
|
||||
setVariableAndInputBlocks: (InputBlock | SetVariableBlock)[]
|
||||
}) => {
|
||||
const isSessionOnly = setVariableAndInputBlocks.some(
|
||||
(b) =>
|
||||
b.type === LogicBlockType.SET_VARIABLE &&
|
||||
sessionOnlySetVariableOptions.includes(
|
||||
b.options?.type as (typeof sessionOnlySetVariableOptions)[number]
|
||||
)
|
||||
)
|
||||
|
||||
<HStack>
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
icon={<MoreHorizontalIcon />}
|
||||
aria-label={'Settings'}
|
||||
size="sm"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverBody>
|
||||
<SwitchWithLabel
|
||||
label="Save in results?"
|
||||
moreInfoContent="Check this option if you want to save the variable value in the typebot Results table."
|
||||
initialValue={!variable.isSessionVariable}
|
||||
onCheckChange={() =>
|
||||
onChange({
|
||||
...variable,
|
||||
isSessionVariable: !variable.isSessionVariable,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
const isLinkedToAnswer = setVariableAndInputBlocks.some(
|
||||
(b) => isInputBlock(b) && b.options?.variableId === variable.id
|
||||
)
|
||||
|
||||
return (
|
||||
<HStack justifyContent="space-between">
|
||||
<Editable
|
||||
defaultValue={variable.name}
|
||||
onSubmit={(name) => onChange({ name })}
|
||||
>
|
||||
<EditablePreview
|
||||
px="2"
|
||||
noOfLines={1}
|
||||
cursor="text"
|
||||
_hover={{
|
||||
bg: useColorModeValue('gray.100', 'gray.700'),
|
||||
}}
|
||||
/>
|
||||
<EditableInput ml="1" pl="1" />
|
||||
</Editable>
|
||||
|
||||
<HStack>
|
||||
{!isSessionOnly && !isLinkedToAnswer && (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
icon={<MoreHorizontalIcon />}
|
||||
aria-label={'Settings'}
|
||||
size="sm"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverBody>
|
||||
<SwitchWithLabel
|
||||
label="Save in results?"
|
||||
moreInfoContent="Check this option if you want to save the variable value in the typebot Results table."
|
||||
initialValue={!variable.isSessionVariable}
|
||||
onCheckChange={() =>
|
||||
onChange({
|
||||
...variable,
|
||||
isSessionVariable: !variable.isSessionVariable,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
onClick={onDelete}
|
||||
aria-label="Delete"
|
||||
size="sm"
|
||||
/>
|
||||
</Popover>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</HStack>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { WebhookIcon } from '@/components/icons'
|
||||
import { useUser } from '@/features/account/hooks/useUser'
|
||||
import { useEditor } from '@/features/editor/providers/EditorProvider'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { useGraph } from '@/features/graph/providers/GraphProvider'
|
||||
@ -7,6 +8,7 @@ import { Standard } from '@typebot.io/nextjs'
|
||||
import { ContinueChatResponse } from '@typebot.io/schemas'
|
||||
|
||||
export const WebPreview = () => {
|
||||
const { user } = useUser()
|
||||
const { typebot } = useTypebot()
|
||||
const { startPreviewAtGroup, startPreviewAtEvent } = useEditor()
|
||||
const { setPreviewingBlock } = useGraph()
|
||||
@ -40,6 +42,7 @@ export const WebPreview = () => {
|
||||
<Standard
|
||||
key={`web-preview${startPreviewAtGroup ?? ''}`}
|
||||
typebot={typebot}
|
||||
sessionId={user ? `${typebot.id}-${user.id}` : undefined}
|
||||
startFrom={
|
||||
startPreviewAtGroup
|
||||
? { type: 'group', groupId: startPreviewAtGroup }
|
||||
|
@ -15,6 +15,7 @@ import { parseResultHeader } from '@typebot.io/results/parseResultHeader'
|
||||
import { convertResultsToTableData } from '@typebot.io/results/convertResultsToTableData'
|
||||
import { parseCellContent } from './helpers/parseCellContent'
|
||||
import { timeFilterValues } from '../analytics/constants'
|
||||
import { parseBlockIdVariableIdMap } from '@typebot.io/results/parseBlockIdVariableIdMap'
|
||||
|
||||
const resultsContext = createContext<{
|
||||
resultsList: { results: ResultWithAnswers[] }[] | undefined
|
||||
@ -97,11 +98,14 @@ export const ResultsProvider = ({
|
||||
const tableData = useMemo(
|
||||
() =>
|
||||
publishedTypebot
|
||||
? convertResultsToTableData(
|
||||
data?.flatMap((d) => d.results) ?? [],
|
||||
resultHeader,
|
||||
parseCellContent
|
||||
)
|
||||
? convertResultsToTableData({
|
||||
results: data?.flatMap((d) => d.results) ?? [],
|
||||
headerCells: resultHeader,
|
||||
cellParser: parseCellContent,
|
||||
blockIdVariableIdMap: parseBlockIdVariableIdMap(
|
||||
publishedTypebot.groups
|
||||
),
|
||||
})
|
||||
: [],
|
||||
[publishedTypebot, data, resultHeader]
|
||||
)
|
||||
|
@ -71,11 +71,31 @@ export const getResult = authenticatedProcedure
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
include: { answers: true },
|
||||
include: {
|
||||
answers: {
|
||||
select: {
|
||||
blockId: true,
|
||||
content: true,
|
||||
},
|
||||
},
|
||||
answersV2: {
|
||||
select: {
|
||||
blockId: true,
|
||||
content: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (results.length === 0)
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Result not found' })
|
||||
|
||||
return { result: resultWithAnswersSchema.parse(results[0]) }
|
||||
const { answers, answersV2, ...result } = results[0]
|
||||
|
||||
return {
|
||||
result: resultWithAnswersSchema.parse({
|
||||
...result,
|
||||
answers: answers.concat(answersV2),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
@ -104,7 +104,20 @@ export const getResults = authenticatedProcedure
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
include: { answers: true },
|
||||
include: {
|
||||
answers: {
|
||||
select: {
|
||||
blockId: true,
|
||||
content: true,
|
||||
},
|
||||
},
|
||||
answersV2: {
|
||||
select: {
|
||||
blockId: true,
|
||||
content: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
let nextCursor: typeof cursor | undefined
|
||||
@ -114,7 +127,11 @@ export const getResults = authenticatedProcedure
|
||||
}
|
||||
|
||||
return {
|
||||
results: z.array(resultWithAnswersSchema).parse(results),
|
||||
results: z
|
||||
.array(resultWithAnswersSchema)
|
||||
.parse(
|
||||
results.map((r) => ({ ...r, answers: r.answersV2.concat(r.answers) }))
|
||||
),
|
||||
nextCursor,
|
||||
}
|
||||
})
|
||||
|
@ -27,6 +27,7 @@ import { parseUniqueKey } from '@typebot.io/lib/parseUniqueKey'
|
||||
import { useResults } from '../../ResultsProvider'
|
||||
import { byId, isDefined } from '@typebot.io/lib'
|
||||
import { Typebot } from '@typebot.io/schemas'
|
||||
import { parseBlockIdVariableIdMap } from '@typebot.io/results/parseBlockIdVariableIdMap'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
@ -101,7 +102,11 @@ export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => {
|
||||
)
|
||||
: existingResultHeader
|
||||
|
||||
const dataToUnparse = convertResultsToTableData(results, resultHeader)
|
||||
const dataToUnparse = convertResultsToTableData({
|
||||
results,
|
||||
headerCells: resultHeader,
|
||||
blockIdVariableIdMap: parseBlockIdVariableIdMap(typebot?.groups),
|
||||
})
|
||||
|
||||
const headerIds = parseColumnsOrder(
|
||||
typebot?.resultsTablePreferences?.columnsOrder,
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
isPublicIdNotAvailable,
|
||||
sanitizeGroups,
|
||||
sanitizeSettings,
|
||||
sanitizeVariables,
|
||||
} from '../helpers/sanitizers'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { EventType } from '@typebot.io/schemas/features/events/constants'
|
||||
@ -92,6 +93,9 @@ export const createTypebot = authenticatedProcedure
|
||||
if (!existingFolder) typebot.folderId = null
|
||||
}
|
||||
|
||||
const groups = (
|
||||
typebot.groups ? await sanitizeGroups(workspaceId)(typebot.groups) : []
|
||||
) as TypebotV6['groups']
|
||||
const newTypebot = await prisma.typebot.create({
|
||||
data: {
|
||||
version: '6',
|
||||
@ -99,9 +103,7 @@ export const createTypebot = authenticatedProcedure
|
||||
name: typebot.name ?? 'My typebot',
|
||||
icon: typebot.icon,
|
||||
selectedThemeTemplateId: typebot.selectedThemeTemplateId,
|
||||
groups: (typebot.groups
|
||||
? await sanitizeGroups(workspaceId)(typebot.groups)
|
||||
: []) as TypebotV6['groups'],
|
||||
groups,
|
||||
events: typebot.events ?? [
|
||||
{
|
||||
type: EventType.START,
|
||||
@ -118,7 +120,9 @@ export const createTypebot = authenticatedProcedure
|
||||
}
|
||||
: {},
|
||||
folderId: typebot.folderId,
|
||||
variables: typebot.variables ?? [],
|
||||
variables: typebot.variables
|
||||
? sanitizeVariables({ variables: typebot.variables, groups })
|
||||
: [],
|
||||
edges: typebot.edges ?? [],
|
||||
resultsTablePreferences: typebot.resultsTablePreferences ?? undefined,
|
||||
publicId: typebot.publicId ?? undefined,
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
sanitizeFolderId,
|
||||
sanitizeGroups,
|
||||
sanitizeSettings,
|
||||
sanitizeVariables,
|
||||
} from '../helpers/sanitizers'
|
||||
import { preprocessTypebot } from '@typebot.io/schemas/features/typebot/helpers/preprocessTypebot'
|
||||
import { migrateTypebot } from '@typebot.io/migrations/migrateTypebot'
|
||||
@ -122,6 +123,12 @@ export const importTypebot = authenticatedProcedure
|
||||
|
||||
const migratedTypebot = await migrateImportingTypebot(typebot)
|
||||
|
||||
const groups = (
|
||||
migratedTypebot.groups
|
||||
? await sanitizeGroups(workspaceId)(migratedTypebot.groups)
|
||||
: []
|
||||
) as TypebotV6['groups']
|
||||
|
||||
const newTypebot = await prisma.typebot.create({
|
||||
data: {
|
||||
version: '6',
|
||||
@ -129,9 +136,7 @@ export const importTypebot = authenticatedProcedure
|
||||
name: migratedTypebot.name,
|
||||
icon: migratedTypebot.icon,
|
||||
selectedThemeTemplateId: migratedTypebot.selectedThemeTemplateId,
|
||||
groups: (migratedTypebot.groups
|
||||
? await sanitizeGroups(workspaceId)(migratedTypebot.groups)
|
||||
: []) as TypebotV6['groups'],
|
||||
groups,
|
||||
events: migratedTypebot.events ?? undefined,
|
||||
theme: migratedTypebot.theme ? migratedTypebot.theme : {},
|
||||
settings: migratedTypebot.settings
|
||||
@ -147,7 +152,9 @@ export const importTypebot = authenticatedProcedure
|
||||
folderId: migratedTypebot.folderId,
|
||||
workspaceId: workspace.id,
|
||||
}),
|
||||
variables: migratedTypebot.variables ?? [],
|
||||
variables: migratedTypebot.variables
|
||||
? sanitizeVariables({ variables: migratedTypebot.variables, groups })
|
||||
: [],
|
||||
edges: migratedTypebot.edges ?? [],
|
||||
resultsTablePreferences:
|
||||
migratedTypebot.resultsTablePreferences ?? undefined,
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
sanitizeCustomDomain,
|
||||
sanitizeGroups,
|
||||
sanitizeSettings,
|
||||
sanitizeVariables,
|
||||
} from '../helpers/sanitizers'
|
||||
import { isWriteTypebotForbidden } from '../helpers/isWriteTypebotForbidden'
|
||||
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
|
||||
@ -156,6 +157,10 @@ export const updateTypebot = authenticatedProcedure
|
||||
})
|
||||
}
|
||||
|
||||
const groups = typebot.groups
|
||||
? await sanitizeGroups(existingTypebot.workspace.id)(typebot.groups)
|
||||
: undefined
|
||||
|
||||
const newTypebot = await prisma.typebot.update({
|
||||
where: {
|
||||
id: existingTypebot.id,
|
||||
@ -166,9 +171,7 @@ export const updateTypebot = authenticatedProcedure
|
||||
icon: typebot.icon,
|
||||
selectedThemeTemplateId: typebot.selectedThemeTemplateId,
|
||||
events: typebot.events ?? undefined,
|
||||
groups: typebot.groups
|
||||
? await sanitizeGroups(existingTypebot.workspace.id)(typebot.groups)
|
||||
: undefined,
|
||||
groups,
|
||||
theme: typebot.theme ? typebot.theme : undefined,
|
||||
settings: typebot.settings
|
||||
? sanitizeSettings(
|
||||
@ -178,7 +181,13 @@ export const updateTypebot = authenticatedProcedure
|
||||
)
|
||||
: undefined,
|
||||
folderId: typebot.folderId,
|
||||
variables: typebot.variables,
|
||||
variables:
|
||||
typebot.variables && groups
|
||||
? sanitizeVariables({
|
||||
variables: typebot.variables,
|
||||
groups,
|
||||
})
|
||||
: undefined,
|
||||
edges: typebot.edges,
|
||||
resultsTablePreferences:
|
||||
typebot.resultsTablePreferences === null
|
||||
|
@ -4,6 +4,9 @@ import { Plan } from '@typebot.io/prisma'
|
||||
import { Block, Typebot } from '@typebot.io/schemas'
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
import { defaultSendEmailOptions } from '@typebot.io/schemas/features/blocks/integrations/sendEmail/constants'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
import { sessionOnlySetVariableOptions } from '@typebot.io/schemas/features/blocks/logic/setVariable/constants'
|
||||
import { isInputBlock } from '@typebot.io/schemas/helpers'
|
||||
|
||||
export const sanitizeSettings = (
|
||||
settings: Typebot['settings'],
|
||||
@ -160,3 +163,38 @@ export const sanitizeCustomDomain = async ({
|
||||
})
|
||||
return domainCount === 0 ? null : customDomain
|
||||
}
|
||||
|
||||
export const sanitizeVariables = ({
|
||||
variables,
|
||||
groups,
|
||||
}: Pick<Typebot, 'variables' | 'groups'>): Typebot['variables'] => {
|
||||
const blocks = groups
|
||||
.flatMap((group) => group.blocks as Block[])
|
||||
.filter((b) => isInputBlock(b) || b.type === LogicBlockType.SET_VARIABLE)
|
||||
return variables.map((variable) => {
|
||||
if (variable.isSessionVariable) return variable
|
||||
const isVariableLinkedToInputBlock = blocks.some(
|
||||
(block) =>
|
||||
isInputBlock(block) && block.options?.variableId === variable.id
|
||||
)
|
||||
if (isVariableLinkedToInputBlock)
|
||||
return {
|
||||
...variable,
|
||||
isSessionVariable: true,
|
||||
}
|
||||
const isVariableSetToForbiddenResultVar = blocks.some(
|
||||
(block) =>
|
||||
block.type === LogicBlockType.SET_VARIABLE &&
|
||||
block.options?.variableId === variable.id &&
|
||||
sessionOnlySetVariableOptions.includes(
|
||||
block.options.type as (typeof sessionOnlySetVariableOptions)[number]
|
||||
)
|
||||
)
|
||||
if (isVariableSetToForbiddenResultVar)
|
||||
return {
|
||||
...variable,
|
||||
isSessionVariable: true,
|
||||
}
|
||||
return variable
|
||||
})
|
||||
}
|
||||
|
@ -105,6 +105,7 @@ export const startWhatsAppPreview = authenticatedProcedure
|
||||
clientSideActions,
|
||||
logs,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
} = await startSession({
|
||||
version: 2,
|
||||
message: undefined,
|
||||
@ -145,6 +146,7 @@ export const startWhatsAppPreview = authenticatedProcedure
|
||||
state: newSessionState,
|
||||
},
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
})
|
||||
} else {
|
||||
await restartSession({
|
||||
|
@ -1,101 +1,99 @@
|
||||
{
|
||||
"id": "cl0iecee90042961arm5kb0f0",
|
||||
"createdAt": "2022-03-08T17:18:50.337Z",
|
||||
"updatedAt": "2022-03-08T21:05:28.825Z",
|
||||
"name": "Another typebot",
|
||||
"folderId": null,
|
||||
"groups": [
|
||||
"version": "6",
|
||||
"id": "qk6zz1ag2jnm3yny7fzqrbwn",
|
||||
"name": "My link typebot 2",
|
||||
"events": [
|
||||
{
|
||||
"id": "p4ByLVoKiDRyRoPHKmcTfw",
|
||||
"blocks": [
|
||||
{
|
||||
"id": "rw6smEWEJzHKbiVKLUKFvZ",
|
||||
"type": "start",
|
||||
"label": "Start",
|
||||
"groupId": "p4ByLVoKiDRyRoPHKmcTfw",
|
||||
"outgoingEdgeId": "1z3pfiatTUHbraD2uSoA3E"
|
||||
}
|
||||
],
|
||||
"title": "Start",
|
||||
"graphCoordinates": { "x": 0, "y": 0 }
|
||||
},
|
||||
"outgoingEdgeId": "1z3pfiatTUHbraD2uSoA3E",
|
||||
"graphCoordinates": { "x": 0, "y": 0 },
|
||||
"type": "start"
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": "bg4QEJseUsTP496H27j5k2",
|
||||
"title": "Group #1",
|
||||
"graphCoordinates": { "x": 366, "y": 191 },
|
||||
"blocks": [
|
||||
{
|
||||
"id": "s8ZeBL9p5za77eBmdKECLYq",
|
||||
"outgoingEdgeId": "aEBnubX4EMx4Cse6xPAR1m",
|
||||
"type": "text input",
|
||||
"groupId": "bg4QEJseUsTP496H27j5k2",
|
||||
"options": {
|
||||
"isLong": false,
|
||||
"labels": { "button": "Send", "placeholder": "Type your answer..." }
|
||||
},
|
||||
"outgoingEdgeId": "aEBnubX4EMx4Cse6xPAR1m"
|
||||
"labels": {
|
||||
"placeholder": "Type your answer...",
|
||||
"button": "Send"
|
||||
},
|
||||
"isLong": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "Group #1",
|
||||
"graphCoordinates": { "x": 366, "y": 191 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "uhqCZSNbsYVFxop7Gc8xvn",
|
||||
"title": "Group #2",
|
||||
"graphCoordinates": { "x": 793, "y": 99 },
|
||||
"blocks": [
|
||||
{
|
||||
"id": "smyHyeS6yaFaHHU44BNmN4n",
|
||||
"type": "text",
|
||||
"groupId": "uhqCZSNbsYVFxop7Gc8xvn",
|
||||
"content": {
|
||||
"richText": [
|
||||
{ "type": "p", "children": [{ "text": "Second block" }] }
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "Group #2",
|
||||
"graphCoordinates": { "x": 793, "y": 99 }
|
||||
]
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"edges": [
|
||||
{
|
||||
"id": "1z3pfiatTUHbraD2uSoA3E",
|
||||
"to": { "groupId": "bg4QEJseUsTP496H27j5k2" },
|
||||
"from": {
|
||||
"blockId": "rw6smEWEJzHKbiVKLUKFvZ",
|
||||
"groupId": "p4ByLVoKiDRyRoPHKmcTfw"
|
||||
}
|
||||
"from": { "eventId": "p4ByLVoKiDRyRoPHKmcTfw" },
|
||||
"to": { "groupId": "bg4QEJseUsTP496H27j5k2" }
|
||||
},
|
||||
{
|
||||
"id": "aEBnubX4EMx4Cse6xPAR1m",
|
||||
"to": { "groupId": "uhqCZSNbsYVFxop7Gc8xvn" },
|
||||
"from": {
|
||||
"blockId": "s8ZeBL9p5za77eBmdKECLYq",
|
||||
"groupId": "bg4QEJseUsTP496H27j5k2"
|
||||
}
|
||||
"from": { "blockId": "s8ZeBL9p5za77eBmdKECLYq" },
|
||||
"to": { "groupId": "uhqCZSNbsYVFxop7Gc8xvn" }
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"theme": {
|
||||
"general": { "font": "Open Sans", "background": { "type": "None" } },
|
||||
"chat": {
|
||||
"hostBubbles": { "backgroundColor": "#F7F8FF", "color": "#303235" },
|
||||
"guestBubbles": { "backgroundColor": "#FF8E21", "color": "#FFFFFF" },
|
||||
"buttons": { "backgroundColor": "#0042DA", "color": "#FFFFFF" },
|
||||
"inputs": {
|
||||
"color": "#303235",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"color": "#303235",
|
||||
"placeholderColor": "#9095A0"
|
||||
},
|
||||
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
|
||||
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
|
||||
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
|
||||
},
|
||||
"general": { "font": "Open Sans", "background": { "type": "None" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"selectedThemeTemplateId": null,
|
||||
"settings": {
|
||||
"general": {
|
||||
"isBrandingEnabled": true,
|
||||
"isNewResultOnRefreshEnabled": false
|
||||
},
|
||||
"typingEmulation": { "enabled": true, "speed": 300, "maxDelay": 1.5 },
|
||||
"metadata": {
|
||||
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
|
||||
},
|
||||
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
|
||||
}
|
||||
},
|
||||
"createdAt": "2022-03-08T17:18:50.337Z",
|
||||
"updatedAt": "2022-03-08T21:05:28.825Z",
|
||||
"icon": null,
|
||||
"folderId": null,
|
||||
"publicId": null,
|
||||
"customDomain": null
|
||||
"customDomain": null,
|
||||
"workspaceId": "proWorkspace",
|
||||
"resultsTablePreferences": null,
|
||||
"isArchived": false,
|
||||
"isClosed": false,
|
||||
"whatsAppCredentialsId": null,
|
||||
"riskLevel": null
|
||||
}
|
||||
|
339
apps/builder/src/test/assets/typebots/logic/setVariable2.json
Normal file
339
apps/builder/src/test/assets/typebots/logic/setVariable2.json
Normal file
@ -0,0 +1,339 @@
|
||||
{
|
||||
"version": "6",
|
||||
"id": "w8pny2noxxejt2kq36q3udll",
|
||||
"name": "Customer Support",
|
||||
"events": [
|
||||
{
|
||||
"id": "uG1tt8JdDyu2nju3oJ4wc1",
|
||||
"outgoingEdgeId": "2dzxChB1qm9WGfzNF91tfg",
|
||||
"graphCoordinates": { "x": -281, "y": -89 },
|
||||
"type": "start"
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": "vLUAPaxKwPF49iZhg4XZYa",
|
||||
"title": "Menu",
|
||||
"graphCoordinates": { "x": -268.18, "y": -40.15 },
|
||||
"blocks": [
|
||||
{
|
||||
"id": "spud6U3K1omh2dZG8yN2CW4",
|
||||
"type": "text",
|
||||
"content": {
|
||||
"richText": [
|
||||
{ "type": "p", "children": [{ "text": "Hey friend 👋" }] },
|
||||
{ "type": "p", "children": [{ "text": "How can I help you?" }] }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "s6kp2Z4igeY3kL7B64qBdUg",
|
||||
"type": "choice input",
|
||||
"items": [
|
||||
{
|
||||
"id": "fQ8oLDnKmDBuPDK7riJ2kt",
|
||||
"outgoingEdgeId": "dhniFxrsH5r54aEE5JXwK2",
|
||||
"content": "I have a feature request ✨"
|
||||
},
|
||||
{
|
||||
"id": "h2rFDX2UnKS4Kdu3Eyuqq3",
|
||||
"outgoingEdgeId": "2C4mhU5o2Hdm7dztR9xNE9",
|
||||
"content": "There is a bug 🐛"
|
||||
},
|
||||
{
|
||||
"id": "hcUFBPeQA3gSyXRprRk2v9",
|
||||
"outgoingEdgeId": "bTo6CZD1YapDDyVdvJgFDV",
|
||||
"content": "I have a question 💭"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "7MuqF6nen1ZTwGB53Mz8VY",
|
||||
"title": "Bug",
|
||||
"graphCoordinates": { "x": 57.55, "y": -57.03 },
|
||||
"blocks": [
|
||||
{
|
||||
"id": "sjsECyfSBMkUnoWaEnBTmJX",
|
||||
"type": "text",
|
||||
"content": {
|
||||
"richText": [{ "type": "p", "children": [{ "text": "Shoot! 🤪" }] }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "seomQsnPWgiMzQVeZ3us7x2",
|
||||
"type": "text",
|
||||
"content": {
|
||||
"richText": [
|
||||
{
|
||||
"type": "p",
|
||||
"children": [
|
||||
{
|
||||
"text": "Can you describe the bug with as many details as possible?"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "s3LYyyYtjdQ88jkMMV5DSW7",
|
||||
"outgoingEdgeId": "s6i6m1vmx9vl0rniev5iymp1",
|
||||
"type": "text input",
|
||||
"options": {
|
||||
"labels": { "placeholder": "Describe the bug..." },
|
||||
"variableId": "v51BcuecnB6kRU1tsttaGyR",
|
||||
"isLong": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "kyK8JQ77NodUYaz3JLS88A",
|
||||
"title": "Feature request",
|
||||
"graphCoordinates": { "x": 364.36, "y": -517.93 },
|
||||
"blocks": [
|
||||
{
|
||||
"id": "s9bgHcWdobb8Z5cTbrnTz6R",
|
||||
"type": "text",
|
||||
"content": {
|
||||
"richText": [{ "type": "p", "children": [{ "text": "Awesome!" }] }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "s2NbNaBGKhMvdEUdVPXKZjy",
|
||||
"type": "text",
|
||||
"content": {
|
||||
"richText": [
|
||||
{
|
||||
"type": "p",
|
||||
"children": [{ "text": "Head up to the feedback board." }]
|
||||
},
|
||||
{ "type": "p", "children": [{ "text": "" }] },
|
||||
{
|
||||
"type": "p",
|
||||
"children": [
|
||||
{ "text": "👉 " },
|
||||
{
|
||||
"url": "https://app.typebot.io/feedback",
|
||||
"type": "a",
|
||||
"children": [{ "text": "https://app.typebot.io/feedback" }]
|
||||
},
|
||||
{ "text": "" }
|
||||
]
|
||||
},
|
||||
{ "type": "p", "children": [{ "text": "" }] },
|
||||
{
|
||||
"type": "p",
|
||||
"children": [
|
||||
{
|
||||
"text": "There, you'll be able to check existing feature requests and submit yours if it's missing 💪"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cl16lb3b300092e6dh4h01vxw",
|
||||
"type": "choice input",
|
||||
"items": [
|
||||
{
|
||||
"id": "cl16lb3b3000a2e6dy8zdhzpz",
|
||||
"outgoingEdgeId": "wsbg8ht5das922mkojzjh0yy",
|
||||
"content": "Restart"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "j08qxg0h804rngfroedblt5f",
|
||||
"type": "Jump",
|
||||
"options": { "groupId": "vLUAPaxKwPF49iZhg4XZYa" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "puWCBhGWSQRbqTkVH89RCf",
|
||||
"title": "Question",
|
||||
"graphCoordinates": { "x": -234.07, "y": 457.59 },
|
||||
"blocks": [
|
||||
{
|
||||
"id": "sm4iHhLQs9yNdRG3b7xqV8Y",
|
||||
"type": "text",
|
||||
"content": {
|
||||
"richText": [
|
||||
{
|
||||
"type": "p",
|
||||
"children": [
|
||||
{ "text": "First, don't forget to check out the " },
|
||||
{
|
||||
"url": "https://docs.typebot.io/",
|
||||
"type": "a",
|
||||
"children": [{ "text": "Documentation 🙏" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sreX6rwMevEmbTpnkGCtp3k",
|
||||
"type": "text",
|
||||
"content": {
|
||||
"richText": [
|
||||
{
|
||||
"type": "p",
|
||||
"children": [{ "text": "Otherwise, I'm all ears!" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "so4GiKFWWjKCjXgmMJYCGbe",
|
||||
"type": "image",
|
||||
"content": {
|
||||
"url": "https://media0.giphy.com/media/rhgwg4qBu97ISgbfni/giphy-downsized.gif?cid=fe3852a3wimy48e55djt23j44uto7gdlu8ksytylafisvr0q&rid=giphy-downsized.gif&ct=g"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sjd4qACugMarB7gJC8nMhb3",
|
||||
"outgoingEdgeId": "vojurfd82lye9yhtag3rie62",
|
||||
"type": "text input",
|
||||
"options": { "variableId": "v51BcuecnB6kRU1tsttaGyR", "isLong": true }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "1GvxCAAEysxJMxrVngud3X",
|
||||
"title": "Bye",
|
||||
"graphCoordinates": { "x": 143.62, "y": 360.18 },
|
||||
"blocks": [
|
||||
{
|
||||
"id": "s4JATFkBxzmcqqEKQB2xFfa",
|
||||
"type": "choice input",
|
||||
"items": [
|
||||
{ "id": "jqm8wZa5yYb73493n5s3Uc", "content": "Restart" },
|
||||
{
|
||||
"id": "iszohxs8m1yfe0o1q6skmqo5",
|
||||
"outgoingEdgeId": "cqcldkfg50a3lxw8kf6bze2e",
|
||||
"content": "Transcription"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "igdnc34rcmiyamazghr8s708",
|
||||
"type": "Jump",
|
||||
"options": { "groupId": "vLUAPaxKwPF49iZhg4XZYa" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "lhs4apmv49e4zn4vshbqnk0n",
|
||||
"title": "Group #6",
|
||||
"graphCoordinates": { "x": 460.78, "y": 359.03 },
|
||||
"blocks": [
|
||||
{
|
||||
"id": "m1s6w5baydn76trkl145iokz",
|
||||
"type": "Set variable",
|
||||
"options": { "variableId": "vpdyhwqidox0fu265z5r1pxr4" }
|
||||
},
|
||||
{
|
||||
"id": "sdv8sulyi6pg2z2qybzjqmbs",
|
||||
"type": "text",
|
||||
"content": {
|
||||
"richText": [
|
||||
{ "type": "p", "children": [{ "text": "{{Transcription}}" }] }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "2dzxChB1qm9WGfzNF91tfg",
|
||||
"from": { "eventId": "uG1tt8JdDyu2nju3oJ4wc1" },
|
||||
"to": { "groupId": "vLUAPaxKwPF49iZhg4XZYa" }
|
||||
},
|
||||
{
|
||||
"id": "dhniFxrsH5r54aEE5JXwK2",
|
||||
"from": {
|
||||
"blockId": "s6kp2Z4igeY3kL7B64qBdUg",
|
||||
"itemId": "fQ8oLDnKmDBuPDK7riJ2kt"
|
||||
},
|
||||
"to": { "groupId": "kyK8JQ77NodUYaz3JLS88A" }
|
||||
},
|
||||
{
|
||||
"id": "2C4mhU5o2Hdm7dztR9xNE9",
|
||||
"from": {
|
||||
"blockId": "s6kp2Z4igeY3kL7B64qBdUg",
|
||||
"itemId": "h2rFDX2UnKS4Kdu3Eyuqq3"
|
||||
},
|
||||
"to": { "groupId": "7MuqF6nen1ZTwGB53Mz8VY" }
|
||||
},
|
||||
{
|
||||
"id": "bTo6CZD1YapDDyVdvJgFDV",
|
||||
"from": {
|
||||
"blockId": "s6kp2Z4igeY3kL7B64qBdUg",
|
||||
"itemId": "hcUFBPeQA3gSyXRprRk2v9"
|
||||
},
|
||||
"to": { "groupId": "puWCBhGWSQRbqTkVH89RCf" }
|
||||
},
|
||||
{
|
||||
"id": "cl1571xtc00042e6dcptam5jw",
|
||||
"from": { "blockId": "s5Fh7zHUw3j4zDM5xjzwsXB" },
|
||||
"to": { "groupId": "1GvxCAAEysxJMxrVngud3X" }
|
||||
},
|
||||
{
|
||||
"id": "vojurfd82lye9yhtag3rie62",
|
||||
"from": { "blockId": "sjd4qACugMarB7gJC8nMhb3" },
|
||||
"to": { "groupId": "1GvxCAAEysxJMxrVngud3X" }
|
||||
},
|
||||
{
|
||||
"id": "s6i6m1vmx9vl0rniev5iymp1",
|
||||
"from": { "blockId": "s3LYyyYtjdQ88jkMMV5DSW7" },
|
||||
"to": { "groupId": "1GvxCAAEysxJMxrVngud3X" }
|
||||
},
|
||||
{
|
||||
"id": "wsbg8ht5das922mkojzjh0yy",
|
||||
"from": {
|
||||
"blockId": "cl16lb3b300092e6dh4h01vxw",
|
||||
"itemId": "cl16lb3b3000a2e6dy8zdhzpz"
|
||||
},
|
||||
"to": { "groupId": "1GvxCAAEysxJMxrVngud3X" }
|
||||
},
|
||||
{
|
||||
"id": "cqcldkfg50a3lxw8kf6bze2e",
|
||||
"from": {
|
||||
"blockId": "s4JATFkBxzmcqqEKQB2xFfa",
|
||||
"itemId": "iszohxs8m1yfe0o1q6skmqo5"
|
||||
},
|
||||
"to": { "groupId": "lhs4apmv49e4zn4vshbqnk0n" }
|
||||
}
|
||||
],
|
||||
"variables": [
|
||||
{ "id": "t2k6cj3uYfNdJX13APA4b9", "name": "Email" },
|
||||
{ "id": "v51BcuecnB6kRU1tsttaGyR", "name": "Content" },
|
||||
{
|
||||
"id": "vpdyhwqidox0fu265z5r1pxr4",
|
||||
"name": "Transcription",
|
||||
"isSessionVariable": true
|
||||
}
|
||||
],
|
||||
"theme": {},
|
||||
"selectedThemeTemplateId": null,
|
||||
"settings": { "typingEmulation": { "enabled": false } },
|
||||
"createdAt": "2024-05-04T09:07:37.028Z",
|
||||
"updatedAt": "2024-05-04T09:09:56.735Z",
|
||||
"icon": "😍",
|
||||
"folderId": null,
|
||||
"publicId": null,
|
||||
"customDomain": null,
|
||||
"workspaceId": "proWorkspace",
|
||||
"resultsTablePreferences": null,
|
||||
"isArchived": false,
|
||||
"isClosed": false,
|
||||
"whatsAppCredentialsId": null,
|
||||
"riskLevel": null
|
||||
}
|
@ -587,7 +587,8 @@
|
||||
"Result ID",
|
||||
"Random ID",
|
||||
"Phone number",
|
||||
"Contact name"
|
||||
"Contact name",
|
||||
"Transcript"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -2679,10 +2680,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/typebots/{typebotId}/analytics/totalAnswersInBlocks": {
|
||||
"/v1/typebots/{typebotId}/analytics/inDepthData": {
|
||||
"get": {
|
||||
"operationId": "analytics-getTotalAnswers",
|
||||
"summary": "List total answers in blocks",
|
||||
"operationId": "analytics-getInDepthAnalyticsData",
|
||||
"summary": "List total answers in blocks and off-default paths visited edges",
|
||||
"tags": [
|
||||
"Analytics"
|
||||
],
|
||||
@ -2753,123 +2754,8 @@
|
||||
"total"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"totalAnswers"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid input data",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/error.BAD_REQUEST"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Authorization not provided",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/error.UNAUTHORIZED"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Insufficient access",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/error.FORBIDDEN"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/error.NOT_FOUND"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/error.INTERNAL_SERVER_ERROR"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/typebots/{typebotId}/analytics/totalVisitedEdges": {
|
||||
"get": {
|
||||
"operationId": "analytics-getTotalVisitedEdges",
|
||||
"summary": "List total edges used in results",
|
||||
"tags": [
|
||||
"Analytics"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"Authorization": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "typebotId",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "timeFilter",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"today",
|
||||
"last7Days",
|
||||
"last30Days",
|
||||
"monthToDate",
|
||||
"lastMonth",
|
||||
"yearToDate",
|
||||
"allTime"
|
||||
],
|
||||
"default": "last7Days"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "timeZone",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"totalVisitedEdges": {
|
||||
},
|
||||
"offDefaultPathVisitedEdges": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
@ -2889,7 +2775,8 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"totalVisitedEdges"
|
||||
"totalAnswers",
|
||||
"offDefaultPathVisitedEdges"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -5022,7 +4909,8 @@
|
||||
"Result ID",
|
||||
"Random ID",
|
||||
"Phone number",
|
||||
"Contact name"
|
||||
"Contact name",
|
||||
"Transcript"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -8480,7 +8368,8 @@
|
||||
"Result ID",
|
||||
"Random ID",
|
||||
"Phone number",
|
||||
"Contact name"
|
||||
"Contact name",
|
||||
"Transcript"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -11203,43 +11092,25 @@
|
||||
"type": "boolean",
|
||||
"nullable": true
|
||||
},
|
||||
"lastChatSessionId": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"answers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"resultId": {
|
||||
"type": "string"
|
||||
},
|
||||
"blockId": {
|
||||
"type": "string"
|
||||
},
|
||||
"groupId": {
|
||||
"type": "string"
|
||||
},
|
||||
"variableId": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"storageUsed": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"createdAt",
|
||||
"resultId",
|
||||
"blockId",
|
||||
"groupId",
|
||||
"variableId",
|
||||
"content",
|
||||
"storageUsed"
|
||||
"content"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -11252,6 +11123,7 @@
|
||||
"isCompleted",
|
||||
"hasStarted",
|
||||
"isArchived",
|
||||
"lastChatSessionId",
|
||||
"answers"
|
||||
]
|
||||
}
|
||||
@ -11515,43 +11387,25 @@
|
||||
"type": "boolean",
|
||||
"nullable": true
|
||||
},
|
||||
"lastChatSessionId": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"answers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"resultId": {
|
||||
"type": "string"
|
||||
},
|
||||
"blockId": {
|
||||
"type": "string"
|
||||
},
|
||||
"groupId": {
|
||||
"type": "string"
|
||||
},
|
||||
"variableId": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"storageUsed": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"createdAt",
|
||||
"resultId",
|
||||
"blockId",
|
||||
"groupId",
|
||||
"variableId",
|
||||
"content",
|
||||
"storageUsed"
|
||||
"content"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -11564,6 +11418,7 @@
|
||||
"isCompleted",
|
||||
"hasStarted",
|
||||
"isArchived",
|
||||
"lastChatSessionId",
|
||||
"answers"
|
||||
]
|
||||
}
|
||||
@ -17011,7 +16866,8 @@
|
||||
"Result ID",
|
||||
"Random ID",
|
||||
"Phone number",
|
||||
"Contact name"
|
||||
"Contact name",
|
||||
"Transcript"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -22665,7 +22521,8 @@
|
||||
"Result ID",
|
||||
"Random ID",
|
||||
"Phone number",
|
||||
"Contact name"
|
||||
"Contact name",
|
||||
"Transcript"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -25492,7 +25349,8 @@
|
||||
"Result ID",
|
||||
"Random ID",
|
||||
"Phone number",
|
||||
"Contact name"
|
||||
"Contact name",
|
||||
"Transcript"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -1851,6 +1851,10 @@
|
||||
"First name": "John",
|
||||
"Email": "john@gmail.com"
|
||||
}
|
||||
},
|
||||
"sessionId": {
|
||||
"type": "string",
|
||||
"description": "If provided, will be used as the session ID and will overwrite any existing session with the same ID."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3675,7 +3679,8 @@
|
||||
"Result ID",
|
||||
"Random ID",
|
||||
"Phone number",
|
||||
"Contact name"
|
||||
"Contact name",
|
||||
"Transcript"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -7818,7 +7823,8 @@
|
||||
"Result ID",
|
||||
"Random ID",
|
||||
"Phone number",
|
||||
"Contact name"
|
||||
"Contact name",
|
||||
"Transcript"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -29,7 +29,7 @@ export default defineConfig({
|
||||
use: {
|
||||
trace: 'on-first-retry',
|
||||
locale: 'en-US',
|
||||
baseURL: process.env.NEXT_PUBLIC_VIEWER_URL,
|
||||
baseURL: process.env.NEXT_PUBLIC_VIEWER_URL?.split(',')[0],
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
|
@ -60,6 +60,7 @@ export const sendMessageV1 = publicProcedure
|
||||
clientSideActions,
|
||||
newSessionState,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
} = await startSession({
|
||||
version: 1,
|
||||
startParams:
|
||||
@ -136,6 +137,7 @@ export const sendMessageV1 = publicProcedure
|
||||
hasCustomEmbedBubble: messages.some(
|
||||
(message) => message.type === 'custom-embed'
|
||||
),
|
||||
setVariableHistory,
|
||||
})
|
||||
|
||||
return {
|
||||
@ -176,6 +178,7 @@ export const sendMessageV1 = publicProcedure
|
||||
logs,
|
||||
lastMessageNewFormat,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
} = await continueBotFlow(message, { version: 1, state: session.state })
|
||||
|
||||
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
|
||||
@ -193,6 +196,7 @@ export const sendMessageV1 = publicProcedure
|
||||
hasCustomEmbedBubble: messages.some(
|
||||
(message) => message.type === 'custom-embed'
|
||||
),
|
||||
setVariableHistory,
|
||||
})
|
||||
|
||||
return {
|
||||
|
@ -60,6 +60,7 @@ export const sendMessageV2 = publicProcedure
|
||||
clientSideActions,
|
||||
newSessionState,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
} = await startSession({
|
||||
version: 2,
|
||||
startParams:
|
||||
@ -136,6 +137,7 @@ export const sendMessageV2 = publicProcedure
|
||||
hasCustomEmbedBubble: messages.some(
|
||||
(message) => message.type === 'custom-embed'
|
||||
),
|
||||
setVariableHistory,
|
||||
})
|
||||
|
||||
return {
|
||||
@ -175,6 +177,7 @@ export const sendMessageV2 = publicProcedure
|
||||
logs,
|
||||
lastMessageNewFormat,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
} = await continueBotFlow(message, { version: 2, state: session.state })
|
||||
|
||||
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
|
||||
@ -192,6 +195,7 @@ export const sendMessageV2 = publicProcedure
|
||||
hasCustomEmbedBubble: messages.some(
|
||||
(message) => message.type === 'custom-embed'
|
||||
),
|
||||
setVariableHistory,
|
||||
})
|
||||
|
||||
return {
|
||||
|
@ -27,6 +27,7 @@ export const startChatPreview = publicProcedure
|
||||
typebotId,
|
||||
typebot: startTypebot,
|
||||
prefilledVariables,
|
||||
sessionId,
|
||||
},
|
||||
ctx: { user },
|
||||
}) =>
|
||||
@ -39,5 +40,6 @@ export const startChatPreview = publicProcedure
|
||||
typebot: startTypebot,
|
||||
userId: user?.id,
|
||||
prefilledVariables,
|
||||
sessionId,
|
||||
})
|
||||
)
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
ResultValues,
|
||||
Typebot,
|
||||
@ -36,7 +37,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { resultValues, variables, parentTypebotIds } = (
|
||||
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||
) as {
|
||||
resultValues: ResultValues | undefined
|
||||
resultValues: ResultValues
|
||||
variables: Variable[]
|
||||
parentTypebotIds: string[]
|
||||
}
|
||||
@ -76,7 +77,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const linkedTypebots = [...linkedTypebotsParents, ...linkedTypebotsChildren]
|
||||
|
||||
const answers = resultValues
|
||||
? resultValues.answers.map((answer) => ({
|
||||
? resultValues.answers.map((answer: any) => ({
|
||||
key:
|
||||
(answer.variableId
|
||||
? typebot.variables.find(
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
PublicTypebot,
|
||||
ResultValues,
|
||||
@ -203,7 +204,7 @@ const getEmailBody = async ({
|
||||
})) as unknown as PublicTypebot
|
||||
if (!typebot) return
|
||||
const answers = parseAnswers({
|
||||
answers: resultValues.answers.map((answer) => ({
|
||||
answers: (resultValues as any).answers.map((answer: any) => ({
|
||||
key:
|
||||
(answer.variableId
|
||||
? typebot.variables.find(
|
||||
|
@ -1,38 +1,16 @@
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { Answer } from '@typebot.io/prisma'
|
||||
import { got } from 'got'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { isNotDefined } from '@typebot.io/lib'
|
||||
import { methodNotAllowed } from '@typebot.io/lib/api'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'PUT') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { uploadedFiles, ...answer } = (
|
||||
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||
) as Answer & { uploadedFiles?: boolean }
|
||||
let storageUsed = 0
|
||||
if (uploadedFiles && answer.content.includes('http')) {
|
||||
const fileUrls = answer.content.split(', ')
|
||||
const hasReachedStorageLimit = fileUrls[0] === null
|
||||
if (!hasReachedStorageLimit) {
|
||||
for (const url of fileUrls) {
|
||||
const { headers } = await got(url)
|
||||
const size = headers['content-length']
|
||||
if (isNotDefined(size)) return
|
||||
storageUsed += parseInt(size, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await prisma.answer.upsert({
|
||||
where: {
|
||||
resultId_blockId_groupId: {
|
||||
resultId: answer.resultId,
|
||||
groupId: answer.groupId,
|
||||
blockId: answer.blockId,
|
||||
},
|
||||
},
|
||||
create: { ...answer, storageUsed: storageUsed > 0 ? storageUsed : null },
|
||||
update: { ...answer, storageUsed: storageUsed > 0 ? storageUsed : null },
|
||||
) as Answer & { uploadedFiles: string[] }
|
||||
const result = await prisma.answer.createMany({
|
||||
data: [{ ...answer }],
|
||||
})
|
||||
return res.send(result)
|
||||
}
|
||||
|
180
apps/viewer/src/test/assets/typebots/transcript.json
Normal file
180
apps/viewer/src/test/assets/typebots/transcript.json
Normal file
@ -0,0 +1,180 @@
|
||||
{
|
||||
"version": "6",
|
||||
"id": "clvxf9ent0001tjmcr3e4bypk",
|
||||
"name": "My typebot",
|
||||
"events": [
|
||||
{
|
||||
"id": "d8r5fbpb2eqsq8egwydygiu2",
|
||||
"outgoingEdgeId": "eblhvxj5u2tmwr1459lwxrjh",
|
||||
"graphCoordinates": { "x": 0, "y": 0 },
|
||||
"type": "start"
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": "tfsvlygr7lay21s5w475syd8",
|
||||
"title": "Answer",
|
||||
"graphCoordinates": { "x": 579.3, "y": -31.43 },
|
||||
"blocks": [
|
||||
{
|
||||
"id": "pkadqaxm0ow0qvt4d6k9eznd",
|
||||
"type": "text",
|
||||
"content": {
|
||||
"richText": [
|
||||
{
|
||||
"type": "p",
|
||||
"children": [{ "text": "How are you? You said {{Answer}}" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "n8b3zi7wd6eory602o006e2a",
|
||||
"type": "text input",
|
||||
"options": { "variableId": "vzbq6pomlaf5bhav64ef5wx3d" }
|
||||
},
|
||||
{
|
||||
"id": "byrr3jxa2qh3imilup7yu1bz",
|
||||
"outgoingEdgeId": "yyc69sbg26ygd7oofetqrmj3",
|
||||
"type": "Set variable",
|
||||
"options": {
|
||||
"variableId": "ve15gxz2fsq004tcqbub0d4m4",
|
||||
"expressionToEvaluate": "{{Answers count}} + 1"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sw6habablg7wmyxzcat99wia",
|
||||
"title": "Condition",
|
||||
"graphCoordinates": { "x": 950.69, "y": -30.46 },
|
||||
"blocks": [
|
||||
{
|
||||
"id": "k7hs4zsybbbece1b0080d2pj",
|
||||
"type": "Condition",
|
||||
"items": [
|
||||
{
|
||||
"id": "ukawer7gc6qdpr4eh0fw2pnv",
|
||||
"content": {
|
||||
"comparisons": [
|
||||
{
|
||||
"id": "jgb06bu8qz0va8vtnarqxivd",
|
||||
"variableId": "ve15gxz2fsq004tcqbub0d4m4",
|
||||
"comparisonOperator": "Equal to",
|
||||
"value": "3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"outgoingEdgeId": "hyel5nw6btuiudmt83b25dvu"
|
||||
}
|
||||
],
|
||||
"outgoingEdgeId": "cz2ayuq8nsoqosxlzu8pyebd"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "kpmjs3nqbbq88f63us13yqyy",
|
||||
"title": "Init",
|
||||
"graphCoordinates": { "x": 235.16, "y": -17.47 },
|
||||
"blocks": [
|
||||
{
|
||||
"id": "w487kr9s9wg3mar7ilfr3tep",
|
||||
"outgoingEdgeId": "mdcj3y9t8kh4uy8lhoh4avdj",
|
||||
"type": "Set variable",
|
||||
"options": {
|
||||
"variableId": "ve15gxz2fsq004tcqbub0d4m4",
|
||||
"expressionToEvaluate": "0"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "wno2kz74jmhzgbi05z4ftjoj",
|
||||
"title": "Transcript",
|
||||
"graphCoordinates": { "x": 1308.8, "y": -41 },
|
||||
"blocks": [
|
||||
{
|
||||
"id": "ejy8vk6gnzegn5copktmw74q",
|
||||
"type": "Set variable",
|
||||
"options": {
|
||||
"variableId": "vs2p20vizsf45xcpgwq5ab3rw",
|
||||
"type": "Transcript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "qoa74xt647j42sk5b0yyvz9k",
|
||||
"type": "text",
|
||||
"content": {
|
||||
"richText": [
|
||||
{ "type": "p", "children": [{ "text": "{{Transcript}}" }] }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "eblhvxj5u2tmwr1459lwxrjh",
|
||||
"from": { "eventId": "d8r5fbpb2eqsq8egwydygiu2" },
|
||||
"to": { "groupId": "kpmjs3nqbbq88f63us13yqyy" }
|
||||
},
|
||||
{
|
||||
"id": "mdcj3y9t8kh4uy8lhoh4avdj",
|
||||
"from": { "blockId": "w487kr9s9wg3mar7ilfr3tep" },
|
||||
"to": { "groupId": "tfsvlygr7lay21s5w475syd8" }
|
||||
},
|
||||
{
|
||||
"id": "yyc69sbg26ygd7oofetqrmj3",
|
||||
"from": { "blockId": "byrr3jxa2qh3imilup7yu1bz" },
|
||||
"to": { "groupId": "sw6habablg7wmyxzcat99wia" }
|
||||
},
|
||||
{
|
||||
"from": {
|
||||
"blockId": "k7hs4zsybbbece1b0080d2pj",
|
||||
"itemId": "ukawer7gc6qdpr4eh0fw2pnv"
|
||||
},
|
||||
"to": { "groupId": "wno2kz74jmhzgbi05z4ftjoj" },
|
||||
"id": "hyel5nw6btuiudmt83b25dvu"
|
||||
},
|
||||
{
|
||||
"from": { "blockId": "k7hs4zsybbbece1b0080d2pj" },
|
||||
"to": { "groupId": "tfsvlygr7lay21s5w475syd8" },
|
||||
"id": "cz2ayuq8nsoqosxlzu8pyebd"
|
||||
}
|
||||
],
|
||||
"variables": [
|
||||
{
|
||||
"id": "ve15gxz2fsq004tcqbub0d4m4",
|
||||
"name": "Answers count",
|
||||
"isSessionVariable": true
|
||||
},
|
||||
{
|
||||
"id": "vs2p20vizsf45xcpgwq5ab3rw",
|
||||
"name": "Transcript",
|
||||
"isSessionVariable": true
|
||||
},
|
||||
{
|
||||
"id": "vzbq6pomlaf5bhav64ef5wx3d",
|
||||
"name": "Answer",
|
||||
"isSessionVariable": true
|
||||
}
|
||||
],
|
||||
"theme": {},
|
||||
"selectedThemeTemplateId": null,
|
||||
"settings": {
|
||||
"typingEmulation": { "enabled": false }
|
||||
},
|
||||
"createdAt": "2024-05-08T06:11:55.385Z",
|
||||
"updatedAt": "2024-05-08T06:28:18.313Z",
|
||||
"icon": null,
|
||||
"folderId": null,
|
||||
"publicId": null,
|
||||
"customDomain": null,
|
||||
"workspaceId": "proWorkspace",
|
||||
"resultsTablePreferences": null,
|
||||
"isArchived": false,
|
||||
"isClosed": false,
|
||||
"whatsAppCredentialsId": null,
|
||||
"riskLevel": null
|
||||
}
|
34
apps/viewer/src/test/transcript.spec.ts
Normal file
34
apps/viewer/src/test/transcript.spec.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { importTypebotInDatabase } from '@typebot.io/playwright/databaseActions'
|
||||
import { getTestAsset } from './utils/playwright'
|
||||
|
||||
test('Transcript set variable should be correctly computed', async ({
|
||||
page,
|
||||
}) => {
|
||||
const typebotId = createId()
|
||||
await importTypebotInDatabase(getTestAsset('typebots/transcript.json'), {
|
||||
id: typebotId,
|
||||
publicId: `${typebotId}-public`,
|
||||
})
|
||||
|
||||
await page.goto(`/${typebotId}-public`)
|
||||
await page.getByPlaceholder('Type your answer...').fill('hey')
|
||||
await page.getByRole('button').click()
|
||||
await page.getByPlaceholder('Type your answer...').fill('hey 2')
|
||||
await page.getByRole('button').click()
|
||||
await page.getByPlaceholder('Type your answer...').fill('hey 3')
|
||||
await page.getByRole('button').click()
|
||||
await expect(
|
||||
page.getByText('Assistant: "How are you? You said "')
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
page.getByText('Assistant: "How are you? You said hey"')
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
page.getByText('Assistant: "How are you? You said hey 3"')
|
||||
).toBeVisible()
|
||||
await expect(page.getByText('User: "hey"')).toBeVisible()
|
||||
await expect(page.getByText('User: "hey 2"')).toBeVisible()
|
||||
await expect(page.getByText('User: "hey 3"')).toBeVisible()
|
||||
})
|
Reference in New Issue
Block a user