feat(results): ✨ Add logs in results
This commit is contained in:
@@ -362,3 +362,12 @@ export const UsersIcon = (props: IconProps) => (
|
|||||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const AlignLeftTextIcon = (props: IconProps) => (
|
||||||
|
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||||
|
<line x1="17" y1="10" x2="3" y2="10"></line>
|
||||||
|
<line x1="21" y1="6" x2="3" y2="6"></line>
|
||||||
|
<line x1="21" y1="14" x2="3" y2="14"></line>
|
||||||
|
<line x1="17" y1="18" x2="3" y2="18"></line>
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
FlexProps,
|
FlexProps,
|
||||||
useEventListener,
|
useEventListener,
|
||||||
useToast,
|
useToast,
|
||||||
|
UseToastOptions,
|
||||||
VStack,
|
VStack,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { TypebotViewer } from 'bot-engine'
|
import { TypebotViewer } from 'bot-engine'
|
||||||
@@ -14,7 +15,8 @@ import { headerHeight } from 'components/shared/TypebotHeader'
|
|||||||
import { useEditor } from 'contexts/EditorContext'
|
import { useEditor } from 'contexts/EditorContext'
|
||||||
import { useGraph } from 'contexts/GraphContext'
|
import { useGraph } from 'contexts/GraphContext'
|
||||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||||
import React, { useEffect, useMemo, useState } from 'react'
|
import { Log } from 'db'
|
||||||
|
import React, { useMemo, useState } from 'react'
|
||||||
import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
|
import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
|
||||||
|
|
||||||
export const PreviewDrawer = () => {
|
export const PreviewDrawer = () => {
|
||||||
@@ -57,20 +59,10 @@ export const PreviewDrawer = () => {
|
|||||||
setRightPanel(undefined)
|
setRightPanel(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const handleNewLog = (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => {
|
||||||
const onMessageFromBot = (event: MessageEvent) => {
|
toast(log as UseToastOptions)
|
||||||
if (event.data.typebotInfo) {
|
console.log(log.details)
|
||||||
toast({ description: event.data.typebotInfo })
|
}
|
||||||
}
|
|
||||||
if (event.data.typebotError) {
|
|
||||||
toast({ description: event.data.typebotError, status: 'error' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.addEventListener('message', onMessageFromBot)
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('message', onMessageFromBot)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
@@ -113,6 +105,7 @@ export const PreviewDrawer = () => {
|
|||||||
<TypebotViewer
|
<TypebotViewer
|
||||||
typebot={publicTypebot}
|
typebot={publicTypebot}
|
||||||
onNewBlockVisible={setPreviewingEdge}
|
onNewBlockVisible={setPreviewingEdge}
|
||||||
|
onNewLog={handleNewLog}
|
||||||
isPreview
|
isPreview
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable react/jsx-key */
|
/* eslint-disable react/jsx-key */
|
||||||
import { chakra, Checkbox, Flex } from '@chakra-ui/react'
|
import { Button, chakra, Checkbox, Flex, HStack, Text } from '@chakra-ui/react'
|
||||||
|
import { AlignLeftTextIcon } from 'assets/icons'
|
||||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||||
import React, { useEffect, useMemo, useRef } from 'react'
|
import React, { useEffect, useMemo, useRef } from 'react'
|
||||||
import { Hooks, useRowSelect, useTable } from 'react-table'
|
import { Hooks, useRowSelect, useTable } from 'react-table'
|
||||||
@@ -12,6 +13,7 @@ type SubmissionsTableProps = {
|
|||||||
hasMore?: boolean
|
hasMore?: boolean
|
||||||
onNewSelection: (indices: number[]) => void
|
onNewSelection: (indices: number[]) => void
|
||||||
onScrollToBottom: () => void
|
onScrollToBottom: () => void
|
||||||
|
onLogOpenIndex: (index: number) => () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SubmissionsTable = ({
|
export const SubmissionsTable = ({
|
||||||
@@ -19,13 +21,13 @@ export const SubmissionsTable = ({
|
|||||||
hasMore,
|
hasMore,
|
||||||
onNewSelection,
|
onNewSelection,
|
||||||
onScrollToBottom,
|
onScrollToBottom,
|
||||||
|
onLogOpenIndex,
|
||||||
}: SubmissionsTableProps) => {
|
}: SubmissionsTableProps) => {
|
||||||
const { publishedTypebot } = useTypebot()
|
const { publishedTypebot } = useTypebot()
|
||||||
const columns: any = useMemo(
|
const columns: any = useMemo(
|
||||||
() => (publishedTypebot ? parseSubmissionsColumns(publishedTypebot) : []),
|
() => (publishedTypebot ? parseSubmissionsColumns(publishedTypebot) : []),
|
||||||
[publishedTypebot]
|
[publishedTypebot]
|
||||||
)
|
)
|
||||||
|
|
||||||
const bottomElement = useRef<HTMLDivElement | null>(null)
|
const bottomElement = useRef<HTMLDivElement | null>(null)
|
||||||
const tableWrapper = useRef<HTMLDivElement | null>(null)
|
const tableWrapper = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
@@ -87,6 +89,19 @@ export const SubmissionsTable = ({
|
|||||||
</chakra.th>
|
</chakra.th>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
<chakra.th
|
||||||
|
px="4"
|
||||||
|
py="2"
|
||||||
|
border="1px"
|
||||||
|
borderColor="gray.200"
|
||||||
|
fontWeight="normal"
|
||||||
|
whiteSpace="nowrap"
|
||||||
|
>
|
||||||
|
<HStack>
|
||||||
|
<AlignLeftTextIcon />
|
||||||
|
<Text>Logs</Text>
|
||||||
|
</HStack>
|
||||||
|
</chakra.th>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -118,10 +133,17 @@ export const SubmissionsTable = ({
|
|||||||
</chakra.td>
|
</chakra.td>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
<chakra.td px="4" py="2" border="1px" borderColor="gray.200">
|
||||||
|
<Button size="sm" onClick={onLogOpenIndex(idx)}>
|
||||||
|
See logs
|
||||||
|
</Button>
|
||||||
|
</chakra.td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
{hasMore === true && <LoadingRows totalColumns={columns.length} />}
|
{hasMore === true && (
|
||||||
|
<LoadingRows totalColumns={columns.length + 1} />
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</chakra.table>
|
</chakra.table>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
99
apps/builder/layouts/results/LogsModal.tsx
Normal file
99
apps/builder/layouts/results/LogsModal.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalBody,
|
||||||
|
Stack,
|
||||||
|
Spinner,
|
||||||
|
ModalFooter,
|
||||||
|
Accordion,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionButton,
|
||||||
|
HStack,
|
||||||
|
AccordionIcon,
|
||||||
|
AccordionPanel,
|
||||||
|
Text,
|
||||||
|
Tag,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { Log } from 'db'
|
||||||
|
import { useLogs } from 'services/typebots/logs'
|
||||||
|
import { isDefined } from 'utils'
|
||||||
|
|
||||||
|
export const LogsModal = ({
|
||||||
|
resultId,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
resultId?: string
|
||||||
|
onClose: () => void
|
||||||
|
}) => {
|
||||||
|
const { isLoading, logs } = useLogs(resultId)
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isDefined(resultId)} onClose={onClose} size="xl">
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>Logs</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody as={Stack}>
|
||||||
|
{logs?.map((log, idx) => (
|
||||||
|
<LogCard key={idx} log={log} />
|
||||||
|
))}
|
||||||
|
{isLoading && <Spinner />}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const LogCard = ({ log }: { log: Log }) => {
|
||||||
|
if (log.details)
|
||||||
|
return (
|
||||||
|
<Accordion allowToggle>
|
||||||
|
<AccordionItem style={{ borderBottomWidth: 0, borderWidth: 0 }}>
|
||||||
|
<AccordionButton
|
||||||
|
as={HStack}
|
||||||
|
p="4"
|
||||||
|
cursor="pointer"
|
||||||
|
justifyContent="space-between"
|
||||||
|
borderRadius="md"
|
||||||
|
>
|
||||||
|
<HStack>
|
||||||
|
<StatusTag status={log.status} />
|
||||||
|
<Text>{log.description}</Text>
|
||||||
|
</HStack>
|
||||||
|
<AccordionIcon />
|
||||||
|
</AccordionButton>
|
||||||
|
<AccordionPanel
|
||||||
|
as="pre"
|
||||||
|
overflow="scroll"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderRadius="md"
|
||||||
|
>
|
||||||
|
{log.details}
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<HStack p="4">
|
||||||
|
<StatusTag status={log.status} />
|
||||||
|
<Text>{log.description}</Text>
|
||||||
|
</HStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusTag = ({ status }: { status: string }) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'error':
|
||||||
|
return <Tag colorScheme={'red'}>Fail</Tag>
|
||||||
|
case 'warning':
|
||||||
|
return <Tag colorScheme={'orange'}>Warn</Tag>
|
||||||
|
case 'info':
|
||||||
|
return <Tag colorScheme={'blue'}>Info</Tag>
|
||||||
|
default:
|
||||||
|
return <Tag colorScheme={'green'}>Ok</Tag>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from 'services/typebots'
|
} from 'services/typebots'
|
||||||
import { unparse } from 'papaparse'
|
import { unparse } from 'papaparse'
|
||||||
import { UnlockProPlanInfo } from 'components/shared/Info'
|
import { UnlockProPlanInfo } from 'components/shared/Info'
|
||||||
|
import { LogsModal } from './LogsModal'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
typebotId: string
|
typebotId: string
|
||||||
@@ -27,6 +28,7 @@ export const SubmissionsContent = ({
|
|||||||
const [selectedIndices, setSelectedIndices] = useState<number[]>([])
|
const [selectedIndices, setSelectedIndices] = useState<number[]>([])
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false)
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false)
|
||||||
const [isExportLoading, setIsExportLoading] = useState(false)
|
const [isExportLoading, setIsExportLoading] = useState(false)
|
||||||
|
const [inspectingLogsResultId, setInspectingLogsResultId] = useState<string>()
|
||||||
|
|
||||||
const toast = useToast({
|
const toast = useToast({
|
||||||
position: 'top-right',
|
position: 'top-right',
|
||||||
@@ -109,6 +111,13 @@ export const SubmissionsContent = ({
|
|||||||
[results]
|
[results]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleLogsModalClose = () => setInspectingLogsResultId(undefined)
|
||||||
|
|
||||||
|
const handleLogOpenIndex = (index: number) => () => {
|
||||||
|
if (!results) return
|
||||||
|
setInspectingLogsResultId(results[index].id)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack maxW="1200px" w="full" pb="28">
|
<Stack maxW="1200px" w="full" pb="28">
|
||||||
{totalHiddenResults && (
|
{totalHiddenResults && (
|
||||||
@@ -117,6 +126,10 @@ export const SubmissionsContent = ({
|
|||||||
contentLabel="You are seeing complete submissions only."
|
contentLabel="You are seeing complete submissions only."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<LogsModal
|
||||||
|
resultId={inspectingLogsResultId}
|
||||||
|
onClose={handleLogsModalClose}
|
||||||
|
/>
|
||||||
<Flex w="full" justifyContent="flex-end">
|
<Flex w="full" justifyContent="flex-end">
|
||||||
<ResultsActionButtons
|
<ResultsActionButtons
|
||||||
isDeleteLoading={isDeleteLoading}
|
isDeleteLoading={isDeleteLoading}
|
||||||
@@ -132,6 +145,7 @@ export const SubmissionsContent = ({
|
|||||||
onNewSelection={handleNewSelection}
|
onNewSelection={handleNewSelection}
|
||||||
onScrollToBottom={handleScrolledToBottom}
|
onScrollToBottom={handleScrolledToBottom}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
|
onLogOpenIndex={handleLogOpenIndex}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
typebotId,
|
typebotId,
|
||||||
typebot: { ownerId: user.email === adminEmail ? undefined : user.id },
|
typebot: { ownerId: user.email === adminEmail ? undefined : user.id },
|
||||||
answers: { some: {} },
|
answers: { some: {} },
|
||||||
isCompleted: isFreePlan(user) ? false : undefined,
|
isCompleted: isFreePlan(user) ? true : undefined,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc',
|
createdAt: 'desc',
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { withSentry } from '@sentry/nextjs'
|
||||||
|
import prisma from 'libs/prisma'
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
import { getAuthenticatedUser } from 'services/api/utils'
|
||||||
|
import { methodNotAllowed, notAuthenticated } from 'utils'
|
||||||
|
|
||||||
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
const user = await getAuthenticatedUser(req)
|
||||||
|
if (!user) return notAuthenticated(res)
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
const resultId = req.query.resultId as string
|
||||||
|
const logs = await prisma.log.findMany({ where: { resultId } })
|
||||||
|
return res.send({ logs })
|
||||||
|
}
|
||||||
|
methodNotAllowed(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withSentry(handler)
|
||||||
15
apps/builder/services/typebots/logs.ts
Normal file
15
apps/builder/services/typebots/logs.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Log } from 'db'
|
||||||
|
import { fetcher } from 'services/utils'
|
||||||
|
import useSWR from 'swr'
|
||||||
|
|
||||||
|
export const useLogs = (resultId?: string, onError?: (e: Error) => void) => {
|
||||||
|
const { data, error } = useSWR<{ logs: Log[] }>(
|
||||||
|
resultId ? `/api/typebots/t/results/${resultId}/logs` : null,
|
||||||
|
fetcher
|
||||||
|
)
|
||||||
|
if (error && onError) onError(error)
|
||||||
|
return {
|
||||||
|
logs: data?.logs,
|
||||||
|
isLoading: !error && !data,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { TypebotViewer } from 'bot-engine'
|
import { TypebotViewer } from 'bot-engine'
|
||||||
|
import { Log } from 'db'
|
||||||
import { Answer, PublicTypebot, VariableWithValue } from 'models'
|
import { Answer, PublicTypebot, VariableWithValue } from 'models'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { upsertAnswer } from 'services/answer'
|
import { upsertAnswer } from 'services/answer'
|
||||||
import { SEO } from '../components/Seo'
|
import { SEO } from '../components/Seo'
|
||||||
import { createResult, updateResult } from '../services/result'
|
import { createLog, createResult, updateResult } from '../services/result'
|
||||||
import { ErrorPage } from './ErrorPage'
|
import { ErrorPage } from './ErrorPage'
|
||||||
|
|
||||||
export type TypebotPageProps = {
|
export type TypebotPageProps = {
|
||||||
@@ -58,6 +59,14 @@ export const TypebotPage = ({
|
|||||||
if (error) setError(error)
|
if (error) setError(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleNewLog = async (
|
||||||
|
log: Omit<Log, 'id' | 'createdAt' | 'resultId'>
|
||||||
|
) => {
|
||||||
|
if (!resultId) return setError(new Error('Result was not created'))
|
||||||
|
const { error } = await createLog(resultId, log)
|
||||||
|
if (error) setError(error)
|
||||||
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorPage error={error} />
|
return <ErrorPage error={error} />
|
||||||
}
|
}
|
||||||
@@ -74,6 +83,7 @@ export const TypebotPage = ({
|
|||||||
onNewAnswer={handleNewAnswer}
|
onNewAnswer={handleNewAnswer}
|
||||||
onCompleted={handleCompleted}
|
onCompleted={handleCompleted}
|
||||||
onVariablesPrefilled={initializeResult}
|
onVariablesPrefilled={initializeResult}
|
||||||
|
onNewLog={handleNewLog}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { withSentry } from '@sentry/nextjs'
|
|
||||||
import prisma from 'libs/prisma'
|
|
||||||
import { VariableWithValue } from 'models'
|
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import { methodNotAllowed } from 'utils'
|
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|
||||||
if (req.method === 'POST') {
|
|
||||||
const resultData = (
|
|
||||||
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
|
||||||
) as {
|
|
||||||
typebotId: string
|
|
||||||
prefilledVariables: VariableWithValue[]
|
|
||||||
}
|
|
||||||
const result = await prisma.result.create({
|
|
||||||
data: { ...resultData, isCompleted: false },
|
|
||||||
})
|
|
||||||
return res.send(result)
|
|
||||||
}
|
|
||||||
return methodNotAllowed(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withSentry(handler)
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import prisma from 'libs/prisma'
|
import prisma from 'libs/prisma'
|
||||||
import { ResultWithAnswers, Typebot } from 'models'
|
import { ResultWithAnswers, Typebot, VariableWithValue } from 'models'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { authenticateUser } from 'services/api/utils'
|
import { authenticateUser } from 'services/api/utils'
|
||||||
import { methodNotAllowed, parseAnswers } from 'utils'
|
import { methodNotAllowed, parseAnswers } from 'utils'
|
||||||
@@ -14,16 +14,30 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
})
|
})
|
||||||
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
|
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
|
||||||
const limit = Number(req.query.limit)
|
const limit = Number(req.query.limit)
|
||||||
|
console.log(limit, typebot.id)
|
||||||
const results = (await prisma.result.findMany({
|
const results = (await prisma.result.findMany({
|
||||||
where: { typebotId: typebot.id },
|
where: { typebotId: typebot.id },
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
take: limit,
|
take: limit,
|
||||||
include: { answers: true },
|
include: { answers: true },
|
||||||
})) as unknown as ResultWithAnswers[]
|
})) as unknown as ResultWithAnswers[]
|
||||||
res.send({
|
console.log(results)
|
||||||
|
return res.send({
|
||||||
results: results.map(parseAnswers(typebot as unknown as Typebot)),
|
results: results.map(parseAnswers(typebot as unknown as Typebot)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
const typebotId = req.query.typebotId as string
|
||||||
|
const resultData = (
|
||||||
|
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||||
|
) as {
|
||||||
|
prefilledVariables: VariableWithValue[]
|
||||||
|
}
|
||||||
|
const result = await prisma.result.create({
|
||||||
|
data: { ...resultData, typebotId, isCompleted: false },
|
||||||
|
})
|
||||||
|
return res.send(result)
|
||||||
|
}
|
||||||
methodNotAllowed(res)
|
methodNotAllowed(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
const data = (
|
const data = (
|
||||||
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||||
) as { isCompleted: true }
|
) as { isCompleted: true }
|
||||||
const id = req.query.id.toString()
|
const resultId = req.query.resultId as string
|
||||||
const result = await prisma.result.update({
|
const result = await prisma.result.update({
|
||||||
where: { id },
|
where: { id: resultId },
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
return res.send(result)
|
return res.send(result)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { withSentry } from '@sentry/nextjs'
|
||||||
|
import { Log } from 'db'
|
||||||
|
import prisma from 'libs/prisma'
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
import { badRequest, methodNotAllowed } from 'utils'
|
||||||
|
|
||||||
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
const resultId = req.query.resultId as string
|
||||||
|
const log = req.body as
|
||||||
|
| Omit<Log, 'id' | 'createdAt' | 'resultId'>
|
||||||
|
| undefined
|
||||||
|
if (!log) return badRequest(res)
|
||||||
|
const createdLog = await prisma.log.create({ data: { ...log, resultId } })
|
||||||
|
return res.send(createdLog)
|
||||||
|
}
|
||||||
|
methodNotAllowed(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withSentry(handler)
|
||||||
@@ -15,7 +15,9 @@ test.beforeAll(async () => {
|
|||||||
)
|
)
|
||||||
await createWebhook(typebotId)
|
await createWebhook(typebotId)
|
||||||
await createResults({ typebotId })
|
await createResults({ typebotId })
|
||||||
} catch (err) {}
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test('can list typebots', async ({ request }) => {
|
test('can list typebots', async ({ request }) => {
|
||||||
@@ -112,10 +114,10 @@ test('can get a sample result', async ({ request }) => {
|
|||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
expect(data).toMatchObject({
|
expect(data).toMatchObject({
|
||||||
message: 'This is a sample result, it has been generated ⬇️',
|
message: 'This is a sample result, it has been generated ⬇️',
|
||||||
Welcome: 'Item 1, Item 2, Item3',
|
Welcome: 'Hi!',
|
||||||
Email: 'test@email.com',
|
Email: 'test@email.com',
|
||||||
Name: 'answer value',
|
Name: 'answer value',
|
||||||
Services: 'Item 1, Item 2, Item3',
|
Services: 'Website dev, Content Marketing, Social Media, UI / UX Design',
|
||||||
'Additional information': 'answer value',
|
'Additional information': 'answer value',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
InputStepType,
|
InputStepType,
|
||||||
Metadata,
|
Metadata,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
|
import { typebotViewer } from '../services/selectorUtils'
|
||||||
|
|
||||||
test('Should correctly parse metadata', async ({ page }) => {
|
test('Should correctly parse metadata', async ({ page }) => {
|
||||||
const typebotId = generate()
|
const typebotId = generate()
|
||||||
@@ -48,4 +49,9 @@ test('Should correctly parse metadata', async ({ page }) => {
|
|||||||
(document.querySelector('link[rel="icon"]') as any).getAttribute('href')
|
(document.querySelector('link[rel="icon"]') as any).getAttribute('href')
|
||||||
)
|
)
|
||||||
).toBe(customMetadata.favIconUrl)
|
).toBe(customMetadata.favIconUrl)
|
||||||
|
await expect(
|
||||||
|
typebotViewer(page).locator(
|
||||||
|
`input[placeholder="${defaultTextInputOptions.labels.placeholder}"]`
|
||||||
|
)
|
||||||
|
).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Answer } from 'models'
|
import { Answer } from 'models'
|
||||||
import { sendRequest } from 'utils'
|
import { sendRequest } from 'utils'
|
||||||
|
|
||||||
export const upsertAnswer = async (answer: Answer & { resultId: string }) => {
|
export const upsertAnswer = async (answer: Answer & { resultId: string }) =>
|
||||||
return sendRequest<Answer>({
|
sendRequest<Answer>({
|
||||||
url: `/api/answers`,
|
url: `/api/typebots/t/results/r/answers`,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: answer,
|
body: answer,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Result } from 'db'
|
import { Log, Result } from 'db'
|
||||||
import { VariableWithValue } from 'models'
|
import { VariableWithValue } from 'models'
|
||||||
import { sendRequest } from 'utils'
|
import { sendRequest } from 'utils'
|
||||||
|
|
||||||
@@ -7,19 +7,25 @@ export const createResult = async (
|
|||||||
prefilledVariables: VariableWithValue[]
|
prefilledVariables: VariableWithValue[]
|
||||||
) => {
|
) => {
|
||||||
return sendRequest<Result>({
|
return sendRequest<Result>({
|
||||||
url: `/api/results`,
|
url: `/api/typebots/${typebotId}/results`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { typebotId, prefilledVariables },
|
body: { prefilledVariables },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateResult = async (
|
export const updateResult = async (resultId: string, result: Partial<Result>) =>
|
||||||
resultId: string,
|
sendRequest<Result>({
|
||||||
result: Partial<Result>
|
url: `/api/typebots/t/results/${resultId}`,
|
||||||
) => {
|
|
||||||
return sendRequest<Result>({
|
|
||||||
url: `/api/results/${resultId}`,
|
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: result,
|
body: result,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
export const createLog = (
|
||||||
|
resultId: string,
|
||||||
|
log: Omit<Log, 'id' | 'createdAt' | 'resultId'>
|
||||||
|
) =>
|
||||||
|
sendRequest<Result>({
|
||||||
|
url: `/api/typebots/t/results/${resultId}/logs`,
|
||||||
|
method: 'POST',
|
||||||
|
body: log,
|
||||||
|
})
|
||||||
|
|||||||
@@ -31,8 +31,14 @@ export const ChatBlock = ({
|
|||||||
onScroll,
|
onScroll,
|
||||||
onBlockEnd,
|
onBlockEnd,
|
||||||
}: ChatBlockProps) => {
|
}: ChatBlockProps) => {
|
||||||
const { typebot, updateVariableValue, createEdge, apiHost, isPreview } =
|
const {
|
||||||
useTypebot()
|
typebot,
|
||||||
|
updateVariableValue,
|
||||||
|
createEdge,
|
||||||
|
apiHost,
|
||||||
|
isPreview,
|
||||||
|
onNewLog,
|
||||||
|
} = useTypebot()
|
||||||
const { resultValues } = useAnswers()
|
const { resultValues } = useAnswers()
|
||||||
const [displayedSteps, setDisplayedSteps] = useState<Step[]>([])
|
const [displayedSteps, setDisplayedSteps] = useState<Step[]>([])
|
||||||
|
|
||||||
@@ -72,6 +78,7 @@ export const ChatBlock = ({
|
|||||||
updateVariableValue,
|
updateVariableValue,
|
||||||
resultValues,
|
resultValues,
|
||||||
blocks: typebot.blocks,
|
blocks: typebot.blocks,
|
||||||
|
onNewLog,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
nextEdgeId ? onBlockEnd(nextEdgeId) : displayNextStep()
|
nextEdgeId ? onBlockEnd(nextEdgeId) : displayNextStep()
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { ChatBlock } from './ChatBlock/ChatBlock'
|
|||||||
import { useFrame } from 'react-frame-component'
|
import { useFrame } from 'react-frame-component'
|
||||||
import { setCssVariablesValue } from '../services/theme'
|
import { setCssVariablesValue } from '../services/theme'
|
||||||
import { useAnswers } from '../contexts/AnswersContext'
|
import { useAnswers } from '../contexts/AnswersContext'
|
||||||
import { deepEqual } from 'fast-equals'
|
import { Block, Edge, Theme, VariableWithValue } from 'models'
|
||||||
import { Answer, Block, Edge, Theme, VariableWithValue } from 'models'
|
|
||||||
import { byId, isNotDefined } from 'utils'
|
import { byId, isNotDefined } from 'utils'
|
||||||
import { animateScroll as scroll } from 'react-scroll'
|
import { animateScroll as scroll } from 'react-scroll'
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
@@ -13,14 +12,12 @@ import { useTypebot } from 'contexts/TypebotContext'
|
|||||||
type Props = {
|
type Props = {
|
||||||
theme: Theme
|
theme: Theme
|
||||||
onNewBlockVisible: (edge: Edge) => void
|
onNewBlockVisible: (edge: Edge) => void
|
||||||
onNewAnswer: (answer: Answer) => void
|
|
||||||
onCompleted: () => void
|
onCompleted: () => void
|
||||||
onVariablesPrefilled?: (prefilledVariables: VariableWithValue[]) => void
|
onVariablesPrefilled?: (prefilledVariables: VariableWithValue[]) => void
|
||||||
}
|
}
|
||||||
export const ConversationContainer = ({
|
export const ConversationContainer = ({
|
||||||
theme,
|
theme,
|
||||||
onNewBlockVisible,
|
onNewBlockVisible,
|
||||||
onNewAnswer,
|
|
||||||
onCompleted,
|
onCompleted,
|
||||||
onVariablesPrefilled,
|
onVariablesPrefilled,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
@@ -29,11 +26,7 @@ export const ConversationContainer = ({
|
|||||||
const [displayedBlocks, setDisplayedBlocks] = useState<
|
const [displayedBlocks, setDisplayedBlocks] = useState<
|
||||||
{ block: Block; startStepIndex: number }[]
|
{ block: Block; startStepIndex: number }[]
|
||||||
>([])
|
>([])
|
||||||
const [localAnswer, setLocalAnswer] = useState<Answer | undefined>()
|
const { setPrefilledVariables } = useAnswers()
|
||||||
const {
|
|
||||||
resultValues: { answers },
|
|
||||||
setPrefilledVariables,
|
|
||||||
} = useAnswers()
|
|
||||||
const bottomAnchor = useRef<HTMLDivElement | null>(null)
|
const bottomAnchor = useRef<HTMLDivElement | null>(null)
|
||||||
const scrollableContainer = useRef<HTMLDivElement | null>(null)
|
const scrollableContainer = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
@@ -80,14 +73,6 @@ export const ConversationContainer = ({
|
|||||||
setCssVariablesValue(theme, frameDocument.body.style)
|
setCssVariablesValue(theme, frameDocument.body.style)
|
||||||
}, [theme, frameDocument])
|
}, [theme, frameDocument])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const answer = [...answers].pop()
|
|
||||||
if (!answer || deepEqual(localAnswer, answer)) return
|
|
||||||
setLocalAnswer(answer)
|
|
||||||
onNewAnswer(answer)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [answers])
|
|
||||||
|
|
||||||
const autoScrollToBottom = () => {
|
const autoScrollToBottom = () => {
|
||||||
if (!scrollableContainer.current) return
|
if (!scrollableContainer.current) return
|
||||||
scroll.scrollToBottom({
|
scroll.scrollToBottom({
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
PublicTypebot,
|
PublicTypebot,
|
||||||
VariableWithValue,
|
VariableWithValue,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
|
import { Log } from 'db'
|
||||||
|
|
||||||
export type TypebotViewerProps = {
|
export type TypebotViewerProps = {
|
||||||
typebot: PublicTypebot
|
typebot: PublicTypebot
|
||||||
@@ -24,6 +25,7 @@ export type TypebotViewerProps = {
|
|||||||
apiHost?: string
|
apiHost?: string
|
||||||
onNewBlockVisible?: (edge: Edge) => void
|
onNewBlockVisible?: (edge: Edge) => void
|
||||||
onNewAnswer?: (answer: Answer) => void
|
onNewAnswer?: (answer: Answer) => void
|
||||||
|
onNewLog?: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
|
||||||
onCompleted?: () => void
|
onCompleted?: () => void
|
||||||
onVariablesPrefilled?: (prefilledVariables: VariableWithValue[]) => void
|
onVariablesPrefilled?: (prefilledVariables: VariableWithValue[]) => void
|
||||||
}
|
}
|
||||||
@@ -31,6 +33,7 @@ export const TypebotViewer = ({
|
|||||||
typebot,
|
typebot,
|
||||||
apiHost = process.env.NEXT_PUBLIC_VIEWER_HOST,
|
apiHost = process.env.NEXT_PUBLIC_VIEWER_HOST,
|
||||||
isPreview = false,
|
isPreview = false,
|
||||||
|
onNewLog,
|
||||||
onNewBlockVisible,
|
onNewBlockVisible,
|
||||||
onNewAnswer,
|
onNewAnswer,
|
||||||
onCompleted,
|
onCompleted,
|
||||||
@@ -43,15 +46,15 @@ export const TypebotViewer = ({
|
|||||||
: 'transparent',
|
: 'transparent',
|
||||||
[typebot?.theme?.general?.background]
|
[typebot?.theme?.general?.background]
|
||||||
)
|
)
|
||||||
const handleNewBlockVisible = (edge: Edge) => {
|
const handleNewBlockVisible = (edge: Edge) =>
|
||||||
if (onNewBlockVisible) onNewBlockVisible(edge)
|
onNewBlockVisible && onNewBlockVisible(edge)
|
||||||
}
|
|
||||||
const handleNewAnswer = (answer: Answer) => {
|
const handleNewAnswer = (answer: Answer) => onNewAnswer && onNewAnswer(answer)
|
||||||
if (onNewAnswer) onNewAnswer(answer)
|
|
||||||
}
|
const handleNewLog = (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) =>
|
||||||
const handleCompleted = () => {
|
onNewLog && onNewLog(log)
|
||||||
if (onCompleted) onCompleted()
|
|
||||||
}
|
const handleCompleted = () => onCompleted && onCompleted()
|
||||||
|
|
||||||
if (!apiHost)
|
if (!apiHost)
|
||||||
return <p>process.env.NEXT_PUBLIC_VIEWER_HOST is missing in env</p>
|
return <p>process.env.NEXT_PUBLIC_VIEWER_HOST is missing in env</p>
|
||||||
@@ -76,8 +79,13 @@ export const TypebotViewer = ({
|
|||||||
}:wght@300;400;600&display=swap');`,
|
}:wght@300;400;600&display=swap');`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TypebotContext typebot={typebot} apiHost={apiHost} isPreview={isPreview}>
|
<TypebotContext
|
||||||
<AnswersContext>
|
typebot={typebot}
|
||||||
|
apiHost={apiHost}
|
||||||
|
isPreview={isPreview}
|
||||||
|
onNewLog={handleNewLog}
|
||||||
|
>
|
||||||
|
<AnswersContext onNewAnswer={handleNewAnswer}>
|
||||||
<div
|
<div
|
||||||
className="flex text-base overflow-hidden bg-cover h-screen w-screen flex-col items-center typebot-container"
|
className="flex text-base overflow-hidden bg-cover h-screen w-screen flex-col items-center typebot-container"
|
||||||
style={{
|
style={{
|
||||||
@@ -90,7 +98,6 @@ export const TypebotViewer = ({
|
|||||||
<ConversationContainer
|
<ConversationContainer
|
||||||
theme={typebot.theme}
|
theme={typebot.theme}
|
||||||
onNewBlockVisible={handleNewBlockVisible}
|
onNewBlockVisible={handleNewBlockVisible}
|
||||||
onNewAnswer={handleNewAnswer}
|
|
||||||
onCompleted={handleCompleted}
|
onCompleted={handleCompleted}
|
||||||
onVariablesPrefilled={onVariablesPrefilled}
|
onVariablesPrefilled={onVariablesPrefilled}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,18 +9,26 @@ const answersContext = createContext<{
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
export const AnswersContext = ({ children }: { children: ReactNode }) => {
|
export const AnswersContext = ({
|
||||||
|
children,
|
||||||
|
onNewAnswer,
|
||||||
|
}: {
|
||||||
|
onNewAnswer: (answer: Answer) => void
|
||||||
|
children: ReactNode
|
||||||
|
}) => {
|
||||||
const [resultValues, setResultValues] = useState<ResultValues>({
|
const [resultValues, setResultValues] = useState<ResultValues>({
|
||||||
answers: [],
|
answers: [],
|
||||||
prefilledVariables: [],
|
prefilledVariables: [],
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const addAnswer = (answer: Answer) =>
|
const addAnswer = (answer: Answer) => {
|
||||||
setResultValues((resultValues) => ({
|
setResultValues((resultValues) => ({
|
||||||
...resultValues,
|
...resultValues,
|
||||||
answers: [...resultValues.answers, answer],
|
answers: [...resultValues.answers, answer],
|
||||||
}))
|
}))
|
||||||
|
onNewAnswer(answer)
|
||||||
|
}
|
||||||
|
|
||||||
const setPrefilledVariables = (variables: VariableWithValue[]) =>
|
const setPrefilledVariables = (variables: VariableWithValue[]) =>
|
||||||
setResultValues((resultValues) => ({
|
setResultValues((resultValues) => ({
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Log } from 'db'
|
||||||
import { Edge, PublicTypebot } from 'models'
|
import { Edge, PublicTypebot } from 'models'
|
||||||
import React, {
|
import React, {
|
||||||
createContext,
|
createContext,
|
||||||
@@ -13,6 +14,7 @@ const typebotContext = createContext<{
|
|||||||
isPreview: boolean
|
isPreview: boolean
|
||||||
updateVariableValue: (variableId: string, value: string) => void
|
updateVariableValue: (variableId: string, value: string) => void
|
||||||
createEdge: (edge: Edge) => void
|
createEdge: (edge: Edge) => void
|
||||||
|
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
}>({})
|
}>({})
|
||||||
@@ -22,11 +24,13 @@ export const TypebotContext = ({
|
|||||||
typebot,
|
typebot,
|
||||||
apiHost,
|
apiHost,
|
||||||
isPreview,
|
isPreview,
|
||||||
|
onNewLog,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
typebot: PublicTypebot
|
typebot: PublicTypebot
|
||||||
apiHost: string
|
apiHost: string
|
||||||
isPreview: boolean
|
isPreview: boolean
|
||||||
|
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
|
||||||
}) => {
|
}) => {
|
||||||
const [localTypebot, setLocalTypebot] = useState<PublicTypebot>(typebot)
|
const [localTypebot, setLocalTypebot] = useState<PublicTypebot>(typebot)
|
||||||
|
|
||||||
@@ -63,6 +67,7 @@ export const TypebotContext = ({
|
|||||||
isPreview,
|
isPreview,
|
||||||
updateVariableValue,
|
updateVariableValue,
|
||||||
createEdge,
|
createEdge,
|
||||||
|
onNewLog,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Log } from 'db'
|
||||||
import {
|
import {
|
||||||
IntegrationStep,
|
IntegrationStep,
|
||||||
IntegrationStepType,
|
IntegrationStepType,
|
||||||
@@ -18,7 +19,6 @@ import {
|
|||||||
import { stringify } from 'qs'
|
import { stringify } from 'qs'
|
||||||
import { sendRequest } from 'utils'
|
import { sendRequest } from 'utils'
|
||||||
import { sendGaEvent } from '../../lib/gtag'
|
import { sendGaEvent } from '../../lib/gtag'
|
||||||
import { sendErrorMessage, sendInfoMessage } from './postMessage'
|
|
||||||
import { parseVariables, parseVariablesInObject } from './variable'
|
import { parseVariables, parseVariablesInObject } from './variable'
|
||||||
|
|
||||||
const safeEval = eval
|
const safeEval = eval
|
||||||
@@ -33,6 +33,7 @@ type IntegrationContext = {
|
|||||||
resultValues: ResultValues
|
resultValues: ResultValues
|
||||||
blocks: Block[]
|
blocks: Block[]
|
||||||
updateVariableValue: (variableId: string, value: string) => void
|
updateVariableValue: (variableId: string, value: string) => void
|
||||||
|
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const executeIntegration = ({
|
export const executeIntegration = ({
|
||||||
@@ -87,10 +88,17 @@ const executeGoogleSheetIntegration = async (
|
|||||||
|
|
||||||
const insertRowInGoogleSheets = async (
|
const insertRowInGoogleSheets = async (
|
||||||
options: GoogleSheetsInsertRowOptions,
|
options: GoogleSheetsInsertRowOptions,
|
||||||
{ variables, apiHost }: IntegrationContext
|
{ variables, apiHost, onNewLog }: IntegrationContext
|
||||||
) => {
|
) => {
|
||||||
if (!options.cellsToInsert) return
|
if (!options.cellsToInsert) {
|
||||||
return sendRequest({
|
onNewLog({
|
||||||
|
status: 'warning',
|
||||||
|
description: 'Cells to insert are undefined',
|
||||||
|
details: null,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { error } = await sendRequest({
|
||||||
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}`,
|
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
@@ -98,14 +106,21 @@ const insertRowInGoogleSheets = async (
|
|||||||
values: parseCellValues(options.cellsToInsert, variables),
|
values: parseCellValues(options.cellsToInsert, variables),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
onNewLog(
|
||||||
|
parseLog(
|
||||||
|
error,
|
||||||
|
'Succesfully inserted a row in the sheet',
|
||||||
|
'Failed to insert a row in the sheet'
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateRowInGoogleSheets = async (
|
const updateRowInGoogleSheets = async (
|
||||||
options: GoogleSheetsUpdateRowOptions,
|
options: GoogleSheetsUpdateRowOptions,
|
||||||
{ variables, apiHost }: IntegrationContext
|
{ variables, apiHost, onNewLog }: IntegrationContext
|
||||||
) => {
|
) => {
|
||||||
if (!options.cellsToUpsert || !options.referenceCell) return
|
if (!options.cellsToUpsert || !options.referenceCell) return
|
||||||
return sendRequest({
|
const { error } = await sendRequest({
|
||||||
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}`,
|
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}`,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: {
|
body: {
|
||||||
@@ -117,11 +132,18 @@ const updateRowInGoogleSheets = async (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
onNewLog(
|
||||||
|
parseLog(
|
||||||
|
error,
|
||||||
|
'Succesfully updated a row in the sheet',
|
||||||
|
'Failed to update a row in the sheet'
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRowFromGoogleSheets = async (
|
const getRowFromGoogleSheets = async (
|
||||||
options: GoogleSheetsGetOptions,
|
options: GoogleSheetsGetOptions,
|
||||||
{ variables, updateVariableValue, apiHost }: IntegrationContext
|
{ variables, updateVariableValue, apiHost, onNewLog }: IntegrationContext
|
||||||
) => {
|
) => {
|
||||||
if (!options.referenceCell || !options.cellsToExtract) return
|
if (!options.referenceCell || !options.cellsToExtract) return
|
||||||
const queryParams = stringify(
|
const queryParams = stringify(
|
||||||
@@ -135,10 +157,17 @@ const getRowFromGoogleSheets = async (
|
|||||||
},
|
},
|
||||||
{ indices: false }
|
{ indices: false }
|
||||||
)
|
)
|
||||||
const { data } = await sendRequest<{ [key: string]: string }>({
|
const { data, error } = await sendRequest<{ [key: string]: string }>({
|
||||||
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}?${queryParams}`,
|
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}?${queryParams}`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
})
|
})
|
||||||
|
onNewLog(
|
||||||
|
parseLog(
|
||||||
|
error,
|
||||||
|
'Succesfully fetched data from sheet',
|
||||||
|
'Failed to fetch data from sheet'
|
||||||
|
)
|
||||||
|
)
|
||||||
if (!data) return
|
if (!data) return
|
||||||
options.cellsToExtract.forEach((cell) =>
|
options.cellsToExtract.forEach((cell) =>
|
||||||
updateVariableValue(cell.variableId ?? '', data[cell.column ?? ''])
|
updateVariableValue(cell.variableId ?? '', data[cell.column ?? ''])
|
||||||
@@ -167,7 +196,7 @@ const executeWebhook = async (
|
|||||||
typebotId,
|
typebotId,
|
||||||
apiHost,
|
apiHost,
|
||||||
resultValues,
|
resultValues,
|
||||||
isPreview,
|
onNewLog,
|
||||||
}: IntegrationContext
|
}: IntegrationContext
|
||||||
) => {
|
) => {
|
||||||
const { data, error } = await sendRequest({
|
const { data, error } = await sendRequest({
|
||||||
@@ -178,8 +207,15 @@ const executeWebhook = async (
|
|||||||
resultValues,
|
resultValues,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
console.error(error)
|
const statusCode = (data as Record<string, string>).statusCode.toString()
|
||||||
if (isPreview && error) sendErrorMessage(`Webhook failed: ${error.message}`)
|
const isError = statusCode.startsWith('4') || statusCode.startsWith('5')
|
||||||
|
onNewLog({
|
||||||
|
status: error ? 'error' : isError ? 'warning' : 'success',
|
||||||
|
description: isError
|
||||||
|
? 'Webhook returned an error'
|
||||||
|
: 'Webhook successfuly executed',
|
||||||
|
details: JSON.stringify(error ?? data, null, 2).substring(0, 1000),
|
||||||
|
})
|
||||||
step.options.responseVariableMapping.forEach((varMapping) => {
|
step.options.responseVariableMapping.forEach((varMapping) => {
|
||||||
if (!varMapping?.bodyPath || !varMapping.variableId) return
|
if (!varMapping?.bodyPath || !varMapping.variableId) return
|
||||||
const value = safeEval(`(${JSON.stringify(data)}).${varMapping?.bodyPath}`)
|
const value = safeEval(`(${JSON.stringify(data)}).${varMapping?.bodyPath}`)
|
||||||
@@ -190,10 +226,16 @@ const executeWebhook = async (
|
|||||||
|
|
||||||
const sendEmail = async (
|
const sendEmail = async (
|
||||||
step: SendEmailStep,
|
step: SendEmailStep,
|
||||||
{ variables, apiHost, isPreview }: IntegrationContext
|
{ variables, apiHost, isPreview, onNewLog }: IntegrationContext
|
||||||
) => {
|
) => {
|
||||||
if (isPreview) sendInfoMessage('Emails are not sent in preview mode')
|
if (isPreview) {
|
||||||
if (isPreview) return step.outgoingEdgeId
|
onNewLog({
|
||||||
|
status: 'info',
|
||||||
|
description: 'Emails are not sent in preview mode',
|
||||||
|
details: null,
|
||||||
|
})
|
||||||
|
return step.outgoingEdgeId
|
||||||
|
}
|
||||||
const { options } = step
|
const { options } = step
|
||||||
const { error } = await sendRequest({
|
const { error } = await sendRequest({
|
||||||
url: `${apiHost}/api/integrations/email`,
|
url: `${apiHost}/api/integrations/email`,
|
||||||
@@ -207,6 +249,18 @@ const sendEmail = async (
|
|||||||
bcc: (options.bcc ?? []).map(parseVariables(variables)),
|
bcc: (options.bcc ?? []).map(parseVariables(variables)),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
console.error(error)
|
onNewLog(
|
||||||
|
parseLog(error, 'Succesfully sent an email', 'Failed to send an email')
|
||||||
|
)
|
||||||
return step.outgoingEdgeId
|
return step.outgoingEdgeId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parseLog = (
|
||||||
|
error: Error | undefined,
|
||||||
|
successMessage: string,
|
||||||
|
errorMessage: string
|
||||||
|
): Omit<Log, 'id' | 'createdAt' | 'resultId'> => ({
|
||||||
|
status: error ? 'error' : 'success',
|
||||||
|
description: error ? errorMessage : successMessage,
|
||||||
|
details: (error && JSON.stringify(error, null, 2).substring(0, 1000)) ?? null,
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
export const sendInfoMessage = (typebotInfo: string) => {
|
|
||||||
parent.postMessage({ typebotInfo })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const sendErrorMessage = (typebotError: string) => {
|
|
||||||
parent.postMessage({ typebotError })
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Log" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"resultId" TEXT NOT NULL,
|
||||||
|
"status" TEXT NOT NULL,
|
||||||
|
"description" TEXT NOT NULL,
|
||||||
|
"details" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "Log_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Log" ADD CONSTRAINT "Log_resultId_fkey" FOREIGN KEY ("resultId") REFERENCES "Result"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -129,7 +129,7 @@ model Typebot {
|
|||||||
customDomain String? @unique
|
customDomain String? @unique
|
||||||
collaborators CollaboratorsOnTypebots[]
|
collaborators CollaboratorsOnTypebots[]
|
||||||
invitations Invitation[]
|
invitations Invitation[]
|
||||||
webhooks Webhook[]
|
webhooks Webhook[]
|
||||||
|
|
||||||
@@unique([id, ownerId])
|
@@unique([id, ownerId])
|
||||||
}
|
}
|
||||||
@@ -184,6 +184,17 @@ model Result {
|
|||||||
answers Answer[]
|
answers Answer[]
|
||||||
prefilledVariables Json[]
|
prefilledVariables Json[]
|
||||||
isCompleted Boolean
|
isCompleted Boolean
|
||||||
|
logs Log[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Log {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
resultId String
|
||||||
|
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
|
||||||
|
status String
|
||||||
|
description String
|
||||||
|
details String?
|
||||||
}
|
}
|
||||||
|
|
||||||
model Answer {
|
model Answer {
|
||||||
|
|||||||
Reference in New Issue
Block a user