diff --git a/apps/builder/components/results/SubmissionsTable/SubmissionsTable.tsx b/apps/builder/components/results/SubmissionsTable/SubmissionsTable.tsx
index 02b0ed0f2ef..6b87d9c7921 100644
--- a/apps/builder/components/results/SubmissionsTable/SubmissionsTable.tsx
+++ b/apps/builder/components/results/SubmissionsTable/SubmissionsTable.tsx
@@ -2,9 +2,10 @@
/* eslint-disable react/jsx-key */
import { Button, chakra, Checkbox, Flex, HStack, Text } from '@chakra-ui/react'
import { AlignLeftTextIcon } from 'assets/icons'
+import { ResultHeaderCell } from 'models'
import React, { useEffect, useMemo, useRef } from 'react'
import { Hooks, useRowSelect, useTable } from 'react-table'
-import { parseSubmissionsColumns, ResultHeaderCell } from 'services/typebots'
+import { parseSubmissionsColumns } from 'services/typebots'
import { LoadingRows } from './LoadingRows'
type SubmissionsTableProps = {
diff --git a/apps/builder/layouts/results/SubmissionContent.tsx b/apps/builder/layouts/results/SubmissionContent.tsx
index cb6e83959aa..d81d84ea3ca 100644
--- a/apps/builder/layouts/results/SubmissionContent.tsx
+++ b/apps/builder/layouts/results/SubmissionContent.tsx
@@ -7,14 +7,13 @@ import {
deleteAllResults,
deleteResults,
getAllResults,
- parseResultHeader,
useResults,
} from 'services/typebots'
import { unparse } from 'papaparse'
import { UnlockProPlanInfo } from 'components/shared/Info'
import { LogsModal } from './LogsModal'
import { useTypebot } from 'contexts/TypebotContext'
-import { isDefined } from 'utils'
+import { isDefined, parseResultHeader } from 'utils'
type Props = {
typebotId: string
diff --git a/apps/builder/playwright/fixtures/typebots/integrations/easyConfigWebhook.json b/apps/builder/playwright/fixtures/typebots/integrations/easyConfigWebhook.json
new file mode 100644
index 00000000000..52e41f969d3
--- /dev/null
+++ b/apps/builder/playwright/fixtures/typebots/integrations/easyConfigWebhook.json
@@ -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": "
Hi how are you?
",
+ "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": "How old are you?
",
+ "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": "Do you like cookies?
",
+ "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": "Alright, cheers!
",
+ "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
+}
diff --git a/apps/builder/playwright/tests/integrations/webhook.spec.ts b/apps/builder/playwright/tests/integrations/webhook.spec.ts
index 795c7ebe8bd..699b2d932dd 100644
--- a/apps/builder/playwright/tests/integrations/webhook.spec.ts
+++ b/apps/builder/playwright/tests/integrations/webhook.spec.ts
@@ -8,7 +8,10 @@ test.describe('Webhook step', () => {
test('easy configuration should work', async ({ page }) => {
const typebotId = cuid()
await importTypebotInDatabase(
- path.join(__dirname, '../../fixtures/typebots/integrations/webhook.json'),
+ path.join(
+ __dirname,
+ '../../fixtures/typebots/integrations/easyConfigWebhook.json'
+ ),
{
id: typebotId,
}
@@ -22,7 +25,7 @@ test.describe('Webhook step', () => {
)
await page.click('text=Test the request')
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 }) => {
diff --git a/apps/builder/services/typebots/results.tsx b/apps/builder/services/typebots/results.tsx
index d8e3aa9eefa..b53219d8f42 100644
--- a/apps/builder/services/typebots/results.tsx
+++ b/apps/builder/services/typebots/results.tsx
@@ -1,15 +1,8 @@
-import {
- Block,
- InputStep,
- InputStepType,
- ResultWithAnswers,
- Variable,
- VariableWithValue,
-} from 'models'
+import { ResultWithAnswers, VariableWithValue, ResultHeaderCell } from 'models'
import useSWRInfinite from 'swr/infinite'
import { stringify } from 'qs'
import { Answer } from 'db'
-import { byId, isDefined, isInputStep, sendRequest } from 'utils'
+import { isDefined, sendRequest } from 'utils'
import { fetcher } from 'services/utils'
import { HStack, Text } from '@chakra-ui/react'
import { CodeIcon, CalendarIcon } from 'assets/icons'
@@ -110,14 +103,6 @@ type HeaderCell = {
accessor: string
}
-export type ResultHeaderCell = {
- label: string
- stepId?: string
- stepType?: InputStepType
- isLong?: boolean
- variableId?: string
-}
-
export const parseSubmissionsColumns = (
resultHeader: ResultHeaderCell[]
): HeaderCell[] =>
@@ -140,83 +125,6 @@ const HeaderIcon = ({ header }: { header: ResultHeaderCell }) =>
)
-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((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((headers, v) => {
- if (stepResultHeader.find((h) => h.variableId === v.id)) return headers
- return [
- ...headers,
- {
- label: v.name,
- variableId: v.id,
- },
- ]
- }, [])
-
export const convertResultsToTableData = (
results: ResultWithAnswers[] | undefined,
header: ResultHeaderCell[]
diff --git a/apps/viewer/pages/api/typebots/[typebotId]/results.ts b/apps/viewer/pages/api/typebots/[typebotId]/results.ts
index 732eb3020cd..1fdc7b90064 100644
--- a/apps/viewer/pages/api/typebots/[typebotId]/results.ts
+++ b/apps/viewer/pages/api/typebots/[typebotId]/results.ts
@@ -1,8 +1,8 @@
import prisma from 'libs/prisma'
-import { ResultWithAnswers, Typebot, VariableWithValue } from 'models'
+import { ResultWithAnswers, VariableWithValue } from 'models'
import { NextApiRequest, NextApiResponse } from 'next'
import { authenticateUser } from 'services/api/utils'
-import { methodNotAllowed, parseAnswers } from 'utils'
+import { methodNotAllowed } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
@@ -20,9 +20,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
take: limit,
include: { answers: true },
})) as unknown as ResultWithAnswers[]
- return res.send({
- results: results.map(parseAnswers(typebot as unknown as Typebot)),
- })
+ return res.send({ results })
}
if (req.method === 'POST') {
const typebotId = req.query.typebotId as string
diff --git a/apps/viewer/playwright/tests/settings.spec.ts b/apps/viewer/playwright/tests/settings.spec.ts
index 53f3b8722c6..c9dce76f7d7 100644
--- a/apps/viewer/playwright/tests/settings.spec.ts
+++ b/apps/viewer/playwright/tests/settings.spec.ts
@@ -14,13 +14,15 @@ test('Result should be in storage by default', async ({ page }) => {
}),
},
])
- await page.goto(`/${typebotId}-public`)
- await page.waitForResponse(
- (resp) =>
- resp.request().url().includes(`/api/typebots/${typebotId}/results`) &&
- resp.status() === 200 &&
- resp.request().method() === 'POST'
- )
+ await Promise.all([
+ page.goto(`/${typebotId}-public`),
+ page.waitForResponse(
+ (resp) =>
+ resp.request().url().includes(`/api/typebots/${typebotId}/results`) &&
+ resp.status() === 200 &&
+ resp.request().method() === 'POST'
+ ),
+ ])
await page.reload()
const resultId = await page.evaluate(() => sessionStorage.getItem('resultId'))
expect(resultId).toBeDefined()
@@ -45,20 +47,24 @@ test.describe('Create result on page refresh enabled', () => {
}),
},
])
- await page.goto(`/${typebotId}-public`)
- await page.waitForResponse(
- (resp) =>
- resp.request().url().includes(`/api/typebots/${typebotId}/results`) &&
- resp.status() === 200 &&
- resp.request().method() === 'POST'
- )
- await page.reload()
- await page.waitForResponse(
- (resp) =>
- resp.request().url().includes(`/api/typebots/${typebotId}/results`) &&
- resp.status() === 200 &&
- resp.request().method() === 'POST'
- )
+ await Promise.all([
+ page.goto(`/${typebotId}-public`),
+ page.waitForResponse(
+ (resp) =>
+ resp.request().url().includes(`/api/typebots/${typebotId}/results`) &&
+ resp.status() === 200 &&
+ resp.request().method() === 'POST'
+ ),
+ ])
+ await Promise.all([
+ page.reload(),
+ page.waitForResponse(
+ (resp) =>
+ resp.request().url().includes(`/api/typebots/${typebotId}/results`) &&
+ resp.status() === 200 &&
+ resp.request().method() === 'POST'
+ ),
+ ])
const resultId = await page.evaluate(() =>
sessionStorage.getItem('resultId')
)
diff --git a/apps/viewer/services/api/webhooks.ts b/apps/viewer/services/api/webhooks.ts
index f55f92631e4..feab566debd 100644
--- a/apps/viewer/services/api/webhooks.ts
+++ b/apps/viewer/services/api/webhooks.ts
@@ -1,39 +1,46 @@
-import { Block, InputStep, InputStepType, PublicTypebot, Typebot } from 'models'
-import { isInputStep, byId, isDefined } from 'utils'
+import {
+ InputStep,
+ InputStepType,
+ PublicTypebot,
+ ResultHeaderCell,
+ Typebot,
+} from 'models'
+import { isInputStep, byId, parseResultHeader, isNotDefined } from 'utils'
export const parseSampleResult =
(typebot: Pick) =>
(currentBlockId: string): Record => {
- const previousBlocks = (typebot.blocks as Block[]).filter((b) =>
- getPreviousBlockIds(typebot)(currentBlockId).includes(b.id)
- )
- const parsedBlocks = parseBlocksResultSample(typebot, previousBlocks)
+ const header = parseResultHeader(typebot)
+ const previousInputSteps = getPreviousInputSteps(typebot)({
+ blockId: currentBlockId,
+ })
return {
message: 'This is a sample result, it has been generated ⬇️',
'Submitted at': new Date().toISOString(),
- ...parsedBlocks,
- ...parseVariablesHeaders(typebot, parsedBlocks),
+ ...parseBlocksResultSample(previousInputSteps, header),
}
}
const parseBlocksResultSample = (
- typebot: Pick,
- blocks: Block[]
+ inputSteps: InputStep[],
+ header: ResultHeaderCell[]
) =>
- blocks
- .filter((block) => typebot && block.steps.some((step) => isInputStep(step)))
- .reduce>((blocks, block) => {
- const inputStep = block.steps.find((step) => isInputStep(step))
- if (!inputStep || !isInputStep(inputStep)) return blocks
- const matchedVariableName =
- inputStep.options.variableId &&
- typebot.variables.find(byId(inputStep.options.variableId))?.name
- const value = getSampleValue(inputStep)
- return {
- ...blocks,
- [matchedVariableName ?? block.title]: value,
- }
- }, {})
+ header.reduce>((steps, cell) => {
+ const inputStep = inputSteps.find((step) => step.id === cell.stepId)
+ if (isNotDefined(inputStep)) {
+ if (cell.variableId)
+ return {
+ ...steps,
+ [cell.label]: 'content',
+ }
+ return steps
+ }
+ const value = getSampleValue(inputStep)
+ return {
+ ...steps,
+ [cell.label]: value,
+ }
+ }, {})
const getSampleValue = (step: InputStep) => {
switch (step.type) {
@@ -56,27 +63,46 @@ const getSampleValue = (step: InputStep) => {
}
}
-const parseVariablesHeaders = (
- typebot: Pick,
- parsedBlocks: Record
-) =>
- typebot.variables.reduce>((headers, v) => {
- if (parsedBlocks[v.name]) return headers
- return {
- ...headers,
- [v.name]: 'value',
- }
- }, {})
+const getPreviousInputSteps =
+ (typebot: Pick) =>
+ ({ blockId, stepId }: { blockId: string; stepId?: string }): InputStep[] => {
+ const previousInputSteps = getPreviousInputStepsInBlock(typebot)({
+ blockId,
+ stepId,
+ })
+ const previousBlockIds = getPreviousBlockIds(typebot)(blockId)
+ return [
+ ...previousInputSteps,
+ ...previousBlockIds.flatMap((blockId) =>
+ getPreviousInputSteps(typebot)({ blockId })
+ ),
+ ]
+ }
const getPreviousBlockIds =
(typebot: Pick) =>
(blockId: string): string[] => {
- const previousBlocks = typebot.edges
- .map((edge) =>
- edge.to.blockId === blockId ? edge.from.blockId : undefined
- )
- .filter(isDefined)
+ const previousBlocks = typebot.edges.reduce(
+ (blockIds, edge) =>
+ edge.to.blockId === blockId
+ ? [...blockIds, edge.from.blockId]
+ : blockIds,
+ []
+ )
return previousBlocks.concat(
previousBlocks.flatMap(getPreviousBlockIds(typebot))
)
}
+
+const getPreviousInputStepsInBlock =
+ (typebot: Pick) =>
+ ({ 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
+ }
diff --git a/packages/models/src/result.ts b/packages/models/src/result.ts
index 71a82248f67..8acd6c79924 100644
--- a/packages/models/src/result.ts
+++ b/packages/models/src/result.ts
@@ -1,5 +1,5 @@
import { Result as ResultFromPrisma } from 'db'
-import { Answer, VariableWithValue } from '.'
+import { Answer, InputStepType, VariableWithValue } from '.'
export type Result = Omit<
ResultFromPrisma,
@@ -12,3 +12,11 @@ export type ResultValues = Pick<
ResultWithAnswers,
'answers' | 'createdAt' | 'prefilledVariables'
>
+
+export type ResultHeaderCell = {
+ label: string
+ stepId?: string
+ stepType?: InputStepType
+ isLong?: boolean
+ variableId?: string
+}
diff --git a/packages/utils/src/apiUtils.ts b/packages/utils/src/apiUtils.ts
index d401b652830..eaf1c2ed0dc 100644
--- a/packages/utils/src/apiUtils.ts
+++ b/packages/utils/src/apiUtils.ts
@@ -4,10 +4,10 @@ import {
VariableWithValue,
ResultWithAnswers,
PublicTypebot,
- Block,
} from 'models'
import { NextApiRequest, NextApiResponse } from 'next'
-import { byId, isDefined } from './utils'
+import { parseResultHeader } from './results'
+import { isDefined } from './utils'
export const methodNotAllowed = (res: NextApiResponse) =>
res.status(405).json({ message: 'Method Not Allowed' })
@@ -41,37 +41,3 @@ export const initMiddleware =
return resolve(result)
})
})
-
-export const parseAnswers =
- ({
- blocks,
- variables,
- }: Pick) =>
- ({
- 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 }
- }, {}),
- })
diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
index 5faa967104c..6364cfc41fa 100644
--- a/packages/utils/src/index.ts
+++ b/packages/utils/src/index.ts
@@ -1,3 +1,4 @@
export * from './utils'
export * from './apiUtils'
export * from './encryption'
+export * from './results'
diff --git a/packages/utils/src/results.ts b/packages/utils/src/results.ts
new file mode 100644
index 00000000000..ad83acaddeb
--- /dev/null
+++ b/packages/utils/src/results.ts
@@ -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((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((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): {
+ [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 }
+ }, {}),
+ }
+ }