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 } + }, {}), + } + }