feat(integration): 🚸 Easy webhook config
This commit is contained in:
@ -6,10 +6,11 @@ import {
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Button,
|
||||
Flex,
|
||||
HStack,
|
||||
Spinner,
|
||||
Stack,
|
||||
useToast,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { InputWithVariableButton } from 'components/shared/TextboxWithVariableButton/InputWithVariableButton'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
@ -36,6 +37,7 @@ import { VariableForTestInputs } from './VariableForTestInputs'
|
||||
import { DataVariableInputs } from './ResponseMappingInputs'
|
||||
import { byId } from 'utils'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||
|
||||
type Props = {
|
||||
step: WebhookStep
|
||||
@ -111,6 +113,12 @@ export const WebhookSettings = ({
|
||||
responseVariableMapping: ResponseVariableMapping[]
|
||||
) => onOptionsChange({ ...options, responseVariableMapping })
|
||||
|
||||
const handleAdvancedConfigChange = (isAdvancedConfig: boolean) =>
|
||||
onOptionsChange({ ...options, isAdvancedConfig })
|
||||
|
||||
const handleBodyFormStateChange = (isCustomBody: boolean) =>
|
||||
onOptionsChange({ ...options, isCustomBody })
|
||||
|
||||
const handleTestRequestClick = async () => {
|
||||
if (!typebot || !localWebhook) return
|
||||
setIsTestResponseLoading(true)
|
||||
@ -139,107 +147,126 @@ export const WebhookSettings = ({
|
||||
if (!localWebhook) return <Spinner />
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<InputWithVariableButton
|
||||
placeholder="Your Webhook URL..."
|
||||
initialValue={localWebhook.url ?? ''}
|
||||
onChange={handleUrlChange}
|
||||
/>
|
||||
<SwitchWithLabel
|
||||
id={'easy-config'}
|
||||
label="Advanced configuration"
|
||||
initialValue={options.isAdvancedConfig ?? true}
|
||||
onCheckChange={handleAdvancedConfigChange}
|
||||
/>
|
||||
{(options.isAdvancedConfig ?? true) && (
|
||||
<Stack>
|
||||
<HStack justify="space-between">
|
||||
<Text>Method:</Text>
|
||||
<DropdownList<HttpMethod>
|
||||
currentItem={localWebhook.method as HttpMethod}
|
||||
onItemSelect={handleMethodChange}
|
||||
items={Object.values(HttpMethod)}
|
||||
/>
|
||||
</HStack>
|
||||
<Accordion allowToggle allowMultiple>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Query params
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<KeyValue>
|
||||
initialItems={localWebhook.queryParams}
|
||||
onItemsChange={handleQueryParamsChange}
|
||||
Item={QueryParamsInputs}
|
||||
addLabel="Add a param"
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Headers
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<KeyValue>
|
||||
initialItems={localWebhook.headers}
|
||||
onItemsChange={handleHeadersChange}
|
||||
Item={HeadersInputs}
|
||||
addLabel="Add a value"
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Body
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<SwitchWithLabel
|
||||
id={'custom-body'}
|
||||
label="Custom body"
|
||||
initialValue={options.isCustomBody ?? true}
|
||||
onCheckChange={handleBodyFormStateChange}
|
||||
/>
|
||||
{(options.isCustomBody ?? true) && (
|
||||
<CodeEditor
|
||||
value={localWebhook.body ?? ''}
|
||||
lang="json"
|
||||
onChange={handleBodyChange}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Variable values for test
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<VariableForTest>
|
||||
initialItems={
|
||||
options?.variablesForTest ?? { byId: {}, allIds: [] }
|
||||
}
|
||||
onItemsChange={handleVariablesChange}
|
||||
Item={VariableForTestInputs}
|
||||
addLabel="Add an entry"
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</Stack>
|
||||
)}
|
||||
<Stack>
|
||||
<Flex>
|
||||
<DropdownList<HttpMethod>
|
||||
currentItem={localWebhook.method as HttpMethod}
|
||||
onItemSelect={handleMethodChange}
|
||||
items={Object.values(HttpMethod)}
|
||||
/>
|
||||
</Flex>
|
||||
<InputWithVariableButton
|
||||
placeholder="Your Webhook URL..."
|
||||
initialValue={localWebhook.url ?? ''}
|
||||
onChange={handleUrlChange}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleTestRequestClick}
|
||||
colorScheme="blue"
|
||||
isLoading={isTestResponseLoading}
|
||||
>
|
||||
Test the request
|
||||
</Button>
|
||||
{testResponse && (
|
||||
<CodeEditor isReadOnly lang="json" value={testResponse} />
|
||||
)}
|
||||
{(testResponse || options?.responseVariableMapping) && (
|
||||
<Accordion allowToggle allowMultiple>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Save in variables
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<ResponseVariableMapping>
|
||||
initialItems={options.responseVariableMapping}
|
||||
onItemsChange={handleResponseMappingChange}
|
||||
Item={ResponseMappingInputs}
|
||||
addLabel="Add an entry"
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
</Stack>
|
||||
<Accordion allowToggle allowMultiple>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Query params
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<KeyValue>
|
||||
initialItems={localWebhook.queryParams}
|
||||
onItemsChange={handleQueryParamsChange}
|
||||
Item={QueryParamsInputs}
|
||||
addLabel="Add a param"
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Headers
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<KeyValue>
|
||||
initialItems={localWebhook.headers}
|
||||
onItemsChange={handleHeadersChange}
|
||||
Item={HeadersInputs}
|
||||
addLabel="Add a value"
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Body
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<CodeEditor
|
||||
value={localWebhook.body ?? ''}
|
||||
lang="json"
|
||||
onChange={handleBodyChange}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Variable values for test
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<VariableForTest>
|
||||
initialItems={
|
||||
options?.variablesForTest ?? { byId: {}, allIds: [] }
|
||||
}
|
||||
onItemsChange={handleVariablesChange}
|
||||
Item={VariableForTestInputs}
|
||||
addLabel="Add an entry"
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<Button
|
||||
onClick={handleTestRequestClick}
|
||||
colorScheme="blue"
|
||||
isLoading={isTestResponseLoading}
|
||||
>
|
||||
Test the request
|
||||
</Button>
|
||||
{testResponse && (
|
||||
<CodeEditor isReadOnly lang="json" value={testResponse} />
|
||||
)}
|
||||
{(testResponse || options?.responseVariableMapping) && (
|
||||
<Accordion allowToggle allowMultiple>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Save in variables
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<ResponseVariableMapping>
|
||||
initialItems={options.responseVariableMapping}
|
||||
onItemsChange={handleResponseMappingChange}
|
||||
Item={ResponseMappingInputs}
|
||||
addLabel="Add an entry"
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
13
apps/builder/pages/api/mock/webhook-easy-config.ts
Normal file
13
apps/builder/pages/api/mock/webhook-easy-config.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { methodNotAllowed } from 'utils'
|
||||
|
||||
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||
console.log(req.method)
|
||||
if (req.method === 'POST') {
|
||||
return res.status(200).send(req.body)
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
@ -62,7 +62,12 @@
|
||||
"id": "soSmiE7zyb3WF77GxFxAjYX",
|
||||
"blockId": "8XnDM1QsqPms4LQHh8q3Jo",
|
||||
"type": "Webhook",
|
||||
"options": { "responseVariableMapping": [], "variablesForTest": [] },
|
||||
"options": {
|
||||
"responseVariableMapping": [],
|
||||
"variablesForTest": [],
|
||||
"isAdvancedConfig": false,
|
||||
"isCustomBody": false
|
||||
},
|
||||
"webhookId": "webhook1"
|
||||
}
|
||||
]
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
PublicTypebot,
|
||||
Step,
|
||||
Typebot,
|
||||
Webhook,
|
||||
} from 'models'
|
||||
import { CollaborationType, DashboardFolder, PrismaClient, User } from 'db'
|
||||
import { readFileSync } from 'fs'
|
||||
@ -19,6 +20,7 @@ export const teardownDatabase = async () => {
|
||||
await prisma.user.deleteMany({
|
||||
where: { id: { in: ['freeUser', 'proUser'] } },
|
||||
})
|
||||
await prisma.webhook.deleteMany()
|
||||
await prisma.credentials.deleteMany(ownerFilter)
|
||||
await prisma.dashboardFolder.deleteMany(ownerFilter)
|
||||
return prisma.typebot.deleteMany(ownerFilter)
|
||||
@ -37,8 +39,17 @@ export const createUsers = () =>
|
||||
],
|
||||
})
|
||||
|
||||
export const createWebhook = (typebotId: string) =>
|
||||
prisma.webhook.create({ data: { method: 'GET', typebotId, id: 'webhook1' } })
|
||||
export const createWebhook = async (
|
||||
typebotId: string,
|
||||
webhookProps?: Partial<Webhook>
|
||||
) => {
|
||||
try {
|
||||
await prisma.webhook.delete({ where: { id: 'webhook1' } })
|
||||
} catch {}
|
||||
return prisma.webhook.create({
|
||||
data: { method: 'GET', typebotId, id: 'webhook1', ...webhookProps },
|
||||
})
|
||||
}
|
||||
|
||||
export const createCollaboration = (
|
||||
userId: string,
|
||||
|
@ -2,11 +2,31 @@ import test, { expect, Page } from '@playwright/test'
|
||||
import { createWebhook, importTypebotInDatabase } from '../../services/database'
|
||||
import path from 'path'
|
||||
import { generate } from 'short-uuid'
|
||||
|
||||
const typebotId = generate()
|
||||
import { HttpMethod } from 'models'
|
||||
|
||||
test.describe('Webhook step', () => {
|
||||
test('its configuration should work', async ({ page }) => {
|
||||
test('easy configuration should work', async ({ page }) => {
|
||||
const typebotId = generate()
|
||||
await importTypebotInDatabase(
|
||||
path.join(__dirname, '../../fixtures/typebots/integrations/webhook.json'),
|
||||
{
|
||||
id: typebotId,
|
||||
}
|
||||
)
|
||||
await createWebhook(typebotId, { method: HttpMethod.POST })
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('text=Configure...')
|
||||
await page.fill(
|
||||
'input[placeholder="Your Webhook URL..."]',
|
||||
`${process.env.PLAYWRIGHT_BUILDER_TEST_BASE_URL}/api/mock/webhook-easy-config`
|
||||
)
|
||||
await page.click('text=Test the request')
|
||||
await expect(page.locator('div[role="textbox"] >> nth=-1')).toContainText(
|
||||
'"statusCode": 200'
|
||||
)
|
||||
})
|
||||
test('Generated body should work', async ({ page }) => {
|
||||
const typebotId = generate()
|
||||
await importTypebotInDatabase(
|
||||
path.join(__dirname, '../../fixtures/typebots/integrations/webhook.json'),
|
||||
{
|
||||
@ -17,12 +37,38 @@ test.describe('Webhook step', () => {
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('text=Configure...')
|
||||
await page.fill(
|
||||
'input[placeholder="Your Webhook URL..."]',
|
||||
`${process.env.PLAYWRIGHT_BUILDER_TEST_BASE_URL}/api/mock/webhook-easy-config`
|
||||
)
|
||||
await page.click('text=Advanced configuration')
|
||||
await page.click('text=GET')
|
||||
await page.click('text=POST')
|
||||
|
||||
await page.click('text=Test the request')
|
||||
await expect(page.locator('div[role="textbox"] >> nth=-1')).toContainText(
|
||||
'"message": "This is a sample result, it has been generated ⬇️"'
|
||||
)
|
||||
})
|
||||
test('its configuration should work', async ({ page }) => {
|
||||
const typebotId = generate()
|
||||
await importTypebotInDatabase(
|
||||
path.join(__dirname, '../../fixtures/typebots/integrations/webhook.json'),
|
||||
{
|
||||
id: typebotId,
|
||||
}
|
||||
)
|
||||
await createWebhook(typebotId)
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('text=Configure...')
|
||||
await page.fill(
|
||||
'input[placeholder="Your Webhook URL..."]',
|
||||
`${process.env.PLAYWRIGHT_BUILDER_TEST_BASE_URL}/api/mock/webhook`
|
||||
)
|
||||
await page.click('text=Advanced configuration')
|
||||
await page.click('text=GET')
|
||||
await page.click('text=POST')
|
||||
|
||||
await page.click('text=Query params')
|
||||
await page.click('text=Add a param')
|
||||
@ -45,6 +91,7 @@ test.describe('Webhook step', () => {
|
||||
)
|
||||
|
||||
await page.click('text=Body')
|
||||
await page.click('text=Custom body')
|
||||
await page.fill('div[role="textbox"]', '{ "customField": "{{secret 4}}" }')
|
||||
|
||||
await page.click('text=Variable values for test')
|
||||
|
@ -1,11 +1,13 @@
|
||||
import prisma from 'libs/prisma'
|
||||
import {
|
||||
defaultWebhookAttributes,
|
||||
KeyValue,
|
||||
PublicTypebot,
|
||||
ResultValues,
|
||||
Typebot,
|
||||
Variable,
|
||||
Webhook,
|
||||
WebhookOptions,
|
||||
WebhookResponse,
|
||||
WebhookStep,
|
||||
} from 'models'
|
||||
@ -35,14 +37,17 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
where: { id: typebotId },
|
||||
include: { webhooks: true },
|
||||
})) as unknown as Typebot & { webhooks: Webhook[] }
|
||||
const step = typebot.blocks.find(byId(blockId))?.steps.find(byId(stepId))
|
||||
const webhook = typebot.webhooks.find(byId((step as WebhookStep).webhookId))
|
||||
const step = typebot.blocks
|
||||
.find(byId(blockId))
|
||||
?.steps.find(byId(stepId)) as WebhookStep
|
||||
const webhook = typebot.webhooks.find(byId(step.webhookId))
|
||||
if (!webhook)
|
||||
return res
|
||||
.status(404)
|
||||
.send({ statusCode: 404, data: { message: `Couldn't find webhook` } })
|
||||
const preparedWebhook = prepareWebhookAttributes(webhook, step.options)
|
||||
const result = await executeWebhook(typebot)(
|
||||
webhook,
|
||||
preparedWebhook,
|
||||
variables,
|
||||
blockId,
|
||||
resultValues
|
||||
@ -52,6 +57,18 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
const prepareWebhookAttributes = (
|
||||
webhook: Webhook,
|
||||
options: WebhookOptions
|
||||
): Webhook => {
|
||||
if (options.isAdvancedConfig === false) {
|
||||
return { ...webhook, body: '{{state}}', ...defaultWebhookAttributes }
|
||||
} else if (options.isCustomBody === false) {
|
||||
return { ...webhook, body: '{{state}}' }
|
||||
}
|
||||
return webhook
|
||||
}
|
||||
|
||||
const executeWebhook =
|
||||
(typebot: Typebot) =>
|
||||
async (
|
||||
|
Reference in New Issue
Block a user