fix(webhook): 🐛 Sample result w/ multi input blocks
This commit is contained in:
@@ -2,9 +2,10 @@
|
|||||||
/* eslint-disable react/jsx-key */
|
/* eslint-disable react/jsx-key */
|
||||||
import { Button, chakra, Checkbox, Flex, HStack, Text } from '@chakra-ui/react'
|
import { Button, chakra, Checkbox, Flex, HStack, Text } from '@chakra-ui/react'
|
||||||
import { AlignLeftTextIcon } from 'assets/icons'
|
import { AlignLeftTextIcon } from 'assets/icons'
|
||||||
|
import { ResultHeaderCell } from 'models'
|
||||||
import React, { useEffect, useMemo, useRef } from 'react'
|
import React, { useEffect, useMemo, useRef } from 'react'
|
||||||
import { Hooks, useRowSelect, useTable } from 'react-table'
|
import { Hooks, useRowSelect, useTable } from 'react-table'
|
||||||
import { parseSubmissionsColumns, ResultHeaderCell } from 'services/typebots'
|
import { parseSubmissionsColumns } from 'services/typebots'
|
||||||
import { LoadingRows } from './LoadingRows'
|
import { LoadingRows } from './LoadingRows'
|
||||||
|
|
||||||
type SubmissionsTableProps = {
|
type SubmissionsTableProps = {
|
||||||
|
|||||||
@@ -7,14 +7,13 @@ import {
|
|||||||
deleteAllResults,
|
deleteAllResults,
|
||||||
deleteResults,
|
deleteResults,
|
||||||
getAllResults,
|
getAllResults,
|
||||||
parseResultHeader,
|
|
||||||
useResults,
|
useResults,
|
||||||
} from 'services/typebots'
|
} from 'services/typebots'
|
||||||
import { unparse } from 'papaparse'
|
import { unparse } from 'papaparse'
|
||||||
import { UnlockProPlanInfo } from 'components/shared/Info'
|
import { UnlockProPlanInfo } from 'components/shared/Info'
|
||||||
import { LogsModal } from './LogsModal'
|
import { LogsModal } from './LogsModal'
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
import { isDefined } from 'utils'
|
import { isDefined, parseResultHeader } from 'utils'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
typebotId: string
|
typebotId: string
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"id": "cl10u677f0075a01a6xgl6phe",
|
||||||
|
"createdAt": "2022-03-21T15:01:46.107Z",
|
||||||
|
"updatedAt": "2022-03-21T15:03:07.312Z",
|
||||||
|
"name": "My typebot",
|
||||||
|
"ownerId": "cl10hgjy90000lm1a1gyccuqj",
|
||||||
|
"publishedTypebotId": null,
|
||||||
|
"folderId": null,
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "cl10u677d0000a01aa4g4aazg",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"id": "cl10u677d0001a01a0xfo3d11",
|
||||||
|
"type": "start",
|
||||||
|
"label": "Start",
|
||||||
|
"blockId": "cl10u677d0000a01aa4g4aazg",
|
||||||
|
"outgoingEdgeId": "cl10u6cw500052e6dq284zju3"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Start",
|
||||||
|
"graphCoordinates": { "x": 0, "y": 0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cl10u68pw00032e6depze2oiy",
|
||||||
|
"graphCoordinates": { "x": 353, "y": 121 },
|
||||||
|
"title": "Block #1",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"id": "cl10u68q000042e6dhdipu2wg",
|
||||||
|
"blockId": "cl10u68pw00032e6depze2oiy",
|
||||||
|
"type": "text",
|
||||||
|
"content": {
|
||||||
|
"html": "<div>Hi how are you?</div>",
|
||||||
|
"richText": [
|
||||||
|
{ "type": "p", "children": [{ "text": "Hi how are you?" }] }
|
||||||
|
],
|
||||||
|
"plainText": "Hi how are you?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cl10u6ey300062e6dea9ikpko",
|
||||||
|
"blockId": "cl10u68pw00032e6depze2oiy",
|
||||||
|
"type": "text input",
|
||||||
|
"options": {
|
||||||
|
"isLong": false,
|
||||||
|
"labels": { "button": "Send", "placeholder": "Type your answer..." }
|
||||||
|
},
|
||||||
|
"outgoingEdgeId": "cl10u7ax4000g2e6dkqoq18kp"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cl10u6jzd00072e6dvo0zwy0s",
|
||||||
|
"graphCoordinates": { "x": 691, "y": 127 },
|
||||||
|
"title": "Block #2",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"id": "cl10u6jzt00082e6dgw1piz0q",
|
||||||
|
"blockId": "cl10u6jzd00072e6dvo0zwy0s",
|
||||||
|
"type": "text",
|
||||||
|
"content": {
|
||||||
|
"html": "<div>How old are you?</div>",
|
||||||
|
"richText": [
|
||||||
|
{ "type": "p", "children": [{ "text": "How old are you?" }] }
|
||||||
|
],
|
||||||
|
"plainText": "How old are you?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cl10u6qa300092e6dh5izz7ig",
|
||||||
|
"blockId": "cl10u6jzd00072e6dvo0zwy0s",
|
||||||
|
"type": "number input",
|
||||||
|
"options": {
|
||||||
|
"labels": { "button": "Send", "placeholder": "Type a number..." }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cl10u6vbo000a2e6davz2hfw7",
|
||||||
|
"blockId": "cl10u6jzd00072e6dvo0zwy0s",
|
||||||
|
"type": "text",
|
||||||
|
"content": {
|
||||||
|
"html": "<div>Do you like cookies?</div>",
|
||||||
|
"richText": [
|
||||||
|
{ "type": "p", "children": [{ "text": "Do you like cookies?" }] }
|
||||||
|
],
|
||||||
|
"plainText": "Do you like cookies?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cl10u6zk0000b2e6dvabq067r",
|
||||||
|
"blockId": "cl10u6jzd00072e6dvo0zwy0s",
|
||||||
|
"type": "choice input",
|
||||||
|
"options": { "buttonLabel": "Send", "isMultipleChoice": false },
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "cl10u6zk1000c2e6d0d4ivgcl",
|
||||||
|
"stepId": "cl10u6zk0000b2e6dvabq067r",
|
||||||
|
"type": 0,
|
||||||
|
"content": "Yes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stepId": "cl10u6zk0000b2e6dvabq067r",
|
||||||
|
"type": 0,
|
||||||
|
"id": "cl10u70gi000d2e6d924ywjsb",
|
||||||
|
"content": "No"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cl10u759h000f2e6d0rhfwep4",
|
||||||
|
"blockId": "cl10u6jzd00072e6dvo0zwy0s",
|
||||||
|
"type": "text",
|
||||||
|
"content": {
|
||||||
|
"html": "<div>Alright, cheers!</div>",
|
||||||
|
"richText": [
|
||||||
|
{ "type": "p", "children": [{ "text": "Alright, cheers!" }] }
|
||||||
|
],
|
||||||
|
"plainText": "Alright, cheers!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cl10u7i6n000h2e6d537h38pg",
|
||||||
|
"blockId": "cl10u6jzd00072e6dvo0zwy0s",
|
||||||
|
"type": "Webhook",
|
||||||
|
"options": {
|
||||||
|
"responseVariableMapping": [],
|
||||||
|
"variablesForTest": [],
|
||||||
|
"isAdvancedConfig": false,
|
||||||
|
"isCustomBody": false
|
||||||
|
},
|
||||||
|
"webhookId": "webhook1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variables": [],
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"from": {
|
||||||
|
"blockId": "cl10u677d0000a01aa4g4aazg",
|
||||||
|
"stepId": "cl10u677d0001a01a0xfo3d11"
|
||||||
|
},
|
||||||
|
"to": { "blockId": "cl10u68pw00032e6depze2oiy" },
|
||||||
|
"id": "cl10u6cw500052e6dq284zju3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": {
|
||||||
|
"blockId": "cl10u68pw00032e6depze2oiy",
|
||||||
|
"stepId": "cl10u6ey300062e6dea9ikpko"
|
||||||
|
},
|
||||||
|
"to": { "blockId": "cl10u6jzd00072e6dvo0zwy0s" },
|
||||||
|
"id": "cl10u7ax4000g2e6dkqoq18kp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme": {
|
||||||
|
"chat": {
|
||||||
|
"inputs": {
|
||||||
|
"color": "#303235",
|
||||||
|
"backgroundColor": "#FFFFFF",
|
||||||
|
"placeholderColor": "#9095A0"
|
||||||
|
},
|
||||||
|
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
|
||||||
|
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
|
||||||
|
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
|
||||||
|
},
|
||||||
|
"general": { "font": "Open Sans", "background": { "type": "None" } }
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"general": {
|
||||||
|
"isBrandingEnabled": true,
|
||||||
|
"isInputPrefillEnabled": true,
|
||||||
|
"isNewResultOnRefreshEnabled": false
|
||||||
|
},
|
||||||
|
"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 }
|
||||||
|
},
|
||||||
|
"publicId": null,
|
||||||
|
"customDomain": null
|
||||||
|
}
|
||||||
@@ -8,7 +8,10 @@ test.describe('Webhook step', () => {
|
|||||||
test('easy configuration should work', async ({ page }) => {
|
test('easy configuration should work', async ({ page }) => {
|
||||||
const typebotId = cuid()
|
const typebotId = cuid()
|
||||||
await importTypebotInDatabase(
|
await importTypebotInDatabase(
|
||||||
path.join(__dirname, '../../fixtures/typebots/integrations/webhook.json'),
|
path.join(
|
||||||
|
__dirname,
|
||||||
|
'../../fixtures/typebots/integrations/easyConfigWebhook.json'
|
||||||
|
),
|
||||||
{
|
{
|
||||||
id: typebotId,
|
id: typebotId,
|
||||||
}
|
}
|
||||||
@@ -22,7 +25,7 @@ test.describe('Webhook step', () => {
|
|||||||
)
|
)
|
||||||
await page.click('text=Test the request')
|
await page.click('text=Test the request')
|
||||||
await expect(page.locator('div[role="textbox"] >> nth=-1')).toContainText(
|
await expect(page.locator('div[role="textbox"] >> nth=-1')).toContainText(
|
||||||
'"statusCode": 200'
|
`"Block #1": "answer value", "Block #2": "20", "Block #2 (1)": "Yes"`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
test('Generated body should work', async ({ page }) => {
|
test('Generated body should work', async ({ page }) => {
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import {
|
import { ResultWithAnswers, VariableWithValue, ResultHeaderCell } from 'models'
|
||||||
Block,
|
|
||||||
InputStep,
|
|
||||||
InputStepType,
|
|
||||||
ResultWithAnswers,
|
|
||||||
Variable,
|
|
||||||
VariableWithValue,
|
|
||||||
} from 'models'
|
|
||||||
import useSWRInfinite from 'swr/infinite'
|
import useSWRInfinite from 'swr/infinite'
|
||||||
import { stringify } from 'qs'
|
import { stringify } from 'qs'
|
||||||
import { Answer } from 'db'
|
import { Answer } from 'db'
|
||||||
import { byId, isDefined, isInputStep, sendRequest } from 'utils'
|
import { isDefined, sendRequest } from 'utils'
|
||||||
import { fetcher } from 'services/utils'
|
import { fetcher } from 'services/utils'
|
||||||
import { HStack, Text } from '@chakra-ui/react'
|
import { HStack, Text } from '@chakra-ui/react'
|
||||||
import { CodeIcon, CalendarIcon } from 'assets/icons'
|
import { CodeIcon, CalendarIcon } from 'assets/icons'
|
||||||
@@ -110,14 +103,6 @@ type HeaderCell = {
|
|||||||
accessor: string
|
accessor: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ResultHeaderCell = {
|
|
||||||
label: string
|
|
||||||
stepId?: string
|
|
||||||
stepType?: InputStepType
|
|
||||||
isLong?: boolean
|
|
||||||
variableId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const parseSubmissionsColumns = (
|
export const parseSubmissionsColumns = (
|
||||||
resultHeader: ResultHeaderCell[]
|
resultHeader: ResultHeaderCell[]
|
||||||
): HeaderCell[] =>
|
): HeaderCell[] =>
|
||||||
@@ -140,83 +125,6 @@ const HeaderIcon = ({ header }: { header: ResultHeaderCell }) =>
|
|||||||
<CalendarIcon />
|
<CalendarIcon />
|
||||||
)
|
)
|
||||||
|
|
||||||
export const parseResultHeader = ({
|
|
||||||
blocks,
|
|
||||||
variables,
|
|
||||||
}: {
|
|
||||||
blocks: Block[]
|
|
||||||
variables: Variable[]
|
|
||||||
}): ResultHeaderCell[] => {
|
|
||||||
const parsedBlocks = parseInputsResultHeader({ blocks, variables })
|
|
||||||
return [
|
|
||||||
{ label: 'Submitted at' },
|
|
||||||
...parsedBlocks,
|
|
||||||
...parseVariablesHeaders(variables, parsedBlocks),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseInputsResultHeader = ({
|
|
||||||
blocks,
|
|
||||||
variables,
|
|
||||||
}: {
|
|
||||||
blocks: Block[]
|
|
||||||
variables: Variable[]
|
|
||||||
}): ResultHeaderCell[] =>
|
|
||||||
(
|
|
||||||
blocks
|
|
||||||
.flatMap((b) =>
|
|
||||||
b.steps.map((s) => ({
|
|
||||||
...s,
|
|
||||||
blockTitle: b.title,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
.filter((step) => isInputStep(step)) as (InputStep & {
|
|
||||||
blockTitle: string
|
|
||||||
})[]
|
|
||||||
).reduce<ResultHeaderCell[]>((headers, inputStep) => {
|
|
||||||
if (
|
|
||||||
headers.find(
|
|
||||||
(h) =>
|
|
||||||
isDefined(h.variableId) &&
|
|
||||||
h.variableId ===
|
|
||||||
variables.find(byId(inputStep.options.variableId))?.id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return headers
|
|
||||||
const matchedVariableName =
|
|
||||||
inputStep.options.variableId &&
|
|
||||||
variables.find(byId(inputStep.options.variableId))?.name
|
|
||||||
|
|
||||||
let label = matchedVariableName ?? inputStep.blockTitle
|
|
||||||
const totalPrevious = headers.filter((h) => h.label.includes(label)).length
|
|
||||||
if (totalPrevious > 0) label = label + ` (${totalPrevious})`
|
|
||||||
return [
|
|
||||||
...headers,
|
|
||||||
{
|
|
||||||
stepType: inputStep.type,
|
|
||||||
stepId: inputStep.id,
|
|
||||||
variableId: inputStep.options.variableId,
|
|
||||||
label,
|
|
||||||
isLong: 'isLong' in inputStep.options && inputStep.options.isLong,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const parseVariablesHeaders = (
|
|
||||||
variables: Variable[],
|
|
||||||
stepResultHeader: ResultHeaderCell[]
|
|
||||||
) =>
|
|
||||||
variables.reduce<ResultHeaderCell[]>((headers, v) => {
|
|
||||||
if (stepResultHeader.find((h) => h.variableId === v.id)) return headers
|
|
||||||
return [
|
|
||||||
...headers,
|
|
||||||
{
|
|
||||||
label: v.name,
|
|
||||||
variableId: v.id,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
export const convertResultsToTableData = (
|
export const convertResultsToTableData = (
|
||||||
results: ResultWithAnswers[] | undefined,
|
results: ResultWithAnswers[] | undefined,
|
||||||
header: ResultHeaderCell[]
|
header: ResultHeaderCell[]
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import prisma from 'libs/prisma'
|
import prisma from 'libs/prisma'
|
||||||
import { ResultWithAnswers, Typebot, VariableWithValue } from 'models'
|
import { ResultWithAnswers, VariableWithValue } from 'models'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { authenticateUser } from 'services/api/utils'
|
import { authenticateUser } from 'services/api/utils'
|
||||||
import { methodNotAllowed, parseAnswers } from 'utils'
|
import { methodNotAllowed } from 'utils'
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
@@ -20,9 +20,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
take: limit,
|
take: limit,
|
||||||
include: { answers: true },
|
include: { answers: true },
|
||||||
})) as unknown as ResultWithAnswers[]
|
})) as unknown as ResultWithAnswers[]
|
||||||
return res.send({
|
return res.send({ results })
|
||||||
results: results.map(parseAnswers(typebot as unknown as Typebot)),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const typebotId = req.query.typebotId as string
|
const typebotId = req.query.typebotId as string
|
||||||
|
|||||||
@@ -14,13 +14,15 @@ test('Result should be in storage by default', async ({ page }) => {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
await page.goto(`/${typebotId}-public`)
|
await Promise.all([
|
||||||
await page.waitForResponse(
|
page.goto(`/${typebotId}-public`),
|
||||||
(resp) =>
|
page.waitForResponse(
|
||||||
resp.request().url().includes(`/api/typebots/${typebotId}/results`) &&
|
(resp) =>
|
||||||
resp.status() === 200 &&
|
resp.request().url().includes(`/api/typebots/${typebotId}/results`) &&
|
||||||
resp.request().method() === 'POST'
|
resp.status() === 200 &&
|
||||||
)
|
resp.request().method() === 'POST'
|
||||||
|
),
|
||||||
|
])
|
||||||
await page.reload()
|
await page.reload()
|
||||||
const resultId = await page.evaluate(() => sessionStorage.getItem('resultId'))
|
const resultId = await page.evaluate(() => sessionStorage.getItem('resultId'))
|
||||||
expect(resultId).toBeDefined()
|
expect(resultId).toBeDefined()
|
||||||
@@ -45,20 +47,24 @@ test.describe('Create result on page refresh enabled', () => {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
await page.goto(`/${typebotId}-public`)
|
await Promise.all([
|
||||||
await page.waitForResponse(
|
page.goto(`/${typebotId}-public`),
|
||||||
(resp) =>
|
page.waitForResponse(
|
||||||
resp.request().url().includes(`/api/typebots/${typebotId}/results`) &&
|
(resp) =>
|
||||||
resp.status() === 200 &&
|
resp.request().url().includes(`/api/typebots/${typebotId}/results`) &&
|
||||||
resp.request().method() === 'POST'
|
resp.status() === 200 &&
|
||||||
)
|
resp.request().method() === 'POST'
|
||||||
await page.reload()
|
),
|
||||||
await page.waitForResponse(
|
])
|
||||||
(resp) =>
|
await Promise.all([
|
||||||
resp.request().url().includes(`/api/typebots/${typebotId}/results`) &&
|
page.reload(),
|
||||||
resp.status() === 200 &&
|
page.waitForResponse(
|
||||||
resp.request().method() === 'POST'
|
(resp) =>
|
||||||
)
|
resp.request().url().includes(`/api/typebots/${typebotId}/results`) &&
|
||||||
|
resp.status() === 200 &&
|
||||||
|
resp.request().method() === 'POST'
|
||||||
|
),
|
||||||
|
])
|
||||||
const resultId = await page.evaluate(() =>
|
const resultId = await page.evaluate(() =>
|
||||||
sessionStorage.getItem('resultId')
|
sessionStorage.getItem('resultId')
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,39 +1,46 @@
|
|||||||
import { Block, InputStep, InputStepType, PublicTypebot, Typebot } from 'models'
|
import {
|
||||||
import { isInputStep, byId, isDefined } from 'utils'
|
InputStep,
|
||||||
|
InputStepType,
|
||||||
|
PublicTypebot,
|
||||||
|
ResultHeaderCell,
|
||||||
|
Typebot,
|
||||||
|
} from 'models'
|
||||||
|
import { isInputStep, byId, parseResultHeader, isNotDefined } from 'utils'
|
||||||
|
|
||||||
export const parseSampleResult =
|
export const parseSampleResult =
|
||||||
(typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>) =>
|
(typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>) =>
|
||||||
(currentBlockId: string): Record<string, string> => {
|
(currentBlockId: string): Record<string, string> => {
|
||||||
const previousBlocks = (typebot.blocks as Block[]).filter((b) =>
|
const header = parseResultHeader(typebot)
|
||||||
getPreviousBlockIds(typebot)(currentBlockId).includes(b.id)
|
const previousInputSteps = getPreviousInputSteps(typebot)({
|
||||||
)
|
blockId: currentBlockId,
|
||||||
const parsedBlocks = parseBlocksResultSample(typebot, previousBlocks)
|
})
|
||||||
return {
|
return {
|
||||||
message: 'This is a sample result, it has been generated ⬇️',
|
message: 'This is a sample result, it has been generated ⬇️',
|
||||||
'Submitted at': new Date().toISOString(),
|
'Submitted at': new Date().toISOString(),
|
||||||
...parsedBlocks,
|
...parseBlocksResultSample(previousInputSteps, header),
|
||||||
...parseVariablesHeaders(typebot, parsedBlocks),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseBlocksResultSample = (
|
const parseBlocksResultSample = (
|
||||||
typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables'>,
|
inputSteps: InputStep[],
|
||||||
blocks: Block[]
|
header: ResultHeaderCell[]
|
||||||
) =>
|
) =>
|
||||||
blocks
|
header.reduce<Record<string, string>>((steps, cell) => {
|
||||||
.filter((block) => typebot && block.steps.some((step) => isInputStep(step)))
|
const inputStep = inputSteps.find((step) => step.id === cell.stepId)
|
||||||
.reduce<Record<string, string>>((blocks, block) => {
|
if (isNotDefined(inputStep)) {
|
||||||
const inputStep = block.steps.find((step) => isInputStep(step))
|
if (cell.variableId)
|
||||||
if (!inputStep || !isInputStep(inputStep)) return blocks
|
return {
|
||||||
const matchedVariableName =
|
...steps,
|
||||||
inputStep.options.variableId &&
|
[cell.label]: 'content',
|
||||||
typebot.variables.find(byId(inputStep.options.variableId))?.name
|
}
|
||||||
const value = getSampleValue(inputStep)
|
return steps
|
||||||
return {
|
}
|
||||||
...blocks,
|
const value = getSampleValue(inputStep)
|
||||||
[matchedVariableName ?? block.title]: value,
|
return {
|
||||||
}
|
...steps,
|
||||||
}, {})
|
[cell.label]: value,
|
||||||
|
}
|
||||||
|
}, {})
|
||||||
|
|
||||||
const getSampleValue = (step: InputStep) => {
|
const getSampleValue = (step: InputStep) => {
|
||||||
switch (step.type) {
|
switch (step.type) {
|
||||||
@@ -56,27 +63,46 @@ const getSampleValue = (step: InputStep) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseVariablesHeaders = (
|
const getPreviousInputSteps =
|
||||||
typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables'>,
|
(typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>) =>
|
||||||
parsedBlocks: Record<string, string>
|
({ blockId, stepId }: { blockId: string; stepId?: string }): InputStep[] => {
|
||||||
) =>
|
const previousInputSteps = getPreviousInputStepsInBlock(typebot)({
|
||||||
typebot.variables.reduce<Record<string, string>>((headers, v) => {
|
blockId,
|
||||||
if (parsedBlocks[v.name]) return headers
|
stepId,
|
||||||
return {
|
})
|
||||||
...headers,
|
const previousBlockIds = getPreviousBlockIds(typebot)(blockId)
|
||||||
[v.name]: 'value',
|
return [
|
||||||
}
|
...previousInputSteps,
|
||||||
}, {})
|
...previousBlockIds.flatMap((blockId) =>
|
||||||
|
getPreviousInputSteps(typebot)({ blockId })
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
const getPreviousBlockIds =
|
const getPreviousBlockIds =
|
||||||
(typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>) =>
|
(typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>) =>
|
||||||
(blockId: string): string[] => {
|
(blockId: string): string[] => {
|
||||||
const previousBlocks = typebot.edges
|
const previousBlocks = typebot.edges.reduce<string[]>(
|
||||||
.map((edge) =>
|
(blockIds, edge) =>
|
||||||
edge.to.blockId === blockId ? edge.from.blockId : undefined
|
edge.to.blockId === blockId
|
||||||
)
|
? [...blockIds, edge.from.blockId]
|
||||||
.filter(isDefined)
|
: blockIds,
|
||||||
|
[]
|
||||||
|
)
|
||||||
return previousBlocks.concat(
|
return previousBlocks.concat(
|
||||||
previousBlocks.flatMap(getPreviousBlockIds(typebot))
|
previousBlocks.flatMap(getPreviousBlockIds(typebot))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPreviousInputStepsInBlock =
|
||||||
|
(typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>) =>
|
||||||
|
({ blockId, stepId }: { blockId: string; stepId?: string }) => {
|
||||||
|
const currentBlock = typebot.blocks.find(byId(blockId))
|
||||||
|
if (!currentBlock) return []
|
||||||
|
const inputSteps: InputStep[] = []
|
||||||
|
for (const step of currentBlock.steps) {
|
||||||
|
if (step.id === stepId) break
|
||||||
|
if (isInputStep(step)) inputSteps.push(step)
|
||||||
|
}
|
||||||
|
return inputSteps
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Result as ResultFromPrisma } from 'db'
|
import { Result as ResultFromPrisma } from 'db'
|
||||||
import { Answer, VariableWithValue } from '.'
|
import { Answer, InputStepType, VariableWithValue } from '.'
|
||||||
|
|
||||||
export type Result = Omit<
|
export type Result = Omit<
|
||||||
ResultFromPrisma,
|
ResultFromPrisma,
|
||||||
@@ -12,3 +12,11 @@ export type ResultValues = Pick<
|
|||||||
ResultWithAnswers,
|
ResultWithAnswers,
|
||||||
'answers' | 'createdAt' | 'prefilledVariables'
|
'answers' | 'createdAt' | 'prefilledVariables'
|
||||||
>
|
>
|
||||||
|
|
||||||
|
export type ResultHeaderCell = {
|
||||||
|
label: string
|
||||||
|
stepId?: string
|
||||||
|
stepType?: InputStepType
|
||||||
|
isLong?: boolean
|
||||||
|
variableId?: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import {
|
|||||||
VariableWithValue,
|
VariableWithValue,
|
||||||
ResultWithAnswers,
|
ResultWithAnswers,
|
||||||
PublicTypebot,
|
PublicTypebot,
|
||||||
Block,
|
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { byId, isDefined } from './utils'
|
import { parseResultHeader } from './results'
|
||||||
|
import { isDefined } from './utils'
|
||||||
|
|
||||||
export const methodNotAllowed = (res: NextApiResponse) =>
|
export const methodNotAllowed = (res: NextApiResponse) =>
|
||||||
res.status(405).json({ message: 'Method Not Allowed' })
|
res.status(405).json({ message: 'Method Not Allowed' })
|
||||||
@@ -41,37 +41,3 @@ export const initMiddleware =
|
|||||||
return resolve(result)
|
return resolve(result)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
export const parseAnswers =
|
|
||||||
({
|
|
||||||
blocks,
|
|
||||||
variables,
|
|
||||||
}: Pick<Typebot | PublicTypebot, 'blocks' | 'variables'>) =>
|
|
||||||
({
|
|
||||||
createdAt,
|
|
||||||
answers,
|
|
||||||
prefilledVariables,
|
|
||||||
}: Pick<
|
|
||||||
ResultWithAnswers,
|
|
||||||
'createdAt' | 'answers' | 'prefilledVariables'
|
|
||||||
>) => ({
|
|
||||||
submittedAt: createdAt,
|
|
||||||
...[...answers, ...prefilledVariables].reduce<{
|
|
||||||
[key: string]: string
|
|
||||||
}>((o, answerOrVariable) => {
|
|
||||||
if ('blockId' in answerOrVariable) {
|
|
||||||
const answer = answerOrVariable as Answer
|
|
||||||
const key = answer.variableId
|
|
||||||
? variables.find(byId(answer.variableId))?.name
|
|
||||||
: (blocks as Block[]).find(byId(answer.blockId))?.title
|
|
||||||
if (!key) return o
|
|
||||||
return {
|
|
||||||
...o,
|
|
||||||
[key]: answer.content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const variable = answerOrVariable as VariableWithValue
|
|
||||||
if (isDefined(o[variable.id])) return o
|
|
||||||
return { ...o, [variable.id]: variable.value }
|
|
||||||
}, {}),
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './utils'
|
export * from './utils'
|
||||||
export * from './apiUtils'
|
export * from './apiUtils'
|
||||||
export * from './encryption'
|
export * from './encryption'
|
||||||
|
export * from './results'
|
||||||
|
|||||||
121
packages/utils/src/results.ts
Normal file
121
packages/utils/src/results.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import {
|
||||||
|
Block,
|
||||||
|
Variable,
|
||||||
|
InputStep,
|
||||||
|
ResultHeaderCell,
|
||||||
|
ResultWithAnswers,
|
||||||
|
Answer,
|
||||||
|
VariableWithValue,
|
||||||
|
} from 'models'
|
||||||
|
import { isInputStep, isDefined, byId } from './utils'
|
||||||
|
|
||||||
|
export const parseResultHeader = ({
|
||||||
|
blocks,
|
||||||
|
variables,
|
||||||
|
}: {
|
||||||
|
blocks: Block[]
|
||||||
|
variables: Variable[]
|
||||||
|
}): ResultHeaderCell[] => {
|
||||||
|
const parsedBlocks = parseInputsResultHeader({ blocks, variables })
|
||||||
|
return [
|
||||||
|
{ label: 'Submitted at' },
|
||||||
|
...parsedBlocks,
|
||||||
|
...parseVariablesHeaders(variables, parsedBlocks),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseInputsResultHeader = ({
|
||||||
|
blocks,
|
||||||
|
variables,
|
||||||
|
}: {
|
||||||
|
blocks: Block[]
|
||||||
|
variables: Variable[]
|
||||||
|
}): ResultHeaderCell[] =>
|
||||||
|
(
|
||||||
|
blocks
|
||||||
|
.flatMap((b) =>
|
||||||
|
b.steps.map((s) => ({
|
||||||
|
...s,
|
||||||
|
blockTitle: b.title,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.filter((step) => isInputStep(step)) as (InputStep & {
|
||||||
|
blockTitle: string
|
||||||
|
})[]
|
||||||
|
).reduce<ResultHeaderCell[]>((headers, inputStep) => {
|
||||||
|
if (
|
||||||
|
headers.find(
|
||||||
|
(h) =>
|
||||||
|
isDefined(h.variableId) &&
|
||||||
|
h.variableId ===
|
||||||
|
variables.find(byId(inputStep.options.variableId))?.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return headers
|
||||||
|
const matchedVariableName =
|
||||||
|
inputStep.options.variableId &&
|
||||||
|
variables.find(byId(inputStep.options.variableId))?.name
|
||||||
|
|
||||||
|
let label = matchedVariableName ?? inputStep.blockTitle
|
||||||
|
const totalPrevious = headers.filter((h) => h.label.includes(label)).length
|
||||||
|
if (totalPrevious > 0) label = label + ` (${totalPrevious})`
|
||||||
|
return [
|
||||||
|
...headers,
|
||||||
|
{
|
||||||
|
stepType: inputStep.type,
|
||||||
|
stepId: inputStep.id,
|
||||||
|
variableId: inputStep.options.variableId,
|
||||||
|
label,
|
||||||
|
isLong: 'isLong' in inputStep.options && inputStep.options.isLong,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const parseVariablesHeaders = (
|
||||||
|
variables: Variable[],
|
||||||
|
stepResultHeader: ResultHeaderCell[]
|
||||||
|
) =>
|
||||||
|
variables.reduce<ResultHeaderCell[]>((headers, v) => {
|
||||||
|
if (stepResultHeader.find((h) => h.variableId === v.id)) return headers
|
||||||
|
return [
|
||||||
|
...headers,
|
||||||
|
{
|
||||||
|
label: v.name,
|
||||||
|
variableId: v.id,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
export const parseAnswers =
|
||||||
|
({ blocks, variables }: { blocks: Block[]; variables: Variable[] }) =>
|
||||||
|
({
|
||||||
|
createdAt,
|
||||||
|
answers,
|
||||||
|
prefilledVariables,
|
||||||
|
}: Pick<ResultWithAnswers, 'createdAt' | 'answers' | 'prefilledVariables'>): {
|
||||||
|
[key: string]: string
|
||||||
|
} => {
|
||||||
|
const header = parseResultHeader({ blocks, variables })
|
||||||
|
return {
|
||||||
|
submittedAt: createdAt,
|
||||||
|
...[...answers, ...prefilledVariables].reduce<{
|
||||||
|
[key: string]: string
|
||||||
|
}>((o, answerOrVariable) => {
|
||||||
|
if ('blockId' in answerOrVariable) {
|
||||||
|
const answer = answerOrVariable as Answer
|
||||||
|
const key = answer.variableId
|
||||||
|
? header.find((cell) => cell.variableId === answer.variableId)
|
||||||
|
?.label
|
||||||
|
: header.find((cell) => cell.stepId === answer.stepId)?.label
|
||||||
|
if (!key) return o
|
||||||
|
return {
|
||||||
|
...o,
|
||||||
|
[key]: answer.content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const variable = answerOrVariable as VariableWithValue
|
||||||
|
if (isDefined(o[variable.id])) return o
|
||||||
|
return { ...o, [variable.id]: variable.value }
|
||||||
|
}, {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user