🧐 Add exportResults script
This commit is contained in:
@ -7,10 +7,10 @@ import {
|
|||||||
TypebotLinkBlock,
|
TypebotLinkBlock,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { isInputBlock, byId, isNotDefined } from '@typebot.io/lib'
|
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 { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||||
import { EventType } from '@typebot.io/schemas/features/events/constants'
|
import { EventType } from '@typebot.io/schemas/features/events/constants'
|
||||||
|
import { parseResultHeader } from '@typebot.io/lib/results/parseResultHeader'
|
||||||
|
|
||||||
export const parseResultExample =
|
export const parseResultExample =
|
||||||
({
|
({
|
||||||
|
@ -2,17 +2,18 @@ import { useToast } from '@/hooks/useToast'
|
|||||||
import {
|
import {
|
||||||
ResultHeaderCell,
|
ResultHeaderCell,
|
||||||
ResultWithAnswers,
|
ResultWithAnswers,
|
||||||
|
TableData,
|
||||||
Typebot,
|
Typebot,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { createContext, ReactNode, useContext, useMemo } from 'react'
|
import { createContext, ReactNode, useContext, useMemo } from 'react'
|
||||||
import { parseResultHeader } from '@typebot.io/lib/results'
|
|
||||||
import { useTypebot } from '../editor/providers/TypebotProvider'
|
import { useTypebot } from '../editor/providers/TypebotProvider'
|
||||||
import { useResultsQuery } from './hooks/useResultsQuery'
|
import { useResultsQuery } from './hooks/useResultsQuery'
|
||||||
import { TableData } from './types'
|
|
||||||
import { convertResultsToTableData } from './helpers/convertResultsToTableData'
|
|
||||||
import { trpc } from '@/lib/trpc'
|
import { trpc } from '@/lib/trpc'
|
||||||
import { isDefined } from '@typebot.io/lib/utils'
|
import { isDefined } from '@typebot.io/lib/utils'
|
||||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
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<{
|
const resultsContext = createContext<{
|
||||||
resultsList: { results: ResultWithAnswers[] }[] | undefined
|
resultsList: { results: ResultWithAnswers[] }[] | undefined
|
||||||
@ -94,7 +95,8 @@ export const ResultsProvider = ({
|
|||||||
publishedTypebot
|
publishedTypebot
|
||||||
? convertResultsToTableData(
|
? convertResultsToTableData(
|
||||||
data?.flatMap((d) => d.results) ?? [],
|
data?.flatMap((d) => d.results) ?? [],
|
||||||
resultHeader
|
resultHeader,
|
||||||
|
parseCellContent
|
||||||
)
|
)
|
||||||
: [],
|
: [],
|
||||||
[publishedTypebot, data, resultHeader]
|
[publishedTypebot, data, resultHeader]
|
||||||
|
@ -14,7 +14,7 @@ import React from 'react'
|
|||||||
import { byId, isDefined } from '@typebot.io/lib'
|
import { byId, isDefined } from '@typebot.io/lib'
|
||||||
import { HeaderIcon } from './HeaderIcon'
|
import { HeaderIcon } from './HeaderIcon'
|
||||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
import { parseColumnOrder } from '../helpers/parseColumnsOrder'
|
import { parseColumnsOrder } from '@typebot.io/lib/results/parseColumnsOrder'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
resultId: string | null
|
resultId: string | null
|
||||||
@ -28,7 +28,7 @@ export const ResultModal = ({ resultId, onClose }: Props) => {
|
|||||||
? tableData.find((data) => data.id.plainText === resultId)
|
? tableData.find((data) => data.id.plainText === resultId)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const columnsOrder = parseColumnOrder(
|
const columnsOrder = parseColumnsOrder(
|
||||||
typebot?.resultsTablePreferences?.columnsOrder,
|
typebot?.resultsTablePreferences?.columnsOrder,
|
||||||
resultHeader
|
resultHeader
|
||||||
)
|
)
|
||||||
|
@ -2,7 +2,7 @@ import { chakra, Fade, Button, useColorModeValue } from '@chakra-ui/react'
|
|||||||
import { Cell as CellProps, flexRender } from '@tanstack/react-table'
|
import { Cell as CellProps, flexRender } from '@tanstack/react-table'
|
||||||
import { ExpandIcon } from '@/components/icons'
|
import { ExpandIcon } from '@/components/icons'
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { TableData } from '../../types'
|
import { TableData } from '@typebot.io/schemas'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
cell: CellProps<TableData, unknown>
|
cell: CellProps<TableData, unknown>
|
||||||
|
@ -20,10 +20,11 @@ import {
|
|||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
import { unparse } from 'papaparse'
|
import { unparse } from 'papaparse'
|
||||||
import { useState } from 'react'
|
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 { useResults } from '../../ResultsProvider'
|
||||||
import { parseColumnOrder } from '../../helpers/parseColumnsOrder'
|
|
||||||
import { convertResultsToTableData } from '../../helpers/convertResultsToTableData'
|
|
||||||
import { byId, isDefined } from '@typebot.io/lib'
|
import { byId, isDefined } from '@typebot.io/lib'
|
||||||
import { Typebot } from '@typebot.io/schemas'
|
import { Typebot } from '@typebot.io/schemas'
|
||||||
|
|
||||||
@ -101,7 +102,7 @@ export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => {
|
|||||||
|
|
||||||
const dataToUnparse = convertResultsToTableData(results, resultHeader)
|
const dataToUnparse = convertResultsToTableData(results, resultHeader)
|
||||||
|
|
||||||
const headerIds = parseColumnOrder(
|
const headerIds = parseColumnsOrder(
|
||||||
typebot?.resultsTablePreferences?.columnsOrder,
|
typebot?.resultsTablePreferences?.columnsOrder,
|
||||||
resultHeader
|
resultHeader
|
||||||
).reduce<string[]>((currentHeaderIds, columnId) => {
|
).reduce<string[]>((currentHeaderIds, columnId) => {
|
||||||
@ -182,12 +183,3 @@ export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => {
|
|||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseUniqueKey = (
|
|
||||||
key: string,
|
|
||||||
existingKeys: string[],
|
|
||||||
count = 0
|
|
||||||
): string => {
|
|
||||||
if (!existingKeys.includes(key)) return key
|
|
||||||
return parseUniqueKey(`${key} (${count + 1})`, existingKeys, count + 1)
|
|
||||||
}
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { colors } from '@/lib/theme'
|
import { colors } from '@/lib/theme'
|
||||||
import { Box, BoxProps, chakra, useColorModeValue } from '@chakra-ui/react'
|
import { Box, BoxProps, chakra, useColorModeValue } from '@chakra-ui/react'
|
||||||
import { flexRender, HeaderGroup } from '@tanstack/react-table'
|
import { flexRender, HeaderGroup } from '@tanstack/react-table'
|
||||||
|
import { TableData } from '@typebot.io/schemas'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { TableData } from '../../types'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
headerGroup: HeaderGroup<TableData>
|
headerGroup: HeaderGroup<TableData>
|
||||||
|
@ -8,7 +8,12 @@ import {
|
|||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { AlignLeftTextIcon } from '@/components/icons'
|
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 React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { LoadingRows } from './LoadingRows'
|
import { LoadingRows } from './LoadingRows'
|
||||||
import {
|
import {
|
||||||
@ -22,11 +27,10 @@ import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
|||||||
import { SelectionToolbar } from './SelectionToolbar'
|
import { SelectionToolbar } from './SelectionToolbar'
|
||||||
import { Row } from './Row'
|
import { Row } from './Row'
|
||||||
import { HeaderRow } from './HeaderRow'
|
import { HeaderRow } from './HeaderRow'
|
||||||
import { CellValueType, TableData } from '../../types'
|
|
||||||
import { IndeterminateCheckbox } from './IndeterminateCheckbox'
|
import { IndeterminateCheckbox } from './IndeterminateCheckbox'
|
||||||
import { colors } from '@/lib/theme'
|
import { colors } from '@/lib/theme'
|
||||||
import { parseColumnOrder } from '../../helpers/parseColumnsOrder'
|
|
||||||
import { HeaderIcon } from '../HeaderIcon'
|
import { HeaderIcon } from '../HeaderIcon'
|
||||||
|
import { parseColumnsOrder } from '@typebot.io/lib/results/parseColumnsOrder'
|
||||||
|
|
||||||
type ResultsTableProps = {
|
type ResultsTableProps = {
|
||||||
resultHeader: ResultHeaderCell[]
|
resultHeader: ResultHeaderCell[]
|
||||||
@ -60,7 +64,7 @@ export const ResultsTable = ({
|
|||||||
columnsWidth = {},
|
columnsWidth = {},
|
||||||
} = {
|
} = {
|
||||||
...preferences,
|
...preferences,
|
||||||
columnsOrder: parseColumnOrder(preferences?.columnsOrder, resultHeader),
|
columnsOrder: parseColumnsOrder(preferences?.columnsOrder, resultHeader),
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeColumnOrder = (newColumnOrder: string[]) => {
|
const changeColumnOrder = (newColumnOrder: string[]) => {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Row as RowProps } from '@tanstack/react-table'
|
import { Row as RowProps } from '@tanstack/react-table'
|
||||||
import Cell from './Cell'
|
import Cell from './Cell'
|
||||||
import { TableData } from '../../types'
|
import { TableData } from '@typebot.io/schemas'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
row: RowProps<TableData>
|
row: RowProps<TableData>
|
||||||
|
@ -14,9 +14,9 @@ import React, { useState } from 'react'
|
|||||||
import { useToast } from '@/hooks/useToast'
|
import { useToast } from '@/hooks/useToast'
|
||||||
import { useResults } from '../../ResultsProvider'
|
import { useResults } from '../../ResultsProvider'
|
||||||
import { trpc } from '@/lib/trpc'
|
import { trpc } from '@/lib/trpc'
|
||||||
import { parseColumnOrder } from '../../helpers/parseColumnsOrder'
|
|
||||||
import { byId } from '@typebot.io/lib/utils'
|
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 = {
|
type Props = {
|
||||||
selectedResultsId: string[]
|
selectedResultsId: string[]
|
||||||
@ -70,7 +70,7 @@ export const SelectionToolbar = ({
|
|||||||
selectedResultsId.includes(data.id.plainText)
|
selectedResultsId.includes(data.id.plainText)
|
||||||
)
|
)
|
||||||
|
|
||||||
const headerIds = parseColumnOrder(
|
const headerIds = parseColumnsOrder(
|
||||||
typebot?.resultsTablePreferences?.columnsOrder,
|
typebot?.resultsTablePreferences?.columnsOrder,
|
||||||
resultHeader
|
resultHeader
|
||||||
)
|
)
|
||||||
|
@ -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',
|
|
||||||
})
|
|
@ -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: (
|
||||||
|
<Stack spacing={2}>
|
||||||
|
{content.map((item, idx) => (
|
||||||
|
<Text key={idx}>
|
||||||
|
{idx + 1}. {item}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
plainText: content.join(', '),
|
||||||
|
}
|
||||||
|
return blockType === InputBlockType.FILE
|
||||||
|
? { element: <FileLinks fileNamesStr={content} />, plainText: content }
|
||||||
|
: { plainText: content.toString() }
|
||||||
|
}
|
@ -2,9 +2,3 @@ export type HeaderCell = {
|
|||||||
Header: JSX.Element
|
Header: JSX.Element
|
||||||
accessor: string
|
accessor: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CellValueType = { element?: JSX.Element; plainText: string }
|
|
||||||
|
|
||||||
export type TableData = {
|
|
||||||
id: Pick<CellValueType, 'plainText'>
|
|
||||||
} & Record<string, CellValueType>
|
|
||||||
|
@ -4052,6 +4052,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
},
|
},
|
||||||
|
"customChatsLimit": {
|
||||||
|
"type": "number",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
"customSeatsLimit": {
|
"customSeatsLimit": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
@ -4075,6 +4079,7 @@
|
|||||||
"icon",
|
"icon",
|
||||||
"plan",
|
"plan",
|
||||||
"stripeId",
|
"stripeId",
|
||||||
|
"customChatsLimit",
|
||||||
"customSeatsLimit",
|
"customSeatsLimit",
|
||||||
"isSuspended",
|
"isSuspended",
|
||||||
"isPastDue",
|
"isPastDue",
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
isWebhookBlock,
|
isWebhookBlock,
|
||||||
omit,
|
omit,
|
||||||
} from '@typebot.io/lib'
|
} 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 { initMiddleware, methodNotAllowed, notFound } from '@typebot.io/lib/api'
|
||||||
import { stringify } from 'qs'
|
import { stringify } from 'qs'
|
||||||
import Cors from 'cors'
|
import Cors from 'cors'
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { createTransport, getTestMessageUrl } from 'nodemailer'
|
import { createTransport, getTestMessageUrl } from 'nodemailer'
|
||||||
import { isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib'
|
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 { methodNotAllowed, initMiddleware } from '@typebot.io/lib/api'
|
||||||
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
|
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
|
||||||
|
|
||||||
|
@ -11,7 +11,6 @@ import {
|
|||||||
import { createTransport } from 'nodemailer'
|
import { createTransport } from 'nodemailer'
|
||||||
import Mail from 'nodemailer/lib/mailer'
|
import Mail from 'nodemailer/lib/mailer'
|
||||||
import { byId, isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib'
|
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 { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
|
||||||
import { defaultFrom, defaultTransportOptions } from './constants'
|
import { defaultFrom, defaultTransportOptions } from './constants'
|
||||||
import { findUniqueVariableValue } from '@typebot.io/variables/findUniqueVariableValue'
|
import { findUniqueVariableValue } from '@typebot.io/variables/findUniqueVariableValue'
|
||||||
@ -20,6 +19,7 @@ import { ExecuteIntegrationResponse } from '../../../types'
|
|||||||
import prisma from '@typebot.io/lib/prisma'
|
import prisma from '@typebot.io/lib/prisma'
|
||||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||||
import { defaultSendEmailOptions } from '@typebot.io/schemas/features/blocks/integrations/sendEmail/constants'
|
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 sendEmailSuccessDescription = 'Email successfully sent'
|
||||||
export const sendEmailErrorDescription = 'Email not sent'
|
export const sendEmailErrorDescription = 'Email not sent'
|
||||||
@ -248,7 +248,7 @@ const getEmailBody = async ({
|
|||||||
text: !isBodyCode ? body : undefined,
|
text: !isBodyCode ? body : undefined,
|
||||||
}
|
}
|
||||||
const answers = parseAnswers({
|
const answers = parseAnswers({
|
||||||
variables: getDefinedVariables(typebot.variables),
|
variables: typebot.variables,
|
||||||
answers: answersInSession,
|
answers: answersInSession,
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
|
@ -14,7 +14,6 @@ import {
|
|||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { stringify } from 'qs'
|
import { stringify } from 'qs'
|
||||||
import { isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib'
|
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 got, { Method, HTTPError, OptionsInit } from 'got'
|
||||||
import { resumeWebhookExecution } from './resumeWebhookExecution'
|
import { resumeWebhookExecution } from './resumeWebhookExecution'
|
||||||
import { ExecuteIntegrationResponse } from '../../../types'
|
import { ExecuteIntegrationResponse } from '../../../types'
|
||||||
@ -27,6 +26,7 @@ import {
|
|||||||
maxTimeout,
|
maxTimeout,
|
||||||
} from '@typebot.io/schemas/features/blocks/integrations/webhook/constants'
|
} from '@typebot.io/schemas/features/blocks/integrations/webhook/constants'
|
||||||
import { env } from '@typebot.io/env'
|
import { env } from '@typebot.io/env'
|
||||||
|
import { parseAnswers } from '@typebot.io/lib/results/parseAnswers'
|
||||||
|
|
||||||
type ParsedWebhook = ExecutableWebhook & {
|
type ParsedWebhook = ExecutableWebhook & {
|
||||||
basicAuth: { username?: string; password?: string }
|
basicAuth: { username?: string; password?: string }
|
||||||
@ -297,7 +297,7 @@ const getBodyContent = async ({
|
|||||||
? JSON.stringify(
|
? JSON.stringify(
|
||||||
parseAnswers({
|
parseAnswers({
|
||||||
answers,
|
answers,
|
||||||
variables: getDefinedVariables(variables),
|
variables,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
: body ?? undefined
|
: body ?? undefined
|
||||||
|
@ -8,9 +8,9 @@ import {
|
|||||||
Variable,
|
Variable,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { isInputBlock, byId, isNotDefined } from '@typebot.io/lib'
|
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 { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||||
|
import { parseResultHeader } from '@typebot.io/lib/results/parseResultHeader'
|
||||||
|
|
||||||
export const parseSampleResult =
|
export const parseSampleResult =
|
||||||
(
|
(
|
||||||
|
@ -7,10 +7,10 @@ import {
|
|||||||
import got from 'got'
|
import got from 'got'
|
||||||
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
|
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
|
||||||
import { byId, isDefined, isEmpty } from '@typebot.io/lib'
|
import { byId, isDefined, isEmpty } from '@typebot.io/lib'
|
||||||
import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results'
|
|
||||||
import prisma from '@typebot.io/lib/prisma'
|
import prisma from '@typebot.io/lib/prisma'
|
||||||
import { ExecuteIntegrationResponse } from '../../../types'
|
import { ExecuteIntegrationResponse } from '../../../types'
|
||||||
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
|
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
|
||||||
|
import { parseAnswers } from '@typebot.io/lib/results/parseAnswers'
|
||||||
|
|
||||||
const URL = 'https://api.zemantic.ai/v1/search-documents'
|
const URL = 'https://api.zemantic.ai/v1/search-documents'
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ export const executeZemanticAiBlock = async (
|
|||||||
const { typebot, answers } = newSessionState.typebotsQueue[0]
|
const { typebot, answers } = newSessionState.typebotsQueue[0]
|
||||||
|
|
||||||
const templateVars = parseAnswers({
|
const templateVars = parseAnswers({
|
||||||
variables: getDefinedVariables(typebot.variables),
|
variables: typebot.variables,
|
||||||
answers: answers,
|
answers: answers,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import prisma from '@typebot.io/lib/prisma'
|
import prisma from '@typebot.io/lib/prisma'
|
||||||
import { getDefinedVariables } from '@typebot.io/lib/results'
|
|
||||||
import { TypebotInSession } from '@typebot.io/schemas'
|
import { TypebotInSession } from '@typebot.io/schemas'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -27,7 +26,7 @@ export const createResultIfNotExist = async ({
|
|||||||
typebotId: typebot.id,
|
typebotId: typebot.id,
|
||||||
isCompleted: isCompleted ? true : false,
|
isCompleted: isCompleted ? true : false,
|
||||||
hasStarted,
|
hasStarted,
|
||||||
variables: getDefinedVariables(typebot.variables),
|
variables: typebot.variables,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import prisma from '@typebot.io/lib/prisma'
|
import prisma from '@typebot.io/lib/prisma'
|
||||||
import { getDefinedVariables } from '@typebot.io/lib/results'
|
|
||||||
import { TypebotInSession } from '@typebot.io/schemas'
|
import { TypebotInSession } from '@typebot.io/schemas'
|
||||||
|
import { filterVariablesWithValues } from '@typebot.io/variables/filterVariablesWithValues'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
resultId: string
|
resultId: string
|
||||||
@ -18,7 +18,7 @@ export const upsertResult = async ({
|
|||||||
where: { id: resultId },
|
where: { id: resultId },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
})
|
})
|
||||||
const variablesWithValue = getDefinedVariables(typebot.variables)
|
const variablesWithValue = filterVariablesWithValues(typebot.variables)
|
||||||
|
|
||||||
if (existingResult) {
|
if (existingResult) {
|
||||||
return prisma.result.updateMany({
|
return prisma.result.updateMany({
|
||||||
|
8
packages/lib/parseUniqueKey.ts
Normal file
8
packages/lib/parseUniqueKey.ts
Normal file
@ -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)
|
||||||
|
}
|
@ -1,19 +1,33 @@
|
|||||||
import { Stack, Text } from '@chakra-ui/react'
|
|
||||||
import { isDefined } from '@typebot.io/lib'
|
|
||||||
import {
|
import {
|
||||||
ResultWithAnswers,
|
ResultWithAnswers,
|
||||||
ResultHeaderCell,
|
ResultHeaderCell,
|
||||||
VariableWithValue,
|
VariableWithValue,
|
||||||
Answer,
|
Answer,
|
||||||
|
TableData,
|
||||||
} from '@typebot.io/schemas'
|
} 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 { 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 = (
|
export const convertResultsToTableData = (
|
||||||
results: ResultWithAnswers[] | undefined,
|
results: ResultWithAnswers[] | undefined,
|
||||||
headerCells: ResultHeaderCell[]
|
headerCells: ResultHeaderCell[],
|
||||||
|
cellParser: CellParser = defaultCellParser
|
||||||
): TableData[] =>
|
): TableData[] =>
|
||||||
(results ?? []).map((result) => ({
|
(results ?? []).map((result) => ({
|
||||||
id: { plainText: result.id },
|
id: { plainText: result.id },
|
||||||
@ -39,7 +53,7 @@ export const convertResultsToTableData = (
|
|||||||
const content = variableValue ?? answer.content
|
const content = variableValue ?? answer.content
|
||||||
return {
|
return {
|
||||||
...tableData,
|
...tableData,
|
||||||
[header.id]: parseCellContent(content, header.blockType),
|
[header.id]: cellParser(content, header.blockType),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const variable = answerOrVariable satisfies VariableWithValue
|
const variable = answerOrVariable satisfies VariableWithValue
|
||||||
@ -51,30 +65,15 @@ export const convertResultsToTableData = (
|
|||||||
if (isDefined(tableData[headerId])) return tableData
|
if (isDefined(tableData[headerId])) return tableData
|
||||||
return {
|
return {
|
||||||
...tableData,
|
...tableData,
|
||||||
[headerId]: parseCellContent(variable.value),
|
[headerId]: cellParser(variable.value),
|
||||||
}
|
}
|
||||||
}, {}),
|
}, {}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const parseCellContent = (
|
const convertDateToReadable = (date: Date): string =>
|
||||||
content: VariableWithValue['value'],
|
date.toDateString().split(' ').slice(1, 3).join(' ') +
|
||||||
blockType?: InputBlockType
|
', ' +
|
||||||
): { element?: JSX.Element; plainText: string } => {
|
date.toLocaleTimeString([], {
|
||||||
if (!content) return { element: undefined, plainText: '' }
|
hour: '2-digit',
|
||||||
if (Array.isArray(content))
|
minute: '2-digit',
|
||||||
return {
|
})
|
||||||
element: (
|
|
||||||
<Stack spacing={2}>
|
|
||||||
{content.map((item, idx) => (
|
|
||||||
<Text key={idx}>
|
|
||||||
{idx + 1}. {item}
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
),
|
|
||||||
plainText: content.join(', '),
|
|
||||||
}
|
|
||||||
return blockType === InputBlockType.FILE
|
|
||||||
? { element: <FileLinks fileNamesStr={content} />, plainText: content }
|
|
||||||
: { plainText: content.toString() }
|
|
||||||
}
|
|
39
packages/lib/results/parseAnswers.ts
Normal file
39
packages/lib/results/parseAnswers.ts
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}, {}),
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { ResultHeaderCell } from '@typebot.io/schemas'
|
import { ResultHeaderCell } from '@typebot.io/schemas'
|
||||||
|
|
||||||
export const parseColumnOrder = (
|
export const parseColumnsOrder = (
|
||||||
existingOrder: string[] | undefined,
|
existingOrder: string[] | undefined,
|
||||||
resultHeader: ResultHeaderCell[]
|
resultHeader: ResultHeaderCell[]
|
||||||
) =>
|
) =>
|
@ -1,15 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
|
ResultWithAnswers,
|
||||||
|
ResultHeaderCell,
|
||||||
Group,
|
Group,
|
||||||
Variable,
|
Variable,
|
||||||
InputBlock,
|
InputBlock,
|
||||||
ResultHeaderCell,
|
|
||||||
VariableWithValue,
|
|
||||||
Typebot,
|
Typebot,
|
||||||
ResultWithAnswers,
|
|
||||||
AnswerInSessionState,
|
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { isInputBlock, isDefined, byId, isNotEmpty, isEmpty } from './utils'
|
|
||||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||||
|
import { isInputBlock, byId, isNotEmpty } from '../utils'
|
||||||
|
|
||||||
export const parseResultHeader = (
|
export const parseResultHeader = (
|
||||||
typebot: Pick<Typebot, 'groups' | 'variables'>,
|
typebot: Pick<Typebot, 'groups' | 'variables'>,
|
||||||
@ -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[]
|
|
@ -72,3 +72,9 @@ export type ResultHeaderCell = {
|
|||||||
blockType?: InputBlockType
|
blockType?: InputBlockType
|
||||||
variableIds?: string[]
|
variableIds?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CellValueType = { element?: JSX.Element; plainText: string }
|
||||||
|
|
||||||
|
export type TableData = {
|
||||||
|
id: Pick<CellValueType, 'plainText'>
|
||||||
|
} & Record<string, CellValueType>
|
||||||
|
119
packages/scripts/exportResults.ts
Normal file
119
packages/scripts/exportResults.ts
Normal file
@ -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<string[]>((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()
|
@ -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()
|
|
@ -25,14 +25,17 @@
|
|||||||
"updateWorkspace": "tsx updateWorkspace.ts",
|
"updateWorkspace": "tsx updateWorkspace.ts",
|
||||||
"inspectTypebot": "tsx inspectTypebot.ts",
|
"inspectTypebot": "tsx inspectTypebot.ts",
|
||||||
"inspectWorkspace": "tsx inspectWorkspace.ts",
|
"inspectWorkspace": "tsx inspectWorkspace.ts",
|
||||||
"getCoupon": "tsx getCoupon.ts"
|
"getCoupon": "tsx getCoupon.ts",
|
||||||
|
"exportResults": "tsx exportResults.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typebot.io/emails": "workspace:*",
|
"@typebot.io/emails": "workspace:*",
|
||||||
"@typebot.io/lib": "workspace:*",
|
"@typebot.io/lib": "workspace:*",
|
||||||
"@typebot.io/prisma": "workspace:*",
|
"@typebot.io/prisma": "workspace:*",
|
||||||
"@typebot.io/schemas": "workspace:*",
|
"@typebot.io/schemas": "workspace:*",
|
||||||
|
"@types/cli-progress": "^3.11.5",
|
||||||
"@types/node": "20.4.2",
|
"@types/node": "20.4.2",
|
||||||
|
"@types/papaparse": "5.3.7",
|
||||||
"@types/prompts": "2.4.4",
|
"@types/prompts": "2.4.4",
|
||||||
"deep-object-diff": "1.1.9",
|
"deep-object-diff": "1.1.9",
|
||||||
"got": "12.6.0",
|
"got": "12.6.0",
|
||||||
@ -44,6 +47,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.7.0",
|
"@clack/prompts": "^0.7.0",
|
||||||
"@paralleldrive/cuid2": "2.2.1"
|
"@paralleldrive/cuid2": "2.2.1",
|
||||||
|
"cli-progress": "^3.12.0",
|
||||||
|
"papaparse": "5.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
9
packages/variables/filterVariablesWithValues.ts
Normal file
9
packages/variables/filterVariablesWithValues.ts
Normal file
@ -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[]
|
27
pnpm-lock.yaml
generated
27
pnpm-lock.yaml
generated
@ -1481,6 +1481,12 @@ importers:
|
|||||||
'@paralleldrive/cuid2':
|
'@paralleldrive/cuid2':
|
||||||
specifier: 2.2.1
|
specifier: 2.2.1
|
||||||
version: 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:
|
devDependencies:
|
||||||
'@typebot.io/emails':
|
'@typebot.io/emails':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
@ -1494,9 +1500,15 @@ importers:
|
|||||||
'@typebot.io/schemas':
|
'@typebot.io/schemas':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../schemas
|
version: link:../schemas
|
||||||
|
'@types/cli-progress':
|
||||||
|
specifier: ^3.11.5
|
||||||
|
version: 3.11.5
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: 20.4.2
|
specifier: 20.4.2
|
||||||
version: 20.4.2
|
version: 20.4.2
|
||||||
|
'@types/papaparse':
|
||||||
|
specifier: 5.3.7
|
||||||
|
version: 5.3.7
|
||||||
'@types/prompts':
|
'@types/prompts':
|
||||||
specifier: 2.4.4
|
specifier: 2.4.4
|
||||||
version: 2.4.4
|
version: 2.4.4
|
||||||
@ -7735,6 +7747,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-Yq6rIccwcco0TLD5SMUrIM7Fk7Fe/C0jmNRxJJCLtAF6gebDkPuUjK5EHedxecm69Pi/aA+It39Ux4OHmFhjRw==}
|
resolution: {integrity: sha512-Yq6rIccwcco0TLD5SMUrIM7Fk7Fe/C0jmNRxJJCLtAF6gebDkPuUjK5EHedxecm69Pi/aA+It39Ux4OHmFhjRw==}
|
||||||
dev: true
|
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:
|
/@types/content-type@1.1.6:
|
||||||
resolution: {integrity: sha512-WFHg/KFLCdUQl3m27WSQu0NEaLzoHGmgZHlsSYr0Y0iIvItMcBq7opZc6AGXPXqf+btIM6vTBJyLvuDAihB+zQ==}
|
resolution: {integrity: sha512-WFHg/KFLCdUQl3m27WSQu0NEaLzoHGmgZHlsSYr0Y0iIvItMcBq7opZc6AGXPXqf+btIM6vTBJyLvuDAihB+zQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -7925,7 +7943,7 @@ packages:
|
|||||||
/@types/papaparse@5.3.7:
|
/@types/papaparse@5.3.7:
|
||||||
resolution: {integrity: sha512-f2HKmlnPdCvS0WI33WtCs5GD7X1cxzzS/aduaxSu3I7TbhWlENjSPs6z5TaB9K0J+BH1jbmqTaM+ja5puis4wg==}
|
resolution: {integrity: sha512-f2HKmlnPdCvS0WI33WtCs5GD7X1cxzzS/aduaxSu3I7TbhWlENjSPs6z5TaB9K0J+BH1jbmqTaM+ja5puis4wg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.4.9
|
'@types/node': 20.10.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/parse-json@4.0.0:
|
/@types/parse-json@4.0.0:
|
||||||
@ -10831,6 +10849,13 @@ packages:
|
|||||||
restore-cursor: 4.0.0
|
restore-cursor: 4.0.0
|
||||||
dev: true
|
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:
|
/cli-spinners@2.9.2:
|
||||||
resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
|
resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
Reference in New Issue
Block a user