diff --git a/apps/builder/src/features/blocks/integrations/webhook/helpers/parseResultExample.ts b/apps/builder/src/features/blocks/integrations/webhook/helpers/parseResultExample.ts index d59bca33c..cd6e5c359 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/helpers/parseResultExample.ts +++ b/apps/builder/src/features/blocks/integrations/webhook/helpers/parseResultExample.ts @@ -7,10 +7,10 @@ import { TypebotLinkBlock, } from '@typebot.io/schemas' import { isInputBlock, byId, isNotDefined } from '@typebot.io/lib' -import { parseResultHeader } from '@typebot.io/lib/results' import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants' import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants' import { EventType } from '@typebot.io/schemas/features/events/constants' +import { parseResultHeader } from '@typebot.io/lib/results/parseResultHeader' export const parseResultExample = ({ diff --git a/apps/builder/src/features/results/ResultsProvider.tsx b/apps/builder/src/features/results/ResultsProvider.tsx index 5064174eb..95c3c7160 100644 --- a/apps/builder/src/features/results/ResultsProvider.tsx +++ b/apps/builder/src/features/results/ResultsProvider.tsx @@ -2,17 +2,18 @@ import { useToast } from '@/hooks/useToast' import { ResultHeaderCell, ResultWithAnswers, + TableData, Typebot, } from '@typebot.io/schemas' import { createContext, ReactNode, useContext, useMemo } from 'react' -import { parseResultHeader } from '@typebot.io/lib/results' import { useTypebot } from '../editor/providers/TypebotProvider' import { useResultsQuery } from './hooks/useResultsQuery' -import { TableData } from './types' -import { convertResultsToTableData } from './helpers/convertResultsToTableData' import { trpc } from '@/lib/trpc' import { isDefined } from '@typebot.io/lib/utils' import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants' +import { parseResultHeader } from '@typebot.io/lib/results/parseResultHeader' +import { convertResultsToTableData } from '@typebot.io/lib/results/convertResultsToTableData' +import { parseCellContent } from './helpers/parseCellContent' const resultsContext = createContext<{ resultsList: { results: ResultWithAnswers[] }[] | undefined @@ -94,7 +95,8 @@ export const ResultsProvider = ({ publishedTypebot ? convertResultsToTableData( data?.flatMap((d) => d.results) ?? [], - resultHeader + resultHeader, + parseCellContent ) : [], [publishedTypebot, data, resultHeader] diff --git a/apps/builder/src/features/results/components/ResultModal.tsx b/apps/builder/src/features/results/components/ResultModal.tsx index 163c04c3e..9abd73b84 100644 --- a/apps/builder/src/features/results/components/ResultModal.tsx +++ b/apps/builder/src/features/results/components/ResultModal.tsx @@ -14,7 +14,7 @@ import React from 'react' import { byId, isDefined } from '@typebot.io/lib' import { HeaderIcon } from './HeaderIcon' import { useTypebot } from '@/features/editor/providers/TypebotProvider' -import { parseColumnOrder } from '../helpers/parseColumnsOrder' +import { parseColumnsOrder } from '@typebot.io/lib/results/parseColumnsOrder' type Props = { resultId: string | null @@ -28,7 +28,7 @@ export const ResultModal = ({ resultId, onClose }: Props) => { ? tableData.find((data) => data.id.plainText === resultId) : undefined - const columnsOrder = parseColumnOrder( + const columnsOrder = parseColumnsOrder( typebot?.resultsTablePreferences?.columnsOrder, resultHeader ) diff --git a/apps/builder/src/features/results/components/table/Cell.tsx b/apps/builder/src/features/results/components/table/Cell.tsx index 148829bfa..7e88e83da 100644 --- a/apps/builder/src/features/results/components/table/Cell.tsx +++ b/apps/builder/src/features/results/components/table/Cell.tsx @@ -2,7 +2,7 @@ import { chakra, Fade, Button, useColorModeValue } from '@chakra-ui/react' import { Cell as CellProps, flexRender } from '@tanstack/react-table' import { ExpandIcon } from '@/components/icons' import { memo } from 'react' -import { TableData } from '../../types' +import { TableData } from '@typebot.io/schemas' type Props = { cell: CellProps diff --git a/apps/builder/src/features/results/components/table/ExportAllResultsModal.tsx b/apps/builder/src/features/results/components/table/ExportAllResultsModal.tsx index 02a56d4b2..178421442 100644 --- a/apps/builder/src/features/results/components/table/ExportAllResultsModal.tsx +++ b/apps/builder/src/features/results/components/table/ExportAllResultsModal.tsx @@ -20,10 +20,11 @@ import { import { TRPCError } from '@trpc/server' import { unparse } from 'papaparse' import { useState } from 'react' -import { parseResultHeader } from '@typebot.io/lib/results' +import { parseResultHeader } from '@typebot.io/lib/results/parseResultHeader' +import { convertResultsToTableData } from '@typebot.io/lib/results/convertResultsToTableData' +import { parseColumnsOrder } from '@typebot.io/lib/results/parseColumnsOrder' +import { parseUniqueKey } from '@typebot.io/lib/parseUniqueKey' import { useResults } from '../../ResultsProvider' -import { parseColumnOrder } from '../../helpers/parseColumnsOrder' -import { convertResultsToTableData } from '../../helpers/convertResultsToTableData' import { byId, isDefined } from '@typebot.io/lib' import { Typebot } from '@typebot.io/schemas' @@ -101,7 +102,7 @@ export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => { const dataToUnparse = convertResultsToTableData(results, resultHeader) - const headerIds = parseColumnOrder( + const headerIds = parseColumnsOrder( typebot?.resultsTablePreferences?.columnsOrder, resultHeader ).reduce((currentHeaderIds, columnId) => { @@ -182,12 +183,3 @@ export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => { ) } - -export const parseUniqueKey = ( - key: string, - existingKeys: string[], - count = 0 -): string => { - if (!existingKeys.includes(key)) return key - return parseUniqueKey(`${key} (${count + 1})`, existingKeys, count + 1) -} diff --git a/apps/builder/src/features/results/components/table/HeaderRow.tsx b/apps/builder/src/features/results/components/table/HeaderRow.tsx index c908ff95f..3ddb7afd2 100644 --- a/apps/builder/src/features/results/components/table/HeaderRow.tsx +++ b/apps/builder/src/features/results/components/table/HeaderRow.tsx @@ -1,8 +1,8 @@ import { colors } from '@/lib/theme' import { Box, BoxProps, chakra, useColorModeValue } from '@chakra-ui/react' import { flexRender, HeaderGroup } from '@tanstack/react-table' +import { TableData } from '@typebot.io/schemas' import React from 'react' -import { TableData } from '../../types' type Props = { headerGroup: HeaderGroup diff --git a/apps/builder/src/features/results/components/table/ResultsTable.tsx b/apps/builder/src/features/results/components/table/ResultsTable.tsx index e4c7aea14..297a2c4dd 100644 --- a/apps/builder/src/features/results/components/table/ResultsTable.tsx +++ b/apps/builder/src/features/results/components/table/ResultsTable.tsx @@ -8,7 +8,12 @@ import { useColorModeValue, } from '@chakra-ui/react' import { AlignLeftTextIcon } from '@/components/icons' -import { ResultHeaderCell, ResultsTablePreferences } from '@typebot.io/schemas' +import { + CellValueType, + ResultHeaderCell, + ResultsTablePreferences, + TableData, +} from '@typebot.io/schemas' import React, { useCallback, useEffect, useRef, useState } from 'react' import { LoadingRows } from './LoadingRows' import { @@ -22,11 +27,10 @@ import { useTypebot } from '@/features/editor/providers/TypebotProvider' import { SelectionToolbar } from './SelectionToolbar' import { Row } from './Row' import { HeaderRow } from './HeaderRow' -import { CellValueType, TableData } from '../../types' import { IndeterminateCheckbox } from './IndeterminateCheckbox' import { colors } from '@/lib/theme' -import { parseColumnOrder } from '../../helpers/parseColumnsOrder' import { HeaderIcon } from '../HeaderIcon' +import { parseColumnsOrder } from '@typebot.io/lib/results/parseColumnsOrder' type ResultsTableProps = { resultHeader: ResultHeaderCell[] @@ -60,7 +64,7 @@ export const ResultsTable = ({ columnsWidth = {}, } = { ...preferences, - columnsOrder: parseColumnOrder(preferences?.columnsOrder, resultHeader), + columnsOrder: parseColumnsOrder(preferences?.columnsOrder, resultHeader), } const changeColumnOrder = (newColumnOrder: string[]) => { diff --git a/apps/builder/src/features/results/components/table/Row.tsx b/apps/builder/src/features/results/components/table/Row.tsx index c5075a9e8..2efcdf3c9 100644 --- a/apps/builder/src/features/results/components/table/Row.tsx +++ b/apps/builder/src/features/results/components/table/Row.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' import { Row as RowProps } from '@tanstack/react-table' import Cell from './Cell' -import { TableData } from '../../types' +import { TableData } from '@typebot.io/schemas' type Props = { row: RowProps diff --git a/apps/builder/src/features/results/components/table/SelectionToolbar.tsx b/apps/builder/src/features/results/components/table/SelectionToolbar.tsx index 19153cbe2..f15cc79e3 100644 --- a/apps/builder/src/features/results/components/table/SelectionToolbar.tsx +++ b/apps/builder/src/features/results/components/table/SelectionToolbar.tsx @@ -14,9 +14,9 @@ import React, { useState } from 'react' import { useToast } from '@/hooks/useToast' import { useResults } from '../../ResultsProvider' import { trpc } from '@/lib/trpc' -import { parseColumnOrder } from '../../helpers/parseColumnsOrder' import { byId } from '@typebot.io/lib/utils' -import { parseUniqueKey } from './ExportAllResultsModal' +import { parseColumnsOrder } from '@typebot.io/lib/results/parseColumnsOrder' +import { parseUniqueKey } from '@typebot.io/lib/parseUniqueKey' type Props = { selectedResultsId: string[] @@ -70,7 +70,7 @@ export const SelectionToolbar = ({ selectedResultsId.includes(data.id.plainText) ) - const headerIds = parseColumnOrder( + const headerIds = parseColumnsOrder( typebot?.resultsTablePreferences?.columnsOrder, resultHeader ) diff --git a/apps/builder/src/features/results/helpers/convertDateToReadable.ts b/apps/builder/src/features/results/helpers/convertDateToReadable.ts deleted file mode 100644 index 45f7fe44e..000000000 --- a/apps/builder/src/features/results/helpers/convertDateToReadable.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const convertDateToReadable = (date: Date): string => - date.toDateString().split(' ').slice(1, 3).join(' ') + - ', ' + - date.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - }) diff --git a/apps/builder/src/features/results/helpers/parseCellContent.tsx b/apps/builder/src/features/results/helpers/parseCellContent.tsx new file mode 100644 index 000000000..e3f84058a --- /dev/null +++ b/apps/builder/src/features/results/helpers/parseCellContent.tsx @@ -0,0 +1,27 @@ +import { Stack, Text } from '@chakra-ui/react' +import { VariableWithValue } from '@typebot.io/schemas' +import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants' +import { FileLinks } from '../components/FileLinks' + +export const parseCellContent = ( + content: VariableWithValue['value'], + blockType?: InputBlockType +): { element?: React.JSX.Element; plainText: string } => { + if (!content) return { element: undefined, plainText: '' } + if (Array.isArray(content)) + return { + element: ( + + {content.map((item, idx) => ( + + {idx + 1}. {item} + + ))} + + ), + plainText: content.join(', '), + } + return blockType === InputBlockType.FILE + ? { element: , plainText: content } + : { plainText: content.toString() } +} diff --git a/apps/builder/src/features/results/types.ts b/apps/builder/src/features/results/types.ts index 795f9e62d..91784d5d3 100644 --- a/apps/builder/src/features/results/types.ts +++ b/apps/builder/src/features/results/types.ts @@ -2,9 +2,3 @@ export type HeaderCell = { Header: JSX.Element accessor: string } - -export type CellValueType = { element?: JSX.Element; plainText: string } - -export type TableData = { - id: Pick -} & Record diff --git a/apps/docs/openapi/builder.json b/apps/docs/openapi/builder.json index eb04b4b0d..88cf98de3 100644 --- a/apps/docs/openapi/builder.json +++ b/apps/docs/openapi/builder.json @@ -4052,6 +4052,10 @@ "type": "string", "nullable": true }, + "customChatsLimit": { + "type": "number", + "nullable": true + }, "customSeatsLimit": { "type": "number", "nullable": true @@ -4075,6 +4079,7 @@ "icon", "plan", "stripeId", + "customChatsLimit", "customSeatsLimit", "isSuspended", "isPastDue", diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts index 3b5d04f6b..7e001b601 100644 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts +++ b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts @@ -16,7 +16,7 @@ import { isWebhookBlock, omit, } from '@typebot.io/lib' -import { parseAnswers } from '@typebot.io/lib/results' +import { parseAnswers } from '@typebot.io/lib/results/parseAnswers' import { initMiddleware, methodNotAllowed, notFound } from '@typebot.io/lib/api' import { stringify } from 'qs' import Cors from 'cors' diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/integrations/email.tsx b/apps/viewer/src/pages/api/typebots/[typebotId]/integrations/email.tsx index 1ef8110ff..b36ffb930 100644 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/integrations/email.tsx +++ b/apps/viewer/src/pages/api/typebots/[typebotId]/integrations/email.tsx @@ -7,7 +7,7 @@ import { import { NextApiRequest, NextApiResponse } from 'next' import { createTransport, getTestMessageUrl } from 'nodemailer' import { isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib' -import { parseAnswers } from '@typebot.io/lib/results' +import { parseAnswers } from '@typebot.io/lib/results/parseAnswers' import { methodNotAllowed, initMiddleware } from '@typebot.io/lib/api' import { decrypt } from '@typebot.io/lib/api/encryption/decrypt' diff --git a/packages/bot-engine/blocks/integrations/sendEmail/executeSendEmailBlock.tsx b/packages/bot-engine/blocks/integrations/sendEmail/executeSendEmailBlock.tsx index f9d02b0fa..f99065912 100644 --- a/packages/bot-engine/blocks/integrations/sendEmail/executeSendEmailBlock.tsx +++ b/packages/bot-engine/blocks/integrations/sendEmail/executeSendEmailBlock.tsx @@ -11,7 +11,6 @@ import { import { createTransport } from 'nodemailer' import Mail from 'nodemailer/lib/mailer' import { byId, isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib' -import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results' import { decrypt } from '@typebot.io/lib/api/encryption/decrypt' import { defaultFrom, defaultTransportOptions } from './constants' import { findUniqueVariableValue } from '@typebot.io/variables/findUniqueVariableValue' @@ -20,6 +19,7 @@ import { ExecuteIntegrationResponse } from '../../../types' import prisma from '@typebot.io/lib/prisma' import { parseVariables } from '@typebot.io/variables/parseVariables' import { defaultSendEmailOptions } from '@typebot.io/schemas/features/blocks/integrations/sendEmail/constants' +import { parseAnswers } from '@typebot.io/lib/results/parseAnswers' export const sendEmailSuccessDescription = 'Email successfully sent' export const sendEmailErrorDescription = 'Email not sent' @@ -248,7 +248,7 @@ const getEmailBody = async ({ text: !isBodyCode ? body : undefined, } const answers = parseAnswers({ - variables: getDefinedVariables(typebot.variables), + variables: typebot.variables, answers: answersInSession, }) return { diff --git a/packages/bot-engine/blocks/integrations/webhook/executeWebhookBlock.ts b/packages/bot-engine/blocks/integrations/webhook/executeWebhookBlock.ts index 59cd1130e..e5be0ab8f 100644 --- a/packages/bot-engine/blocks/integrations/webhook/executeWebhookBlock.ts +++ b/packages/bot-engine/blocks/integrations/webhook/executeWebhookBlock.ts @@ -14,7 +14,6 @@ import { } from '@typebot.io/schemas' import { stringify } from 'qs' import { isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib' -import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results' import got, { Method, HTTPError, OptionsInit } from 'got' import { resumeWebhookExecution } from './resumeWebhookExecution' import { ExecuteIntegrationResponse } from '../../../types' @@ -27,6 +26,7 @@ import { maxTimeout, } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants' import { env } from '@typebot.io/env' +import { parseAnswers } from '@typebot.io/lib/results/parseAnswers' type ParsedWebhook = ExecutableWebhook & { basicAuth: { username?: string; password?: string } @@ -297,7 +297,7 @@ const getBodyContent = async ({ ? JSON.stringify( parseAnswers({ answers, - variables: getDefinedVariables(variables), + variables, }) ) : body ?? undefined diff --git a/packages/bot-engine/blocks/integrations/webhook/parseSampleResult.ts b/packages/bot-engine/blocks/integrations/webhook/parseSampleResult.ts index c534d7c29..38a8f27df 100644 --- a/packages/bot-engine/blocks/integrations/webhook/parseSampleResult.ts +++ b/packages/bot-engine/blocks/integrations/webhook/parseSampleResult.ts @@ -8,9 +8,9 @@ import { Variable, } from '@typebot.io/schemas' import { isInputBlock, byId, isNotDefined } from '@typebot.io/lib' -import { parseResultHeader } from '@typebot.io/lib/results' import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants' import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants' +import { parseResultHeader } from '@typebot.io/lib/results/parseResultHeader' export const parseSampleResult = ( diff --git a/packages/bot-engine/blocks/integrations/zemanticAi/executeZemanticAiBlock.ts b/packages/bot-engine/blocks/integrations/zemanticAi/executeZemanticAiBlock.ts index 5f2c46f77..1fb199dbf 100644 --- a/packages/bot-engine/blocks/integrations/zemanticAi/executeZemanticAiBlock.ts +++ b/packages/bot-engine/blocks/integrations/zemanticAi/executeZemanticAiBlock.ts @@ -7,10 +7,10 @@ import { import got from 'got' import { decrypt } from '@typebot.io/lib/api/encryption/decrypt' import { byId, isDefined, isEmpty } from '@typebot.io/lib' -import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results' import prisma from '@typebot.io/lib/prisma' import { ExecuteIntegrationResponse } from '../../../types' import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession' +import { parseAnswers } from '@typebot.io/lib/results/parseAnswers' const URL = 'https://api.zemantic.ai/v1/search-documents' @@ -50,7 +50,7 @@ export const executeZemanticAiBlock = async ( const { typebot, answers } = newSessionState.typebotsQueue[0] const templateVars = parseAnswers({ - variables: getDefinedVariables(typebot.variables), + variables: typebot.variables, answers: answers, }) diff --git a/packages/bot-engine/queries/createResultIfNotExist.ts b/packages/bot-engine/queries/createResultIfNotExist.ts index f8ef96e88..6d5bf0cbf 100644 --- a/packages/bot-engine/queries/createResultIfNotExist.ts +++ b/packages/bot-engine/queries/createResultIfNotExist.ts @@ -1,5 +1,4 @@ import prisma from '@typebot.io/lib/prisma' -import { getDefinedVariables } from '@typebot.io/lib/results' import { TypebotInSession } from '@typebot.io/schemas' type Props = { @@ -27,7 +26,7 @@ export const createResultIfNotExist = async ({ typebotId: typebot.id, isCompleted: isCompleted ? true : false, hasStarted, - variables: getDefinedVariables(typebot.variables), + variables: typebot.variables, }, ], }) diff --git a/packages/bot-engine/queries/upsertResult.ts b/packages/bot-engine/queries/upsertResult.ts index 65a464365..a0d1b1af9 100644 --- a/packages/bot-engine/queries/upsertResult.ts +++ b/packages/bot-engine/queries/upsertResult.ts @@ -1,6 +1,6 @@ import prisma from '@typebot.io/lib/prisma' -import { getDefinedVariables } from '@typebot.io/lib/results' import { TypebotInSession } from '@typebot.io/schemas' +import { filterVariablesWithValues } from '@typebot.io/variables/filterVariablesWithValues' type Props = { resultId: string @@ -18,7 +18,7 @@ export const upsertResult = async ({ where: { id: resultId }, select: { id: true }, }) - const variablesWithValue = getDefinedVariables(typebot.variables) + const variablesWithValue = filterVariablesWithValues(typebot.variables) if (existingResult) { return prisma.result.updateMany({ diff --git a/packages/lib/parseUniqueKey.ts b/packages/lib/parseUniqueKey.ts new file mode 100644 index 000000000..25a944e0c --- /dev/null +++ b/packages/lib/parseUniqueKey.ts @@ -0,0 +1,8 @@ +export const parseUniqueKey = ( + key: string, + existingKeys: string[], + count = 0 +): string => { + if (!existingKeys.includes(key)) return key + return parseUniqueKey(`${key} (${count + 1})`, existingKeys, count + 1) +} diff --git a/apps/builder/src/features/results/helpers/convertResultsToTableData.tsx b/packages/lib/results/convertResultsToTableData.ts similarity index 70% rename from apps/builder/src/features/results/helpers/convertResultsToTableData.tsx rename to packages/lib/results/convertResultsToTableData.ts index 5a3b75caa..c0ea0f8ca 100644 --- a/apps/builder/src/features/results/helpers/convertResultsToTableData.tsx +++ b/packages/lib/results/convertResultsToTableData.ts @@ -1,19 +1,33 @@ -import { Stack, Text } from '@chakra-ui/react' -import { isDefined } from '@typebot.io/lib' import { ResultWithAnswers, ResultHeaderCell, VariableWithValue, Answer, + TableData, } from '@typebot.io/schemas' -import { FileLinks } from '../components/FileLinks' -import { TableData } from '../types' -import { convertDateToReadable } from './convertDateToReadable' import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants' +import { isDefined } from '../utils' + +type CellParser = ( + content: VariableWithValue['value'], + blockType?: InputBlockType +) => { element?: React.JSX.Element; plainText: string } + +const defaultCellParser: CellParser = (content, blockType) => { + if (!content) return { plainText: '' } + if (Array.isArray(content)) + return { + plainText: content.join(', '), + } + return blockType === InputBlockType.FILE + ? { plainText: content } + : { plainText: content.toString() } +} export const convertResultsToTableData = ( results: ResultWithAnswers[] | undefined, - headerCells: ResultHeaderCell[] + headerCells: ResultHeaderCell[], + cellParser: CellParser = defaultCellParser ): TableData[] => (results ?? []).map((result) => ({ id: { plainText: result.id }, @@ -39,7 +53,7 @@ export const convertResultsToTableData = ( const content = variableValue ?? answer.content return { ...tableData, - [header.id]: parseCellContent(content, header.blockType), + [header.id]: cellParser(content, header.blockType), } } const variable = answerOrVariable satisfies VariableWithValue @@ -51,30 +65,15 @@ export const convertResultsToTableData = ( if (isDefined(tableData[headerId])) return tableData return { ...tableData, - [headerId]: parseCellContent(variable.value), + [headerId]: cellParser(variable.value), } }, {}), })) -const parseCellContent = ( - content: VariableWithValue['value'], - blockType?: InputBlockType -): { element?: JSX.Element; plainText: string } => { - if (!content) return { element: undefined, plainText: '' } - if (Array.isArray(content)) - return { - element: ( - - {content.map((item, idx) => ( - - {idx + 1}. {item} - - ))} - - ), - plainText: content.join(', '), - } - return blockType === InputBlockType.FILE - ? { element: , plainText: content } - : { plainText: content.toString() } -} +const convertDateToReadable = (date: Date): string => + date.toDateString().split(' ').slice(1, 3).join(' ') + + ', ' + + date.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + }) diff --git a/packages/lib/results/parseAnswers.ts b/packages/lib/results/parseAnswers.ts new file mode 100644 index 000000000..dcd5636ea --- /dev/null +++ b/packages/lib/results/parseAnswers.ts @@ -0,0 +1,39 @@ +import { + AnswerInSessionState, + Variable, + VariableWithValue, +} from '@typebot.io/schemas' +import { isDefined, isEmpty } from '../utils' + +export const parseAnswers = ({ + answers, + variables: resultVariables, +}: { + answers: AnswerInSessionState[] + variables: Variable[] +}): { + [key: string]: string +} => { + const variablesWithValues = resultVariables.filter((variable) => + isDefined(variable.value) + ) as VariableWithValue[] + + return { + submittedAt: new Date().toISOString(), + ...[...answers, ...variablesWithValues].reduce<{ + [key: string]: string + }>((o, answerOrVariable) => { + if ('id' in answerOrVariable) { + const variable = answerOrVariable + if (variable.value === null) return o + return { ...o, [variable.name]: variable.value.toString() } + } + const answer = answerOrVariable as AnswerInSessionState + if (isEmpty(answer.key)) return o + return { + ...o, + [answer.key]: answer.value, + } + }, {}), + } +} diff --git a/apps/builder/src/features/results/helpers/parseColumnsOrder.ts b/packages/lib/results/parseColumnsOrder.ts similarity index 91% rename from apps/builder/src/features/results/helpers/parseColumnsOrder.ts rename to packages/lib/results/parseColumnsOrder.ts index 5f634806f..c71270bd2 100644 --- a/apps/builder/src/features/results/helpers/parseColumnsOrder.ts +++ b/packages/lib/results/parseColumnsOrder.ts @@ -1,6 +1,6 @@ import { ResultHeaderCell } from '@typebot.io/schemas' -export const parseColumnOrder = ( +export const parseColumnsOrder = ( existingOrder: string[] | undefined, resultHeader: ResultHeaderCell[] ) => diff --git a/packages/lib/results.ts b/packages/lib/results/parseResultHeader.ts similarity index 85% rename from packages/lib/results.ts rename to packages/lib/results/parseResultHeader.ts index feec9a5c4..2f5f817b4 100644 --- a/packages/lib/results.ts +++ b/packages/lib/results/parseResultHeader.ts @@ -1,15 +1,13 @@ import { + ResultWithAnswers, + ResultHeaderCell, Group, Variable, InputBlock, - ResultHeaderCell, - VariableWithValue, Typebot, - ResultWithAnswers, - AnswerInSessionState, } from '@typebot.io/schemas' -import { isInputBlock, isDefined, byId, isNotEmpty, isEmpty } from './utils' import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants' +import { isInputBlock, byId, isNotEmpty } from '../utils' export const parseResultHeader = ( typebot: Pick, @@ -212,37 +210,3 @@ const parseResultsFromPreviousBotVersions = ( }, ] }, []) - -export const parseAnswers = ({ - answers, - variables: resultVariables, -}: { - answers: AnswerInSessionState[] - variables: VariableWithValue[] -}): { - [key: string]: string -} => { - return { - submittedAt: new Date().toISOString(), - ...[...answers, ...resultVariables].reduce<{ - [key: string]: string - }>((o, answerOrVariable) => { - if ('id' in answerOrVariable) { - const variable = answerOrVariable - if (variable.value === null) return o - return { ...o, [variable.name]: variable.value.toString() } - } - const answer = answerOrVariable as AnswerInSessionState - if (isEmpty(answer.key)) return o - return { - ...o, - [answer.key]: answer.value, - } - }, {}), - } -} - -export const getDefinedVariables = (variables: Variable[]) => - variables.filter((variable) => - isDefined(variable.value) - ) as VariableWithValue[] diff --git a/packages/schemas/features/result.ts b/packages/schemas/features/result.ts index fe2146441..009730d34 100644 --- a/packages/schemas/features/result.ts +++ b/packages/schemas/features/result.ts @@ -72,3 +72,9 @@ export type ResultHeaderCell = { blockType?: InputBlockType variableIds?: string[] } + +export type CellValueType = { element?: JSX.Element; plainText: string } + +export type TableData = { + id: Pick +} & Record diff --git a/packages/scripts/exportResults.ts b/packages/scripts/exportResults.ts new file mode 100644 index 000000000..f7e229a11 --- /dev/null +++ b/packages/scripts/exportResults.ts @@ -0,0 +1,119 @@ +import { PrismaClient } from '@typebot.io/prisma' +import * as p from '@clack/prompts' +import { promptAndSetEnvironment } from './utils' +import cliProgress from 'cli-progress' +import { writeFileSync } from 'fs' +import { + ResultWithAnswers, + Typebot, + resultWithAnswersSchema, +} from '@typebot.io/schemas' +import { byId } from '@typebot.io/lib' +import { parseResultHeader } from '@typebot.io/lib/results/parseResultHeader' +import { convertResultsToTableData } from '@typebot.io/lib/results/convertResultsToTableData' +import { parseColumnsOrder } from '@typebot.io/lib/results/parseColumnsOrder' +import { parseUniqueKey } from '@typebot.io/lib/parseUniqueKey' +import { unparse } from 'papaparse' +import { z } from 'zod' + +const exportResults = async () => { + await promptAndSetEnvironment('production') + + const prisma = new PrismaClient() + + const typebotId = (await p.text({ + message: 'Typebot ID?', + })) as string + + if (!typebotId || typeof typebotId !== 'string') { + console.log('No id provided') + return + } + + const progressBar = new cliProgress.SingleBar( + {}, + cliProgress.Presets.shades_classic + ) + + const typebot = (await prisma.typebot.findUnique({ + where: { + id: typebotId, + }, + })) as Typebot | null + + if (!typebot) { + console.log('No typebot found') + return + } + + const totalResultsToExport = await prisma.result.count({ + where: { + typebotId, + hasStarted: true, + isArchived: false, + }, + }) + + progressBar.start(totalResultsToExport, 0) + + const results: ResultWithAnswers[] = [] + + for (let skip = 0; skip < totalResultsToExport; skip += 50) { + results.push( + ...z.array(resultWithAnswersSchema).parse( + await prisma.result.findMany({ + take: 50, + skip, + where: { + typebotId, + hasStarted: true, + isArchived: false, + }, + orderBy: { + createdAt: 'desc', + }, + include: { answers: true }, + }) + ) + ) + progressBar.increment(50) + } + + progressBar.stop() + + writeFileSync('logs/results.json', JSON.stringify(results)) + + const resultHeader = parseResultHeader(typebot, []) + + const dataToUnparse = convertResultsToTableData(results, resultHeader) + + const headerIds = parseColumnsOrder( + typebot?.resultsTablePreferences?.columnsOrder, + resultHeader + ).reduce((currentHeaderIds, columnId) => { + if (typebot?.resultsTablePreferences?.columnsVisibility[columnId] === false) + return currentHeaderIds + const columnLabel = resultHeader.find( + (headerCell) => headerCell.id === columnId + )?.id + if (!columnLabel) return currentHeaderIds + return [...currentHeaderIds, columnLabel] + }, []) + + const data = dataToUnparse.map<{ [key: string]: string }>((data) => { + const newObject: { [key: string]: string } = {} + headerIds?.forEach((headerId) => { + const headerLabel = resultHeader.find(byId(headerId))?.label + if (!headerLabel) return + const newKey = parseUniqueKey(headerLabel, Object.keys(newObject)) + newObject[newKey] = data[headerId]?.plainText + }) + return newObject + }) + + const csv = unparse(data) + + writeFileSync('logs/results.csv', csv) +} + +exportResults() diff --git a/packages/scripts/fixTypebots.ts b/packages/scripts/fixTypebots.ts deleted file mode 100644 index d3897f455..000000000 --- a/packages/scripts/fixTypebots.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { PrismaClient } from '@typebot.io/prisma' -import { writeFileSync } from 'fs' -import { - Block, - BlockOptions, - BlockType, - defaultEmailInputOptions, - Group, - InputBlockType, - PublicTypebot, - publicTypebotSchema, - Theme, - Typebot, -} from '@typebot.io/schemas' -import { isDefined, isNotDefined } from '@typebot.io/lib' -import { promptAndSetEnvironment } from './utils' -import { detailedDiff } from 'deep-object-diff' - -const fixTypebot = (brokenTypebot: Typebot | PublicTypebot) => - ({ - ...brokenTypebot, - theme: fixTheme(brokenTypebot.theme), - groups: fixGroups(brokenTypebot.groups), - } satisfies Typebot | PublicTypebot) - -const fixTheme = (brokenTheme: Theme) => - ({ - ...brokenTheme, - chat: { - ...brokenTheme.chat, - hostAvatar: brokenTheme.chat.hostAvatar - ? { - isEnabled: brokenTheme.chat.hostAvatar.isEnabled, - url: brokenTheme.chat.hostAvatar.url ?? undefined, - } - : undefined, - }, - } satisfies Theme) - -const fixGroups = (brokenGroups: Group[]) => - brokenGroups.map( - (brokenGroup, index) => - ({ - ...brokenGroup, - graphCoordinates: { - ...brokenGroup.graphCoordinates, - x: brokenGroup.graphCoordinates.x ?? 0, - y: brokenGroup.graphCoordinates.y ?? 0, - }, - blocks: fixBlocks(brokenGroup.blocks, brokenGroup.id, index), - } satisfies Group) - ) - -const fixBlocks = ( - brokenBlocks: Block[], - groupId: string, - groupIndex: number -) => { - if (groupIndex === 0 && brokenBlocks.length > 1) return [brokenBlocks[0]] - return brokenBlocks - .filter((block) => block && Object.keys(block).length > 0) - .map((brokenBlock) => { - return removeUndefinedFromObject({ - ...brokenBlock, - webhookId: - ('webhookId' in brokenBlock ? brokenBlock.webhookId : undefined) ?? - ('webhook' in brokenBlock && brokenBlock.webhook - ? //@ts-ignore - brokenBlock.webhook.id - : undefined), - webhook: undefined, - groupId: brokenBlock.groupId ?? groupId, - options: - brokenBlock && 'options' in brokenBlock && brokenBlock.options - ? fixBrokenBlockOption(brokenBlock.options, brokenBlock.type) - : undefined, - }) - }) as Block[] -} - -const fixBrokenBlockOption = (options: BlockOptions, blockType: BlockType) => - removeUndefinedFromObject({ - ...options, - sheetId: - 'sheetId' in options && isDefined(options.sheetId) - ? options.sheetId.toString() - : undefined, - step: - 'step' in options && isDefined(options.step) ? options.step : undefined, - value: - 'value' in options && isDefined(options.value) - ? options.value - : undefined, - retryMessageContent: fixRetryMessageContent( - //@ts-ignore - options.retryMessageContent, - blockType - ), - }) as BlockOptions - -const fixRetryMessageContent = ( - retryMessageContent: string | undefined, - blockType: BlockType -) => { - if (isNotDefined(retryMessageContent) && blockType === InputBlockType.EMAIL) - return defaultEmailInputOptions.retryMessageContent - if (isNotDefined(retryMessageContent)) return undefined - return retryMessageContent -} - -const removeUndefinedFromObject = (obj: any) => { - Object.keys(obj).forEach((key) => obj[key] === undefined && delete obj[key]) - return obj -} - -const resolve = (path: string, obj: object, separator = '.') => { - const properties = Array.isArray(path) ? path : path.split(separator) - //@ts-ignore - return properties.reduce((prev, curr) => prev?.[curr], obj) -} - -const fixTypebots = async () => { - await promptAndSetEnvironment() - const prisma = new PrismaClient({ - log: [{ emit: 'event', level: 'query' }, 'info', 'warn', 'error'], - }) - - const typebots = await prisma.publicTypebot.findMany({ - where: { - updatedAt: { - gte: new Date('2023-01-01T00:00:00.000Z'), - }, - }, - }) - - writeFileSync('logs/typebots.json', JSON.stringify(typebots)) - - const total = typebots.length - let totalFixed = 0 - let progress = 0 - const fixedTypebots: (Typebot | PublicTypebot)[] = [] - const diffs: any[] = [] - for (const typebot of typebots) { - progress += 1 - console.log( - `Progress: ${progress}/${total} (${Math.round( - (progress / total) * 100 - )}%) (${totalFixed} fixed typebots)` - ) - const parser = publicTypebotSchema.safeParse({ - ...typebot, - updatedAt: new Date(typebot.updatedAt), - createdAt: new Date(typebot.createdAt), - }) - if ('error' in parser) { - const fixedTypebot = { - ...fixTypebot(typebot as Typebot | PublicTypebot), - updatedAt: new Date(), - createdAt: new Date(typebot.createdAt), - } - publicTypebotSchema.parse(fixedTypebot) - fixedTypebots.push(fixedTypebot) - totalFixed += 1 - diffs.push({ - id: typebot.id, - failedObject: resolve(parser.error.issues[0].path.join('.'), typebot), - ...detailedDiff(typebot, fixedTypebot), - }) - } - } - writeFileSync('logs/fixedTypebots.json', JSON.stringify(fixedTypebots)) - writeFileSync( - 'logs/diffs.json', - JSON.stringify(diffs.reverse().slice(0, 100)) - ) - - const queries = fixedTypebots.map((fixedTypebot) => - prisma.publicTypebot.updateMany({ - where: { id: fixedTypebot.id }, - data: { - ...fixedTypebot, - // theme: fixedTypebot.theme ?? undefined, - // settings: fixedTypebot.settings ?? undefined, - // resultsTablePreferences: - // 'resultsTablePreferences' in fixedTypebot && - // fixedTypebot.resultsTablePreferences - // ? fixedTypebot.resultsTablePreferences - // : undefined, - } as any, - }) - ) - - const totalQueries = queries.length - progress = 0 - prisma.$on('query', () => { - progress += 1 - console.log(`Progress: ${progress}/${totalQueries}`) - }) - - await prisma.$transaction(queries) -} - -fixTypebots() diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 072974b89..07264fe9b 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -25,14 +25,17 @@ "updateWorkspace": "tsx updateWorkspace.ts", "inspectTypebot": "tsx inspectTypebot.ts", "inspectWorkspace": "tsx inspectWorkspace.ts", - "getCoupon": "tsx getCoupon.ts" + "getCoupon": "tsx getCoupon.ts", + "exportResults": "tsx exportResults.ts" }, "devDependencies": { "@typebot.io/emails": "workspace:*", "@typebot.io/lib": "workspace:*", "@typebot.io/prisma": "workspace:*", "@typebot.io/schemas": "workspace:*", + "@types/cli-progress": "^3.11.5", "@types/node": "20.4.2", + "@types/papaparse": "5.3.7", "@types/prompts": "2.4.4", "deep-object-diff": "1.1.9", "got": "12.6.0", @@ -44,6 +47,8 @@ }, "dependencies": { "@clack/prompts": "^0.7.0", - "@paralleldrive/cuid2": "2.2.1" + "@paralleldrive/cuid2": "2.2.1", + "cli-progress": "^3.12.0", + "papaparse": "5.4.1" } } diff --git a/packages/variables/filterVariablesWithValues.ts b/packages/variables/filterVariablesWithValues.ts new file mode 100644 index 000000000..2d710bda5 --- /dev/null +++ b/packages/variables/filterVariablesWithValues.ts @@ -0,0 +1,9 @@ +import { isDefined } from '@typebot.io/lib' +import { Variable, VariableWithValue } from '../schemas' + +export const filterVariablesWithValues = ( + variables: Variable[] +): VariableWithValue[] => + variables.filter((variable) => + isDefined(variable.value) + ) as VariableWithValue[] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f38406537..a0ef199d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1481,6 +1481,12 @@ importers: '@paralleldrive/cuid2': specifier: 2.2.1 version: 2.2.1 + cli-progress: + specifier: ^3.12.0 + version: 3.12.0 + papaparse: + specifier: 5.4.1 + version: 5.4.1 devDependencies: '@typebot.io/emails': specifier: workspace:* @@ -1494,9 +1500,15 @@ importers: '@typebot.io/schemas': specifier: workspace:* version: link:../schemas + '@types/cli-progress': + specifier: ^3.11.5 + version: 3.11.5 '@types/node': specifier: 20.4.2 version: 20.4.2 + '@types/papaparse': + specifier: 5.3.7 + version: 5.3.7 '@types/prompts': specifier: 2.4.4 version: 2.4.4 @@ -7735,6 +7747,12 @@ packages: resolution: {integrity: sha512-Yq6rIccwcco0TLD5SMUrIM7Fk7Fe/C0jmNRxJJCLtAF6gebDkPuUjK5EHedxecm69Pi/aA+It39Ux4OHmFhjRw==} dev: true + /@types/cli-progress@3.11.5: + resolution: {integrity: sha512-D4PbNRbviKyppS5ivBGyFO29POlySLmA2HyUFE4p5QGazAMM3CwkKWcvTl8gvElSuxRh6FPKL8XmidX873ou4g==} + dependencies: + '@types/node': 20.10.1 + dev: true + /@types/content-type@1.1.6: resolution: {integrity: sha512-WFHg/KFLCdUQl3m27WSQu0NEaLzoHGmgZHlsSYr0Y0iIvItMcBq7opZc6AGXPXqf+btIM6vTBJyLvuDAihB+zQ==} dev: false @@ -7925,7 +7943,7 @@ packages: /@types/papaparse@5.3.7: resolution: {integrity: sha512-f2HKmlnPdCvS0WI33WtCs5GD7X1cxzzS/aduaxSu3I7TbhWlENjSPs6z5TaB9K0J+BH1jbmqTaM+ja5puis4wg==} dependencies: - '@types/node': 20.4.9 + '@types/node': 20.10.1 dev: true /@types/parse-json@4.0.0: @@ -10831,6 +10849,13 @@ packages: restore-cursor: 4.0.0 dev: true + /cli-progress@3.12.0: + resolution: {integrity: sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==} + engines: {node: '>=4'} + dependencies: + string-width: 4.2.3 + dev: false + /cli-spinners@2.9.2: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'}