diff --git a/apps/builder/components/results/ResultModal.tsx b/apps/builder/components/results/ResultModal.tsx index 7043d2af13..b5ea29c472 100644 --- a/apps/builder/components/results/ResultModal.tsx +++ b/apps/builder/components/results/ResultModal.tsx @@ -11,8 +11,8 @@ import { } from '@chakra-ui/react' import { useResults } from 'contexts/ResultsProvider' import React from 'react' +import { HeaderIcon } from 'services/typebots/results' import { isDefined } from 'utils' -import { HeaderIcon } from './ResultsTable/ResultsTable' type Props = { resultIdx: number | null diff --git a/apps/builder/components/results/ResultsContent.tsx b/apps/builder/components/results/ResultsContent.tsx index 817eea3a91..505baf1468 100644 --- a/apps/builder/components/results/ResultsContent.tsx +++ b/apps/builder/components/results/ResultsContent.tsx @@ -27,7 +27,7 @@ export const ResultsContent = () => { const handleResultModalClose = () => setExpandedResultIndex(null) const handleLogOpenIndex = (index: number) => () => { - if (!results) return + if (!results[index]) return setInspectingLogsResultId(results[index].id) } diff --git a/apps/builder/components/results/ResultsTable/ColumnsSettingsButton.tsx b/apps/builder/components/results/ResultsTable/ColumnsSettingsButton.tsx index ef43fc97ab..6036954eec 100644 --- a/apps/builder/components/results/ResultsTable/ColumnsSettingsButton.tsx +++ b/apps/builder/components/results/ResultsTable/ColumnsSettingsButton.tsx @@ -15,7 +15,6 @@ import { ToolIcon, EyeIcon, EyeOffIcon, GripIcon } from 'assets/icons' import { ResultHeaderCell } from 'models' import React, { forwardRef, useState } from 'react' import { isNotDefined } from 'utils' -import { HeaderIcon } from './ResultsTable' import { DndContext, closestCenter, @@ -35,6 +34,7 @@ import { useSortable, arrayMove, } from '@dnd-kit/sortable' +import { HeaderIcon } from 'services/typebots/results' type Props = { resultHeader: ResultHeaderCell[] diff --git a/apps/builder/components/results/ResultsTable/ResultsActionButtons.tsx b/apps/builder/components/results/ResultsTable/ResultsActionButtons.tsx index 7bec8f4cbe..5546b3ac3b 100644 --- a/apps/builder/components/results/ResultsTable/ResultsActionButtons.tsx +++ b/apps/builder/components/results/ResultsTable/ResultsActionButtons.tsx @@ -93,18 +93,39 @@ export const ResultsActionButtons = ({ : tableData.filter((data) => selectedResultsId.includes(data.id.plainText) ) + + const fields = typebot?.resultsTablePreferences?.columnsOrder + ? typebot.resultsTablePreferences.columnsOrder.reduce( + (currentHeaderLabels, columnId) => { + if ( + typebot.resultsTablePreferences?.columnsVisibility[columnId] === + false + ) + return currentHeaderLabels + const columnLabel = resultHeader.find( + (headerCell) => headerCell.id === columnId + )?.label + if (!columnLabel) return currentHeaderLabels + return [...currentHeaderLabels, columnLabel] + }, + [] + ) + : resultHeader.map((headerCell) => headerCell.label) + + const data = dataToUnparse.map<{ [key: string]: string }>((data) => { + const newObject: { [key: string]: string } = {} + fields?.forEach((field) => { + console.log(data[field]) + newObject[field] = data[field]?.plainText + }) + return newObject + }) + const csvData = new Blob( [ unparse({ - data: dataToUnparse.map<{ [key: string]: string }>((data) => { - const newObject: { [key: string]: string } = {} - Object.keys(data).forEach((key) => { - newObject[key] = (data[key] as { plainText: string }) - .plainText as string - }) - return newObject - }), - fields: resultHeader.map((h) => h.label), + data, + fields, }), ], { diff --git a/apps/builder/components/results/ResultsTable/ResultsTable.tsx b/apps/builder/components/results/ResultsTable/ResultsTable.tsx index 978ea96f9f..8a8f0261d3 100644 --- a/apps/builder/components/results/ResultsTable/ResultsTable.tsx +++ b/apps/builder/components/results/ResultsTable/ResultsTable.tsx @@ -8,7 +8,7 @@ import { Stack, Text, } from '@chakra-ui/react' -import { AlignLeftTextIcon, CalendarIcon, CodeIcon } from 'assets/icons' +import { AlignLeftTextIcon } from 'assets/icons' import { ResultHeaderCell, ResultsTablePreferences } from 'models' import React, { useEffect, useRef, useState } from 'react' import { LoadingRows } from './LoadingRows' @@ -18,14 +18,13 @@ import { ColumnOrderState, ColumnDef, } from '@tanstack/react-table' -import { BlockIcon } from 'components/editor/BlocksSideBar/BlockIcon' import { ColumnSettingsButton } from './ColumnsSettingsButton' import { useTypebot } from 'contexts/TypebotContext' import { useDebounce } from 'use-debounce' import { ResultsActionButtons } from './ResultsActionButtons' import { Row } from './Row' import { HeaderRow } from './HeaderRow' -import { CellValueType, TableData } from 'services/typebots/results' +import { CellValueType, HeaderIcon, TableData } from 'services/typebots/results' type ResultsTableProps = { resultHeader: ResultHeaderCell[] @@ -112,7 +111,7 @@ export const ResultsTable = ({ ...resultHeader.map>((header) => ({ id: header.id, accessorKey: header.label, - size: header.isLong ? 400 : 200, + size: 200, header: () => ( @@ -147,8 +146,7 @@ export const ResultsTable = ({ ), }, ], - // eslint-disable-next-line react-hooks/exhaustive-deps - [resultHeader] + [onLogOpenIndex, resultHeader] ) const instance = useReactTable({ @@ -258,12 +256,3 @@ const IndeterminateCheckbox = React.forwardRef( ) } ) - -export const HeaderIcon = ({ header }: { header: ResultHeaderCell }) => - header.blockType ? ( - - ) : header.variableId ? ( - - ) : ( - - ) diff --git a/apps/builder/components/share/EditableUrl.tsx b/apps/builder/components/share/EditableUrl.tsx index 9235d5f58b..e82fcebddb 100644 --- a/apps/builder/components/share/EditableUrl.tsx +++ b/apps/builder/components/share/EditableUrl.tsx @@ -29,12 +29,11 @@ export const EditableUrl = ({ const [value, setValue] = useState(pathname) const handleSubmit = (newPathname: string) => { - if (/^[a-z]+(-[a-z]+)*$/.test(newPathname)) - return onPathnameChange(newPathname) + if (/^[a-z0-9-]*$/.test(newPathname)) return onPathnameChange(newPathname) setValue(pathname) showToast({ title: 'Invalid ID', - description: 'Should contain only contain letters and dashes.', + description: 'Should contain only contain letters, numbers and dashes.', }) } diff --git a/apps/builder/contexts/ResultsProvider.tsx b/apps/builder/contexts/ResultsProvider.tsx index 683e0917a9..04bd733cc9 100644 --- a/apps/builder/contexts/ResultsProvider.tsx +++ b/apps/builder/contexts/ResultsProvider.tsx @@ -6,7 +6,7 @@ import { useResults as useFetchResults, } from 'services/typebots/results' import { KeyedMutator } from 'swr' -import { isDefined, parseResultHeader } from 'utils' +import { parseResultHeader } from 'utils' import { useTypebot } from './TypebotContext' const resultsContext = createContext<{ @@ -46,19 +46,17 @@ export const ResultsProvider = ({ typebotId, }) + console.log(data?.flatMap((d) => d.results) ?? []) + const fetchMore = () => setSize((state) => state + 1) - const groupsAndVariables = { - groups: [ - ...(publishedTypebot?.groups ?? []), - ...(linkedTypebots?.flatMap((t) => t.groups) ?? []), - ].filter(isDefined), - variables: [ - ...(publishedTypebot?.variables ?? []), - ...(linkedTypebots?.flatMap((t) => t.variables) ?? []), - ].filter(isDefined), - } - const resultHeader = parseResultHeader(groupsAndVariables) + const resultHeader = useMemo( + () => + publishedTypebot + ? parseResultHeader(publishedTypebot, linkedTypebots) + : [], + [linkedTypebots, publishedTypebot] + ) const tableData = useMemo( () => @@ -68,8 +66,7 @@ export const ResultsProvider = ({ resultHeader ) : [], - // eslint-disable-next-line react-hooks/exhaustive-deps - [publishedTypebot?.id, resultHeader.length, data] + [publishedTypebot, data, resultHeader] ) return ( diff --git a/apps/builder/pages/api/typebots.ts b/apps/builder/pages/api/typebots.ts index 314d27192c..19f7c4a8bd 100644 --- a/apps/builder/pages/api/typebots.ts +++ b/apps/builder/pages/api/typebots.ts @@ -45,7 +45,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { ], }, orderBy: { createdAt: 'desc' }, - select: { name: true, id: true, groups: true }, + select: { name: true, id: true, groups: true, variables: true }, }) return res.send({ typebots }) } diff --git a/apps/builder/playwright/tests/billing.spec.ts b/apps/builder/playwright/tests/billing.spec.ts index b17a508b35..b24ea16328 100644 --- a/apps/builder/playwright/tests/billing.spec.ts +++ b/apps/builder/playwright/tests/billing.spec.ts @@ -228,7 +228,7 @@ test('should display invoices', async ({ page }) => { await page.click('text=Billing & Usage') await expect(page.locator('text="Invoices"')).toBeVisible() await expect(page.locator('tr')).toHaveCount(3) - await expect(page.locator('text="€39.00"')).toBeVisible() + await expect(page.locator('text="$39.00"')).toBeVisible() }) test('custom plans should work', async ({ page }) => { diff --git a/apps/builder/playwright/tests/results.spec.ts b/apps/builder/playwright/tests/results.spec.ts index 531e236cea..373824375c 100644 --- a/apps/builder/playwright/tests/results.spec.ts +++ b/apps/builder/playwright/tests/results.spec.ts @@ -21,7 +21,7 @@ test.beforeEach(async () => { await injectFakeResults({ typebotId, count: 200, isChronological: true }) }) -test('Results', async ({ page }) => { +test('table features should work', async ({ page }) => { await page.goto(`/typebots/${typebotId}/results`) await test.step('Check header format', async () => { @@ -147,14 +147,14 @@ test('Results', async ({ page }) => { const validateExportSelection = (data: unknown[]) => { expect(data).toHaveLength(3) - expect((data[1] as unknown[])[3]).toBe('content199') - expect((data[2] as unknown[])[3]).toBe('content198') + expect((data[1] as unknown[])[0]).toBe('content199') + expect((data[2] as unknown[])[0]).toBe('content198') } const validateExportAll = (data: unknown[]) => { expect(data).toHaveLength(201) - expect((data[1] as unknown[])[3]).toBe('content199') - expect((data[200] as unknown[])[3]).toBe('content0') + expect((data[1] as unknown[])[0]).toBe('content199') + expect((data[200] as unknown[])[0]).toBe('content0') } const scrollToBottom = (page: Page) => diff --git a/apps/builder/services/api/dbRules.ts b/apps/builder/services/api/dbRules.ts index dee5517cc7..5ee657198d 100644 --- a/apps/builder/services/api/dbRules.ts +++ b/apps/builder/services/api/dbRules.ts @@ -73,7 +73,7 @@ export const canPublishFileInput = async ({ return false } if (workspace?.plan === Plan.FREE) { - forbidden(res, 'You need to upgrade your plan to use this feature') + forbidden(res, 'You need to upgrade your plan to use file input blocks') return false } return true diff --git a/apps/builder/services/typebots/results.tsx b/apps/builder/services/typebots/results.tsx index ca6badf61c..ce7db647af 100644 --- a/apps/builder/services/typebots/results.tsx +++ b/apps/builder/services/typebots/results.tsx @@ -136,7 +136,7 @@ export const parseSubmissionsColumns = ( ): HeaderCell[] => resultHeader.map((header) => ({ Header: ( - + {header.label} @@ -144,10 +144,10 @@ export const parseSubmissionsColumns = ( accessor: header.label, })) -const HeaderIcon = ({ header }: { header: ResultHeaderCell }) => +export const HeaderIcon = ({ header }: { header: ResultHeaderCell }) => header.blockType ? ( - ) : header.variableId ? ( + ) : header.variableIds ? ( ) : ( @@ -174,9 +174,13 @@ export const convertResultsToTableData = ( if ('groupId' in answerOrVariable) { const answer = answerOrVariable as Answer const header = answer.variableId - ? headerCells.find((h) => h.variableId === answer.variableId) - : headerCells.find((h) => h.blockId === answer.blockId) - if (!header || !header.blockId || !header.blockType) return o + ? headerCells.find((headerCell) => + headerCell.variableIds?.includes(answer.variableId as string) + ) + : headerCells.find((headerCell) => + headerCell.blocks?.some((block) => block.id === answer.blockId) + ) + if (!header || !header.blocks || !header.blockType) return o return { ...o, [header.label]: { @@ -186,7 +190,9 @@ export const convertResultsToTableData = ( } } const variable = answerOrVariable as VariableWithValue - const key = headerCells.find((h) => h.variableId === variable.id)?.label + const key = headerCells.find((headerCell) => + headerCell.variableIds?.includes(variable.id) + )?.label if (!key) return o if (isDefined(o[key])) return o return { diff --git a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts index 82d8117195..3fd587b7cb 100644 --- a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts +++ b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts @@ -203,16 +203,7 @@ const getBodyContent = return body === '{{state}}' ? JSON.stringify( resultValues - ? parseAnswers({ - groups: [ - ...typebot.groups, - ...linkedTypebots.flatMap((t) => t.groups), - ], - variables: [ - ...typebot.variables, - ...linkedTypebots.flatMap((t) => t.variables), - ], - })(resultValues) + ? parseAnswers(typebot, linkedTypebots)(resultValues) : await parseSampleResult(typebot, linkedTypebots)(groupId) ) : body diff --git a/apps/viewer/pages/api/typebots/[typebotId]/integrations/email.tsx b/apps/viewer/pages/api/typebots/[typebotId]/integrations/email.tsx index 0c343123c9..f6da016f25 100644 --- a/apps/viewer/pages/api/typebots/[typebotId]/integrations/email.tsx +++ b/apps/viewer/pages/api/typebots/[typebotId]/integrations/email.tsx @@ -180,13 +180,7 @@ const getEmailBody = async ({ })) as unknown as PublicTypebot if (!typebot) return const linkedTypebots = await getLinkedTypebots(typebot) - const answers = parseAnswers({ - groups: [...typebot.groups, ...linkedTypebots.flatMap((t) => t.groups)], - variables: [ - ...typebot.variables, - ...linkedTypebots.flatMap((t) => t.variables), - ], - })(resultValues) + const answers = parseAnswers(typebot, linkedTypebots)(resultValues) return { html: render( { const response = await request.get(`/api/typebots`, { headers: { Authorization: `Bearer ${apiToken}` }, }) - const { typebots } = await response.json() - expect(typebots).toHaveLength(1) + const { typebots } = (await response.json()) as { typebots: unknown[] } + expect(typebots.length).toBeGreaterThanOrEqual(1) expect(typebots[0]).toMatchObject({ id: typebotId, publishedTypebotId: null, diff --git a/apps/viewer/services/api/webhooks.ts b/apps/viewer/services/api/webhooks.ts index 17659c672f..e6583d9f27 100644 --- a/apps/viewer/services/api/webhooks.ts +++ b/apps/viewer/services/api/webhooks.ts @@ -18,13 +18,7 @@ export const parseSampleResult = async ( currentGroupId: string ): Promise> => { - const header = parseResultHeader({ - groups: [...typebot.groups, ...linkedTypebots.flatMap((t) => t.groups)], - variables: [ - ...typebot.variables, - ...linkedTypebots.flatMap((t) => t.variables), - ], - }) + const header = parseResultHeader(typebot, linkedTypebots) const linkedInputBlocks = await extractLinkedInputBlocks( typebot, linkedTypebots @@ -33,7 +27,7 @@ export const parseSampleResult = return { message: 'This is a sample result, it has been generated ⬇️', 'Submitted at': new Date().toISOString(), - ...parseGroupsResultSample(linkedInputBlocks, header), + ...parseResultSample(linkedInputBlocks, header), } } @@ -81,24 +75,26 @@ const extractLinkedInputBlocks = ).concat(linkedBotInputs.flatMap((l) => l)) } -const parseGroupsResultSample = ( +const parseResultSample = ( inputBlocks: InputBlock[], - header: ResultHeaderCell[] + headerCells: ResultHeaderCell[] ) => - header.reduce>( - (blocks, cell) => { - const inputBlock = inputBlocks.find((block) => block.id === cell.blockId) + headerCells.reduce>( + (resultSample, cell) => { + const inputBlock = inputBlocks.find((inputBlock) => + cell.blocks?.some((block) => block.id === inputBlock.id) + ) if (isNotDefined(inputBlock)) { - if (cell.variableId) + if (cell.variableIds) return { - ...blocks, + ...resultSample, [cell.label]: 'content', } - return blocks + return resultSample } const value = getSampleValue(inputBlock) return { - ...blocks, + ...resultSample, [cell.label]: value, } }, diff --git a/packages/models/result.ts b/packages/models/result.ts index c07aed4334..04cc93d8ae 100644 --- a/packages/models/result.ts +++ b/packages/models/result.ts @@ -17,8 +17,10 @@ export type ResultValues = Pick< export type ResultHeaderCell = { id: string label: string - blockId?: string + blocks?: { + id: string + groupId: string + }[] blockType?: InputBlockType - isLong?: boolean - variableId?: string + variableIds?: string[] } diff --git a/packages/utils/results.ts b/packages/utils/results.ts index 14ef995d7f..1ac92de790 100644 --- a/packages/utils/results.ts +++ b/packages/utils/results.ts @@ -6,90 +6,173 @@ import { ResultWithAnswers, Answer, VariableWithValue, + Typebot, } from 'models' import { isInputBlock, isDefined, byId } from './utils' -export const parseResultHeader = ({ - groups, - variables, -}: { - groups: Group[] - variables: Variable[] -}): ResultHeaderCell[] => { - const parsedGroups = parseInputsResultHeader({ groups, variables }) +export const parseResultHeader = ( + typebot: Pick, + linkedTypebots: Pick[] | undefined +): ResultHeaderCell[] => { + const parsedGroups = [ + ...typebot.groups, + ...(linkedTypebots ?? []).flatMap((linkedTypebot) => linkedTypebot.groups), + ] + const parsedVariables = [ + ...typebot.variables, + ...(linkedTypebots ?? []).flatMap( + (linkedTypebot) => linkedTypebot.variables + ), + ] + const inputsResultHeader = parseInputsResultHeader({ + groups: parsedGroups, + variables: parsedVariables, + }) return [ { label: 'Submitted at', id: 'date' }, - ...parsedGroups, - ...parseVariablesHeaders(variables, parsedGroups), + ...inputsResultHeader, + ...parseVariablesHeaders(parsedVariables, inputsResultHeader), ] } +type ResultHeaderCellWithBlock = Omit & { + blocks: NonNullable +} + const parseInputsResultHeader = ({ groups, variables, }: { groups: Group[] variables: Variable[] -}): ResultHeaderCell[] => +}): ResultHeaderCellWithBlock[] => ( groups - .flatMap((g) => - g.blocks.map((s) => ({ - ...s, - blockTitle: g.title, + .flatMap((group) => + group.blocks.map((block) => ({ + ...block, + groupTitle: group.title, })) ) .filter((block) => isInputBlock(block)) as (InputBlock & { - blockTitle: string + groupTitle: string })[] - ).reduce((headers, inputBlock) => { + ).reduce((existingHeaders, inputBlock) => { if ( - headers.find( - (h) => - isDefined(h.variableId) && - h.variableId === - variables.find(byId(inputBlock.options.variableId))?.id + existingHeaders.some( + (existingHeader) => + inputBlock.options.variableId && + existingHeader.variableIds?.includes(inputBlock.options.variableId) ) ) - return headers + return existingHeaders const matchedVariableName = inputBlock.options.variableId && variables.find(byId(inputBlock.options.variableId))?.name - let label = matchedVariableName ?? inputBlock.blockTitle - const totalPrevious = headers.filter((h) => h.label.includes(label)).length - if (totalPrevious > 0) label = label + ` (${totalPrevious})` - return [ - ...headers, - { - id: inputBlock.id, - blockType: inputBlock.type, - blockId: inputBlock.id, - variableId: inputBlock.options.variableId, - label, - isLong: 'isLong' in inputBlock.options && inputBlock.options.isLong, - }, - ] + let label = matchedVariableName ?? inputBlock.groupTitle + const existingHeader = existingHeaders.find((h) => h.label === label) + if (existingHeader) { + if ( + existingHeader.blocks?.some( + (block) => block.groupId === inputBlock.groupId + ) + ) { + const totalPrevious = existingHeaders.filter((h) => + h.label.includes(label) + ).length + const newHeaderCell: ResultHeaderCellWithBlock = { + id: inputBlock.id, + label: label + ` (${totalPrevious})`, + blocks: [ + { + id: inputBlock.id, + groupId: inputBlock.groupId, + }, + ], + blockType: inputBlock.type, + variableIds: inputBlock.options.variableId + ? [inputBlock.options.variableId] + : undefined, + } + return [...existingHeaders, newHeaderCell] + } + const updatedHeaderCell: ResultHeaderCellWithBlock = { + ...existingHeader, + variableIds: + existingHeader.variableIds && inputBlock.options.variableId + ? existingHeader.variableIds.concat([inputBlock.options.variableId]) + : undefined, + blocks: existingHeader.blocks.concat({ + id: inputBlock.id, + groupId: inputBlock.groupId, + }), + } + return [ + ...existingHeaders.filter( + (existingHeader) => existingHeader.label !== label + ), + updatedHeaderCell, + ] + } + + const newHeaderCell: ResultHeaderCellWithBlock = { + id: inputBlock.id, + label, + blocks: [ + { + id: inputBlock.id, + groupId: inputBlock.groupId, + }, + ], + blockType: inputBlock.type, + variableIds: inputBlock.options.variableId + ? [inputBlock.options.variableId] + : undefined, + } + + return [...existingHeaders, newHeaderCell] }, []) const parseVariablesHeaders = ( variables: Variable[], - blockResultHeader: ResultHeaderCell[] + existingInputResultHeaders: ResultHeaderCell[] ) => - variables.reduce((headers, v) => { - if (blockResultHeader.find((h) => h.variableId === v.id)) return headers - return [ - ...headers, - { - id: v.id, - label: v.name, - variableId: v.id, - }, - ] + variables.reduce((existingHeaders, variable) => { + if ( + existingInputResultHeaders.some((existingInputResultHeader) => + existingInputResultHeader.variableIds?.includes(variable.id) + ) + ) + return existingHeaders + + const headerCellWithSameLabel = existingHeaders.find( + (existingHeader) => existingHeader.label === variable.name + ) + if (headerCellWithSameLabel) { + const updatedHeaderCell: ResultHeaderCell = { + ...headerCellWithSameLabel, + variableIds: headerCellWithSameLabel.variableIds?.concat([variable.id]), + } + return [ + ...existingHeaders.filter((h) => h.label !== variable.name), + updatedHeaderCell, + ] + } + const newHeaderCell: ResultHeaderCell = { + id: variable.id, + label: variable.name, + variableIds: [variable.id], + } + + return [...existingHeaders, newHeaderCell] }, []) export const parseAnswers = - ({ groups, variables }: { groups: Group[]; variables: Variable[] }) => + ( + typebot: Pick, + linkedTypebots: Pick[] | undefined + ) => ({ createdAt, answers, @@ -97,7 +180,7 @@ export const parseAnswers = }: Pick): { [key: string]: string } => { - const header = parseResultHeader({ groups, variables }) + const header = parseResultHeader(typebot, linkedTypebots) return { submittedAt: createdAt, ...[...answers, ...resultVariables].reduce<{ @@ -106,9 +189,14 @@ export const parseAnswers = 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.blockId === answer.blockId)?.label + ? header.find( + (cell) => + answer.variableId && + cell.variableIds?.includes(answer.variableId) + )?.label + : header.find((cell) => + cell.blocks?.some((block) => block.id === answer.blockId) + )?.label if (!key) return o return { ...o,