diff --git a/apps/builder/components/shared/Graph/Nodes/BlockNode/BlockNode.tsx b/apps/builder/components/shared/Graph/Nodes/BlockNode/BlockNode.tsx index d7a2499f1..f2dbdd085 100644 --- a/apps/builder/components/shared/Graph/Nodes/BlockNode/BlockNode.tsx +++ b/apps/builder/components/shared/Graph/Nodes/BlockNode/BlockNode.tsx @@ -91,13 +91,20 @@ export const BlockNode = ({ block, blockIndex }: Props) => { }) } + const onDragStart = () => setIsMouseDown(true) + const onDragStop = () => setIsMouseDown(false) return ( renderMenu={() => } isDisabled={isReadOnly} > {(ref, isOpened) => ( - e.stopPropagation()}> + e.stopPropagation()} + > { - if (!typebot || !localTypebot || deepEqual(typebot, localTypebot)) return - if (typebot?.blocks.length === localTypebot?.blocks.length) - setLocalTypebot({ ...typebot }) + if ( + !typebot || + !localTypebot || + typebot.updatedAt <= localTypebot.updatedAt || + deepEqual(typebot, localTypebot) + ) + return + setLocalTypebot({ ...typebot }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [typebot]) @@ -113,8 +118,8 @@ export const TypebotContext = ({ ] = useUndo(undefined) const saveTypebot = async () => { - if (deepEqual(typebot, localTypebot)) return const typebotToSave = currentTypebotRef.current + if (deepEqual(typebot, typebotToSave)) return if (!typebotToSave) return setIsSavingLoading(true) const { error } = await updateTypebot(typebotToSave.id, typebotToSave) diff --git a/apps/builder/pages/api/coupons/redeem.ts b/apps/builder/pages/api/coupons/redeem.ts index 00d4bd5d3..e8f38e340 100644 --- a/apps/builder/pages/api/coupons/redeem.ts +++ b/apps/builder/pages/api/coupons/redeem.ts @@ -12,7 +12,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.status(401).json({ message: 'Not authenticated' }) const user = session.user as User - const { code } = JSON.parse(req.body) + const { code } = + typeof req.body === 'string' ? JSON.parse(req.body) : req.body const coupon = await prisma.coupon.findFirst({ where: { code, dateRedeemed: null }, }) diff --git a/apps/builder/pages/api/folders.ts b/apps/builder/pages/api/folders.ts index 425a94c75..f671b7067 100644 --- a/apps/builder/pages/api/folders.ts +++ b/apps/builder/pages/api/folders.ts @@ -26,7 +26,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.send({ folders }) } if (req.method === 'POST') { - const data = JSON.parse(req.body) as Pick + const data = ( + typeof req.body === 'string' ? JSON.parse(req.body) : req.body + ) as Pick const folder = await prisma.dashboardFolder.create({ data: { ...data, ownerId: user.id, name: 'New folder' }, }) diff --git a/apps/builder/pages/api/folders/[id].ts b/apps/builder/pages/api/folders/[id].ts index 8467145f2..905e9709a 100644 --- a/apps/builder/pages/api/folders/[id].ts +++ b/apps/builder/pages/api/folders/[id].ts @@ -26,7 +26,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.send({ folders }) } if (req.method === 'PATCH') { - const data = JSON.parse(req.body) as Partial + const data = ( + typeof req.body === 'string' ? JSON.parse(req.body) : req.body + ) as Partial const folders = await prisma.dashboardFolder.update({ where: { id_ownerId: { id, ownerId: user.id } }, data, diff --git a/apps/builder/pages/api/integrations/email/test-config.ts b/apps/builder/pages/api/integrations/email/test-config.ts index 7e17003e6..e0ee190b7 100644 --- a/apps/builder/pages/api/integrations/email/test-config.ts +++ b/apps/builder/pages/api/integrations/email/test-config.ts @@ -5,8 +5,9 @@ import { createTransport } from 'nodemailer' const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'POST') { - const { from, port, isTlsEnabled, username, password, host, to } = - JSON.parse(req.body) as SmtpCredentialsData & { to: string } + const { from, port, isTlsEnabled, username, password, host, to } = ( + typeof req.body === 'string' ? JSON.parse(req.body) : req.body + ) as SmtpCredentialsData & { to: string } const transporter = createTransport({ host, port, diff --git a/apps/builder/pages/api/publicTypebots.ts b/apps/builder/pages/api/publicTypebots.ts index 9f1cecec0..d4ad66c05 100644 --- a/apps/builder/pages/api/publicTypebots.ts +++ b/apps/builder/pages/api/publicTypebots.ts @@ -12,7 +12,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { try { if (req.method === 'POST') { - const data = JSON.parse(req.body) + const data = + typeof req.body === 'string' ? JSON.parse(req.body) : req.body const typebot = await prisma.publicTypebot.create({ data: { ...data }, }) diff --git a/apps/builder/pages/api/publicTypebots/[id].ts b/apps/builder/pages/api/publicTypebots/[id].ts index 48f0c0f97..7b13e7e9a 100644 --- a/apps/builder/pages/api/publicTypebots/[id].ts +++ b/apps/builder/pages/api/publicTypebots/[id].ts @@ -12,7 +12,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const id = req.query.id.toString() if (req.method === 'PUT') { - const data = JSON.parse(req.body) + const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body const typebots = await prisma.publicTypebot.update({ where: { id }, data, diff --git a/apps/builder/pages/api/stripe/checkout.ts b/apps/builder/pages/api/stripe/checkout.ts index 22fd9e49c..1fc2cc7d0 100644 --- a/apps/builder/pages/api/stripe/checkout.ts +++ b/apps/builder/pages/api/stripe/checkout.ts @@ -10,7 +10,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: '2020-08-27', }) - const { email, currency } = JSON.parse(req.body) + const { email, currency } = + typeof req.body === 'string' ? JSON.parse(req.body) : req.body const session = await stripe.checkout.sessions.create({ success_url: `${req.headers.origin}/typebots?stripe=success`, cancel_url: `${req.headers.origin}/typebots?stripe=cancel`, diff --git a/apps/builder/pages/api/typebots.ts b/apps/builder/pages/api/typebots.ts index 9fff12a1d..205e06e5c 100644 --- a/apps/builder/pages/api/typebots.ts +++ b/apps/builder/pages/api/typebots.ts @@ -26,7 +26,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.send({ typebots }) } if (req.method === 'POST') { - const data = JSON.parse(req.body) + const data = + typeof req.body === 'string' ? JSON.parse(req.body) : req.body const typebot = await prisma.typebot.create({ data: 'blocks' in data diff --git a/apps/builder/pages/api/typebots/[typebotId].ts b/apps/builder/pages/api/typebots/[typebotId].ts index 495740814..b8df0b30e 100644 --- a/apps/builder/pages/api/typebots/[typebotId].ts +++ b/apps/builder/pages/api/typebots/[typebotId].ts @@ -41,7 +41,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.send({ typebots }) } if (req.method === 'PUT') { - const data = JSON.parse(req.body) + const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body const typebots = await prisma.typebot.update({ where: { id_ownerId: { id: typebotId, ownerId: user.id } }, data: { @@ -53,7 +53,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.send({ typebots }) } if (req.method === 'PATCH') { - const data = JSON.parse(req.body) + const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body const typebots = await prisma.typebot.update({ where: { id_ownerId: { id: typebotId, ownerId: user.id } }, data, diff --git a/apps/builder/pages/api/users/[id].ts b/apps/builder/pages/api/users/[id].ts index ae28acd4c..36ca883e9 100644 --- a/apps/builder/pages/api/users/[id].ts +++ b/apps/builder/pages/api/users/[id].ts @@ -12,7 +12,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const id = req.query.id.toString() if (req.method === 'PUT') { - const data = JSON.parse(req.body) + const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body const typebots = await prisma.user.update({ where: { id }, data, diff --git a/apps/builder/pages/api/users/[id]/credentials.ts b/apps/builder/pages/api/users/[id]/credentials.ts index d7a52e67f..a4c1ff5ba 100644 --- a/apps/builder/pages/api/users/[id]/credentials.ts +++ b/apps/builder/pages/api/users/[id]/credentials.ts @@ -23,7 +23,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.send({ credentials }) } if (req.method === 'POST') { - const data = JSON.parse(req.body) as Omit + const data = ( + typeof req.body === 'string' ? JSON.parse(req.body) : req.body + ) as Omit const { encryptedData, iv } = encrypt(data.data) const credentials = await prisma.credentials.create({ data: { diff --git a/apps/builder/pages/api/users/[id]/customDomains.ts b/apps/builder/pages/api/users/[id]/customDomains.ts index 3c5704dc3..9bae9e62a 100644 --- a/apps/builder/pages/api/users/[id]/customDomains.ts +++ b/apps/builder/pages/api/users/[id]/customDomains.ts @@ -22,7 +22,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.send({ customDomains }) } if (req.method === 'POST') { - const data = JSON.parse(req.body) as Omit + const data = ( + typeof req.body === 'string' ? JSON.parse(req.body) : req.body + ) as Omit try { await createDomainOnVercel(data.name) } catch (err) { diff --git a/apps/builder/playwright/services/database.ts b/apps/builder/playwright/services/database.ts index 404a48f05..530dd4c95 100644 --- a/apps/builder/playwright/services/database.ts +++ b/apps/builder/playwright/services/database.ts @@ -129,7 +129,7 @@ const createAnswers = () => { const parseTypebotToPublicTypebot = ( id: string, typebot: Typebot -): PublicTypebot => ({ +): Omit => ({ id, name: typebot.name, blocks: parseBlocksToPublicBlocks(typebot.blocks), @@ -157,10 +157,9 @@ const parseTestTypebot = (partialTypebot: Partial): Typebot => ({ ownerId: 'proUser', theme: defaultTheme, settings: defaultSettings, - createdAt: new Date(), publicId: null, - publishedTypebotId: null, updatedAt: new Date(), + publishedTypebotId: null, customDomain: null, variables: [{ id: 'var1', name: 'var1' }], ...partialTypebot, diff --git a/apps/builder/playwright/tests/customDomains.spec.ts b/apps/builder/playwright/tests/customDomains.spec.ts index e083b5e39..08ed51d61 100644 --- a/apps/builder/playwright/tests/customDomains.spec.ts +++ b/apps/builder/playwright/tests/customDomains.spec.ts @@ -6,7 +6,7 @@ import path from 'path' const typebotId = generate() test.describe('Dashboard page', () => { - test('folders navigation should work', async ({ page }) => { + test('should be able to connect custom domain', async ({ page }) => { await createTypebots([ { id: typebotId, diff --git a/apps/viewer/pages/api/answers.ts b/apps/viewer/pages/api/answers.ts index be7a42ec7..be61053df 100644 --- a/apps/viewer/pages/api/answers.ts +++ b/apps/viewer/pages/api/answers.ts @@ -6,7 +6,9 @@ import { methodNotAllowed } from 'utils' const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'PUT') { - const answer = JSON.parse(req.body) as Answer + const answer = ( + typeof req.body === 'string' ? JSON.parse(req.body) : req.body + ) as Answer const result = await prisma.answer.upsert({ where: { resultId_blockId_stepId: { diff --git a/apps/viewer/pages/api/integrations/email.ts b/apps/viewer/pages/api/integrations/email.ts index 60f87064c..2ce652283 100644 --- a/apps/viewer/pages/api/integrations/email.ts +++ b/apps/viewer/pages/api/integrations/email.ts @@ -27,8 +27,8 @@ const defaultFrom = { const handler = async (req: NextApiRequest, res: NextApiResponse) => { await cors(req, res) if (req.method === 'POST') { - const { credentialsId, recipients, body, subject, cc, bcc } = JSON.parse( - req.body + const { credentialsId, recipients, body, subject, cc, bcc } = ( + typeof req.body === 'string' ? JSON.parse(req.body) : req.body ) as SendEmailOptions const { host, port, isTlsEnabled, username, password, from } = diff --git a/apps/viewer/pages/api/integrations/google-sheets/spreadsheets/[spreadsheetId]/sheets/[sheetId].ts b/apps/viewer/pages/api/integrations/google-sheets/spreadsheets/[spreadsheetId]/sheets/[sheetId].ts index c44473bb3..97272e67e 100644 --- a/apps/viewer/pages/api/integrations/google-sheets/spreadsheets/[spreadsheetId]/sheets/[sheetId].ts +++ b/apps/viewer/pages/api/integrations/google-sheets/spreadsheets/[spreadsheetId]/sheets/[sheetId].ts @@ -37,7 +37,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'POST') { const spreadsheetId = req.query.spreadsheetId.toString() const sheetId = req.query.sheetId.toString() - const { credentialsId, values } = JSON.parse(req.body) as { + const { credentialsId, values } = ( + typeof req.body === 'string' ? JSON.parse(req.body) : req.body + ) as { credentialsId: string values: { [key: string]: string } } @@ -51,7 +53,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'PATCH') { const spreadsheetId = req.query.spreadsheetId.toString() const sheetId = req.query.sheetId.toString() - const { credentialsId, values, referenceCell } = JSON.parse(req.body) as { + const { credentialsId, values, referenceCell } = ( + typeof req.body === 'string' ? JSON.parse(req.body) : req.body + ) as { credentialsId: string referenceCell: Cell values: { [key: string]: string } diff --git a/apps/viewer/pages/api/results.ts b/apps/viewer/pages/api/results.ts index 71a7828a6..b6e18a313 100644 --- a/apps/viewer/pages/api/results.ts +++ b/apps/viewer/pages/api/results.ts @@ -6,7 +6,9 @@ import { methodNotAllowed } from 'utils' const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'POST') { - const resultData = JSON.parse(req.body) as { + const resultData = ( + typeof req.body === 'string' ? JSON.parse(req.body) : req.body + ) as { typebotId: string prefilledVariables: VariableWithValue[] } diff --git a/apps/viewer/pages/api/results/[id].ts b/apps/viewer/pages/api/results/[id].ts index f9d23ec76..84e6e3288 100644 --- a/apps/viewer/pages/api/results/[id].ts +++ b/apps/viewer/pages/api/results/[id].ts @@ -5,7 +5,9 @@ import { methodNotAllowed } from 'utils' const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'PATCH') { - const data = JSON.parse(req.body) as { isCompleted: true } + const data = ( + typeof req.body === 'string' ? JSON.parse(req.body) : req.body + ) as { isCompleted: true } const id = req.query.id.toString() const result = await prisma.result.update({ where: { id }, diff --git a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/executeWebhook.ts b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/executeWebhook.ts index b4b077ca2..10344dbf7 100644 --- a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/executeWebhook.ts +++ b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/executeWebhook.ts @@ -1,12 +1,21 @@ import prisma from 'libs/prisma' -import { KeyValue, Typebot, Variable, Webhook, WebhookResponse } from 'models' +import { + KeyValue, + PublicTypebot, + ResultValues, + Typebot, + Variable, + Webhook, + WebhookResponse, +} from 'models' import { parseVariables } from 'bot-engine' import { NextApiRequest, NextApiResponse } from 'next' import got, { Method, Headers, HTTPError } from 'got' -import { byId, initMiddleware, methodNotAllowed } from 'utils' +import { byId, initMiddleware, methodNotAllowed, parseAnswers } from 'utils' import { stringify } from 'qs' import { withSentry } from '@sentry/nextjs' import Cors from 'cors' +import { parseSampleResult } from 'services/api/webhooks' const cors = initMiddleware(Cors()) const handler = async (req: NextApiRequest, res: NextApiResponse) => { @@ -15,87 +24,125 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const typebotId = req.query.typebotId.toString() const blockId = req.query.blockId.toString() const stepId = req.query.stepId.toString() - const variables = JSON.parse(req.body).variables as Variable[] - const typebot = await prisma.typebot.findUnique({ + const { resultValues, variables } = ( + typeof req.body === 'string' ? JSON.parse(req.body) : req.body + ) as { + resultValues: ResultValues | undefined + variables: Variable[] + } + const typebot = (await prisma.typebot.findUnique({ where: { id: typebotId }, - }) - const step = (typebot as unknown as Typebot).blocks - .find(byId(blockId)) - ?.steps.find(byId(stepId)) + })) as unknown as Typebot + const step = typebot.blocks.find(byId(blockId))?.steps.find(byId(stepId)) if (!step || !('webhook' in step)) return res .status(404) .send({ statusCode: 404, data: { message: `Couldn't find webhook` } }) - const result = await executeWebhook(step.webhook, variables) + const result = await executeWebhook(typebot)( + step.webhook, + variables, + blockId, + resultValues + ) return res.status(200).send(result) } return methodNotAllowed(res) } -const executeWebhook = async ( - webhook: Webhook, - variables: Variable[] -): Promise => { - if (!webhook.url || !webhook.method) - return { - statusCode: 400, - data: { message: `Webhook doesn't have url or method` }, - } - const basicAuth: { username?: string; password?: string } = {} - const basicAuthHeaderIdx = webhook.headers.findIndex( - (h) => - h.key?.toLowerCase() === 'authorization' && - h.value?.toLowerCase().includes('basic') - ) - if (basicAuthHeaderIdx !== -1) { - const [username, password] = - webhook.headers[basicAuthHeaderIdx].value?.slice(6).split(':') ?? [] - basicAuth.username = username - basicAuth.password = password - webhook.headers.splice(basicAuthHeaderIdx, 1) - } - const headers = convertKeyValueTableToObject(webhook.headers, variables) as - | Headers - | undefined - const queryParams = stringify( - convertKeyValueTableToObject(webhook.queryParams, variables) - ) - const contentType = headers ? headers['Content-Type'] : undefined - try { - const response = await got( - parseVariables(variables)(webhook.url + `?${queryParams}`), - { - method: webhook.method as Method, - headers, - ...basicAuth, - json: - contentType !== 'x-www-form-urlencoded' && webhook.body - ? JSON.parse(parseVariables(variables)(webhook.body)) - : undefined, - form: - contentType === 'x-www-form-urlencoded' && webhook.body - ? JSON.parse(parseVariables(variables)(webhook.body)) - : undefined, - } - ) - return { - statusCode: response.statusCode, - data: parseBody(response.body), - } - } catch (error) { - if (error instanceof HTTPError) { +const executeWebhook = + (typebot: Typebot) => + async ( + webhook: Webhook, + variables: Variable[], + blockId: string, + resultValues?: ResultValues + ): Promise => { + if (!webhook.url || !webhook.method) return { - statusCode: error.response.statusCode, - data: parseBody(error.response.body as string), + statusCode: 400, + data: { message: `Webhook doesn't have url or method` }, + } + const basicAuth: { username?: string; password?: string } = {} + const basicAuthHeaderIdx = webhook.headers.findIndex( + (h) => + h.key?.toLowerCase() === 'authorization' && + h.value?.toLowerCase().includes('basic') + ) + if (basicAuthHeaderIdx !== -1) { + const [username, password] = + webhook.headers[basicAuthHeaderIdx].value?.slice(6).split(':') ?? [] + basicAuth.username = username + basicAuth.password = password + webhook.headers.splice(basicAuthHeaderIdx, 1) + } + const headers = convertKeyValueTableToObject(webhook.headers, variables) as + | Headers + | undefined + const queryParams = stringify( + convertKeyValueTableToObject(webhook.queryParams, variables) + ) + const contentType = headers ? headers['Content-Type'] : undefined + const body = getBodyContent(typebot)({ + body: webhook.body, + resultValues, + blockId, + }) + try { + const response = await got( + parseVariables(variables)(webhook.url + `?${queryParams}`), + { + method: webhook.method as Method, + headers, + ...basicAuth, + json: + contentType !== 'x-www-form-urlencoded' && body + ? JSON.parse(parseVariables(variables)(body)) + : undefined, + form: + contentType === 'x-www-form-urlencoded' && body + ? JSON.parse(parseVariables(variables)(body)) + : undefined, + } + ) + return { + statusCode: response.statusCode, + data: parseBody(response.body), + } + } catch (error) { + if (error instanceof HTTPError) { + return { + statusCode: error.response.statusCode, + data: parseBody(error.response.body as string), + } + } + console.error(error) + return { + statusCode: 500, + data: { message: `Error from Typebot server: ${error}` }, } } - console.error(error) - return { - statusCode: 500, - data: { message: `Error from Typebot server: ${error}` }, - } } -} + +const getBodyContent = + (typebot: Pick) => + ({ + body, + resultValues, + blockId, + }: { + body?: string + resultValues?: ResultValues + blockId: string + }): string | undefined => { + if (!body) return + return body === '{{state}}' + ? JSON.stringify( + resultValues + ? parseAnswers(typebot)(resultValues) + : parseSampleResult(typebot)(blockId) + ) + : body + } const parseBody = (body: string) => { try { diff --git a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/sampleResult.ts b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/sampleResult.ts index ba2a043d0..f13ef36da 100644 --- a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/sampleResult.ts +++ b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/sampleResult.ts @@ -1,8 +1,9 @@ import prisma from 'libs/prisma' -import { Block, InputStep, InputStepType, Typebot } from 'models' +import { Typebot } from 'models' import { NextApiRequest, NextApiResponse } from 'next' import { authenticateUser } from 'services/api/utils' -import { byId, isDefined, isInputStep, methodNotAllowed } from 'utils' +import { parseSampleResult } from 'services/api/webhooks' +import { methodNotAllowed } from 'utils' const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'GET') { @@ -12,87 +13,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const blockId = req.query.blockId.toString() const typebot = (await prisma.typebot.findUnique({ where: { id_ownerId: { id: typebotId, ownerId: user.id } }, - })) as Typebot | undefined + })) as unknown as Typebot | undefined if (!typebot) return res.status(400).send({ message: 'Typebot not found' }) - const previousBlockIds = getPreviousBlocks(typebot)(blockId) - const previousBlocks = typebot.blocks.filter((b) => - previousBlockIds.includes(b.id) - ) - return res.send(parseSampleResult(typebot)(previousBlocks)) + return res.send(parseSampleResult(typebot)(blockId)) } methodNotAllowed(res) } -const parseSampleResult = - (typebot: Typebot) => - (blocks: Block[]): Record => { - const parsedBlocks = parseBlocksResultSample(typebot, blocks) - return { - message: 'This is a sample result, it has been generated ⬇️', - 'Submitted at': new Date().toISOString(), - ...parsedBlocks, - ...parseVariablesHeaders(typebot, parsedBlocks), - } - } - -const parseBlocksResultSample = (typebot: Typebot, blocks: Block[]) => - blocks - .filter((block) => typebot && block.steps.some((step) => isInputStep(step))) - .reduce>((blocks, block) => { - const inputStep = block.steps.find((step) => isInputStep(step)) - if (!inputStep || !isInputStep(inputStep)) return blocks - const matchedVariableName = - inputStep.options.variableId && - typebot.variables.find(byId(inputStep.options.variableId))?.name - const value = getSampleValue(inputStep) - return { - ...blocks, - [matchedVariableName ?? block.title]: value, - } - }, {}) - -const getSampleValue = (step: InputStep) => { - switch (step.type) { - case InputStepType.CHOICE: - return 'Item 1, Item 2, Item3' - case InputStepType.DATE: - return new Date().toUTCString() - case InputStepType.EMAIL: - return 'test@email.com' - case InputStepType.NUMBER: - return '20' - case InputStepType.PHONE: - return '+33665566773' - case InputStepType.TEXT: - return 'answer value' - case InputStepType.URL: - return 'https://test.com' - } -} - -const parseVariablesHeaders = ( - typebot: Typebot, - parsedBlocks: Record -) => - typebot.variables.reduce>((headers, v) => { - if (parsedBlocks[v.name]) return headers - return { - ...headers, - [v.name]: 'value', - } - }, {}) - -const getPreviousBlocks = - (typebot: Typebot) => - (blockId: string): string[] => { - const previousBlocks = typebot.edges - .map((edge) => - edge.to.blockId === blockId ? edge.from.blockId : undefined - ) - .filter(isDefined) - return previousBlocks.concat( - previousBlocks.flatMap(getPreviousBlocks(typebot)) - ) - } - export default handler diff --git a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/subscribeWebhook.ts b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/subscribeWebhook.ts index 21a8eda81..1b560c6de 100644 --- a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/subscribeWebhook.ts +++ b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/subscribeWebhook.ts @@ -18,7 +18,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const stepId = req.query.stepId.toString() const typebot = (await prisma.typebot.findUnique({ where: { id_ownerId: { id: typebotId, ownerId: user.id } }, - })) as Typebot | undefined + })) as unknown as Typebot | undefined if (!typebot) return res.status(400).send({ message: 'Typebot not found' }) try { const updatedTypebot = addUrlToWebhookStep(url, typebot, stepId) diff --git a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/unsubscribeWebhook.ts b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/unsubscribeWebhook.ts index f35310dcb..4ac124ca4 100644 --- a/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/unsubscribeWebhook.ts +++ b/apps/viewer/pages/api/typebots/[typebotId]/blocks/[blockId]/steps/[stepId]/unsubscribeWebhook.ts @@ -15,7 +15,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const stepId = req.query.stepId.toString() const typebot = (await prisma.typebot.findUnique({ where: { id_ownerId: { id: typebotId, ownerId: user.id } }, - })) as Typebot | undefined + })) as unknown as Typebot | undefined if (!typebot) return res.status(400).send({ message: 'Typebot not found' }) try { const updatedTypebot = removeUrlFromWebhookStep(typebot, stepId) diff --git a/apps/viewer/pages/api/typebots/[typebotId]/results.ts b/apps/viewer/pages/api/typebots/[typebotId]/results.ts index 5a3f3fd4c..d071bafac 100644 --- a/apps/viewer/pages/api/typebots/[typebotId]/results.ts +++ b/apps/viewer/pages/api/typebots/[typebotId]/results.ts @@ -20,7 +20,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { take: limit, include: { answers: true }, })) as unknown as ResultWithAnswers[] - res.send({ results: results.map(parseAnswers(typebot as Typebot)) }) + res.send({ + results: results.map(parseAnswers(typebot as unknown as Typebot)), + }) } methodNotAllowed(res) } diff --git a/apps/viewer/playwright/services/database.ts b/apps/viewer/playwright/services/database.ts index e42275a94..3620b3498 100644 --- a/apps/viewer/playwright/services/database.ts +++ b/apps/viewer/playwright/services/database.ts @@ -74,7 +74,6 @@ const parseTestTypebot = (partialTypebot: Partial): Typebot => ({ ownerId: 'user', theme: defaultTheme, settings: defaultSettings, - createdAt: new Date(), publicId: partialTypebot.id + '-public', publishedTypebotId: null, updatedAt: new Date(), diff --git a/apps/viewer/services/api/webhooks.ts b/apps/viewer/services/api/webhooks.ts new file mode 100644 index 000000000..d6627ff39 --- /dev/null +++ b/apps/viewer/services/api/webhooks.ts @@ -0,0 +1,80 @@ +import { Block, InputStep, InputStepType, PublicTypebot, Typebot } from 'models' +import { isInputStep, byId, isDefined } from 'utils' + +export const parseSampleResult = + (typebot: Pick) => + (currentBlockId: string): Record => { + const previousBlocks = (typebot.blocks as Block[]).filter((b) => + getPreviousBlockIds(typebot)(currentBlockId).includes(b.id) + ) + const parsedBlocks = parseBlocksResultSample(typebot, previousBlocks) + return { + message: 'This is a sample result, it has been generated ⬇️', + 'Submitted at': new Date().toISOString(), + ...parsedBlocks, + ...parseVariablesHeaders(typebot, parsedBlocks), + } + } + +const parseBlocksResultSample = ( + typebot: Pick, + blocks: Block[] +) => + blocks + .filter((block) => typebot && block.steps.some((step) => isInputStep(step))) + .reduce>((blocks, block) => { + const inputStep = block.steps.find((step) => isInputStep(step)) + if (!inputStep || !isInputStep(inputStep)) return blocks + const matchedVariableName = + inputStep.options.variableId && + typebot.variables.find(byId(inputStep.options.variableId))?.name + const value = getSampleValue(inputStep) + return { + ...blocks, + [matchedVariableName ?? block.title]: value, + } + }, {}) + +const getSampleValue = (step: InputStep) => { + switch (step.type) { + case InputStepType.CHOICE: + return 'Item 1, Item 2, Item3' + case InputStepType.DATE: + return new Date().toUTCString() + case InputStepType.EMAIL: + return 'test@email.com' + case InputStepType.NUMBER: + return '20' + case InputStepType.PHONE: + return '+33665566773' + case InputStepType.TEXT: + return 'answer value' + case InputStepType.URL: + return 'https://test.com' + } +} + +const parseVariablesHeaders = ( + typebot: Pick, + parsedBlocks: Record +) => + typebot.variables.reduce>((headers, v) => { + if (parsedBlocks[v.name]) return headers + return { + ...headers, + [v.name]: 'value', + } + }, {}) + +const getPreviousBlockIds = + (typebot: Pick) => + (blockId: string): string[] => { + const previousBlocks = typebot.edges + .map((edge) => + edge.to.blockId === blockId ? edge.from.blockId : undefined + ) + .filter(isDefined) + return previousBlocks.concat( + previousBlocks.flatMap(getPreviousBlockIds(typebot)) + ) + } diff --git a/packages/bot-engine/src/contexts/AnswersContext.tsx b/packages/bot-engine/src/contexts/AnswersContext.tsx index 2e12db2b9..fc0f587ee 100644 --- a/packages/bot-engine/src/contexts/AnswersContext.tsx +++ b/packages/bot-engine/src/contexts/AnswersContext.tsx @@ -1,10 +1,6 @@ -import { Answer, ResultWithAnswers, VariableWithValue } from 'models' +import { Answer, ResultValues, VariableWithValue } from 'models' import React, { createContext, ReactNode, useContext, useState } from 'react' -export type ResultValues = Pick< - ResultWithAnswers, - 'answers' | 'createdAt' | 'prefilledVariables' -> const answersContext = createContext<{ resultValues: ResultValues addAnswer: (answer: Answer) => void diff --git a/packages/bot-engine/src/services/integration.ts b/packages/bot-engine/src/services/integration.ts index 579598976..4f0ca59d6 100644 --- a/packages/bot-engine/src/services/integration.ts +++ b/packages/bot-engine/src/services/integration.ts @@ -1,4 +1,3 @@ -import { ResultValues } from 'contexts/AnswersContext' import { IntegrationStep, IntegrationStepType, @@ -14,9 +13,10 @@ import { SendEmailStep, PublicBlock, ZapierStep, + ResultValues, } from 'models' import { stringify } from 'qs' -import { parseAnswers, sendRequest } from 'utils' +import { sendRequest } from 'utils' import { sendGaEvent } from '../../lib/gtag' import { sendErrorMessage, sendInfoMessage } from './postMessage' import { parseVariables, parseVariablesInObject } from './variable' @@ -166,6 +166,8 @@ const executeWebhook = async ( updateVariableValue, typebotId, apiHost, + resultValues, + isPreview, }: IntegrationContext ) => { if (!step.webhook) return step.outgoingEdgeId @@ -174,9 +176,11 @@ const executeWebhook = async ( method: 'POST', body: { variables, + resultValues, }, }) console.error(error) + if (isPreview && error) sendErrorMessage(`Webhook failed: ${error.message}`) step.options.responseVariableMapping.forEach((varMapping) => { if (!varMapping?.bodyPath || !varMapping.variableId) return const value = safeEval(`(${JSON.stringify(data)}).${varMapping?.bodyPath}`) @@ -186,7 +190,7 @@ const executeWebhook = async ( const sendEmail = async ( step: SendEmailStep, - { variables, apiHost, isPreview, resultValues, blocks }: IntegrationContext + { variables, apiHost, isPreview }: IntegrationContext ) => { if (isPreview) sendInfoMessage('Emails are not sent in preview mode') if (isPreview) return step.outgoingEdgeId @@ -198,15 +202,11 @@ const sendEmail = async ( credentialsId: options.credentialsId, recipients: options.recipients.map(parseVariables(variables)), subject: parseVariables(variables)(options.subject ?? ''), - body: - options.body === '{{state}}' - ? parseAnswers({ variables, blocks })(resultValues) - : parseVariables(variables)(options.body ?? ''), + body: parseVariables(variables)(options.body ?? ''), cc: (options.cc ?? []).map(parseVariables(variables)), bcc: (options.bcc ?? []).map(parseVariables(variables)), }, }) console.error(error) - if (isPreview && error) sendErrorMessage(`Webhook failed: ${error.message}`) return step.outgoingEdgeId } diff --git a/packages/db/prisma/migrations/20220222091415_add_updated_at_fields/migration.sql b/packages/db/prisma/migrations/20220222091415_add_updated_at_fields/migration.sql new file mode 100644 index 000000000..c49de9a55 --- /dev/null +++ b/packages/db/prisma/migrations/20220222091415_add_updated_at_fields/migration.sql @@ -0,0 +1,9 @@ +-- AlterTable +ALTER TABLE "DashboardFolder" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "PublicTypebot" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "Result" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 04d6a8af8..73ac9d477 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -90,6 +90,7 @@ model VerificationToken { model DashboardFolder { id String @id @default(cuid()) createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt name String owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) ownerId String @@ -104,7 +105,7 @@ model DashboardFolder { model Typebot { id String @id @default(cuid()) createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt name String ownerId String owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) @@ -125,22 +126,25 @@ model Typebot { } model PublicTypebot { - id String @id @default(cuid()) - typebotId String @unique - typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + typebotId String @unique + typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade) name String blocks Json[] variables Json[] edges Json[] theme Json settings Json - publicId String? @unique - customDomain String? @unique + publicId String? @unique + customDomain String? @unique } model Result { id String @id @default(cuid()) createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt typebotId String typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade) answers Answer[] diff --git a/packages/models/src/publicTypebot.ts b/packages/models/src/publicTypebot.ts index 913892d72..e53702b5e 100644 --- a/packages/models/src/publicTypebot.ts +++ b/packages/models/src/publicTypebot.ts @@ -3,7 +3,13 @@ import { PublicTypebot as PublicTypebotFromPrisma } from 'db' export type PublicTypebot = Omit< PublicTypebotFromPrisma, - 'blocks' | 'theme' | 'settings' | 'variables' | 'edges' + | 'blocks' + | 'theme' + | 'settings' + | 'variables' + | 'edges' + | 'createdAt' + | 'updatedAt' > & { blocks: PublicBlock[] variables: Variable[] diff --git a/packages/models/src/result.ts b/packages/models/src/result.ts index c858fa63a..71a82248f 100644 --- a/packages/models/src/result.ts +++ b/packages/models/src/result.ts @@ -7,3 +7,8 @@ export type Result = Omit< > & { createdAt: string; prefilledVariables: VariableWithValue[] } export type ResultWithAnswers = Result & { answers: Answer[] } + +export type ResultValues = Pick< + ResultWithAnswers, + 'answers' | 'createdAt' | 'prefilledVariables' +> diff --git a/packages/models/src/typebot/typebot.ts b/packages/models/src/typebot/typebot.ts index 46c68e54e..37568bdb0 100644 --- a/packages/models/src/typebot/typebot.ts +++ b/packages/models/src/typebot/typebot.ts @@ -6,7 +6,7 @@ import { Variable } from './variable' export type Typebot = Omit< TypebotFromPrisma, - 'blocks' | 'theme' | 'settings' | 'variables' | 'edges' + 'blocks' | 'theme' | 'settings' | 'variables' | 'edges' | 'createdAt' > & { blocks: Block[] variables: Variable[] diff --git a/packages/utils/src/utils.ts b/packages/utils/src/utils.ts index dc5f348a6..36ff9fb95 100644 --- a/packages/utils/src/utils.ts +++ b/packages/utils/src/utils.ts @@ -34,6 +34,12 @@ export const sendRequest = async ( const response = await fetch(url, { method: typeof params === 'string' ? 'GET' : params.method, mode: 'cors', + headers: + typeof params !== 'string' && isDefined(params.body) + ? { + 'Content-Type': 'application/json', + } + : undefined, body: typeof params !== 'string' && isDefined(params.body) ? JSON.stringify(params.body)