♻️ (webhook) Integrate webhook in typebot schema

Closes #313
This commit is contained in:
Baptiste Arnaud
2023-08-06 10:03:45 +02:00
parent 53e4bc2b75
commit fc25734689
66 changed files with 1501 additions and 876 deletions

View File

@@ -9,7 +9,7 @@ type Props = {
export const MakeComContent = ({ block }: Props) => {
const { webhooks } = useTypebot()
const webhook = webhooks.find(byId(block.webhookId))
const webhook = block.options.webhook ?? webhooks.find(byId(block.webhookId))
if (isNotDefined(webhook?.body))
return <Text color="gray.500">Configure...</Text>

View File

@@ -22,10 +22,17 @@ export const MakeComSettings = ({
const setLocalWebhook = useCallback(
async (newLocalWebhook: Webhook) => {
if (options.webhook) {
onOptionsChange({
...options,
webhook: newLocalWebhook,
})
return
}
_setLocalWebhook(newLocalWebhook)
await updateWebhook(newLocalWebhook.id, newLocalWebhook)
},
[updateWebhook]
[onOptionsChange, options, updateWebhook]
)
useEffect(() => {
@@ -33,20 +40,23 @@ export const MakeComSettings = ({
!localWebhook ||
localWebhook.url ||
!webhook?.url ||
webhook.url === localWebhook.url
webhook.url === localWebhook.url ||
options.webhook
)
return
setLocalWebhook({
...localWebhook,
url: webhook?.url,
})
}, [webhook, localWebhook, setLocalWebhook])
}, [webhook, localWebhook, setLocalWebhook, options.webhook])
const url = options.webhook?.url ?? localWebhook?.url
return (
<Stack spacing={4}>
<Alert status={localWebhook?.url ? 'success' : 'info'} rounded="md">
<Alert status={url ? 'success' : 'info'} rounded="md">
<AlertIcon />
{localWebhook?.url ? (
{url ? (
<>Your scenario is correctly configured 🚀</>
) : (
<Stack>
@@ -62,10 +72,10 @@ export const MakeComSettings = ({
</Stack>
)}
</Alert>
{localWebhook && (
{(localWebhook || options.webhook) && (
<WebhookAdvancedConfigForm
blockId={blockId}
webhook={localWebhook}
webhook={(options.webhook ?? localWebhook) as Webhook}
options={options}
onWebhookChange={setLocalWebhook}
onOptionsChange={onOptionsChange}

View File

@@ -9,7 +9,7 @@ type Props = {
export const PabblyConnectContent = ({ block }: Props) => {
const { webhooks } = useTypebot()
const webhook = webhooks.find(byId(block.webhookId))
const webhook = block.options.webhook ?? webhooks.find(byId(block.webhookId))
if (isNotDefined(webhook?.body))
return <Text color="gray.500">Configure...</Text>

View File

@@ -27,6 +27,13 @@ export const PabblyConnectSettings = ({
)
const setLocalWebhook = async (newLocalWebhook: Webhook) => {
if (options.webhook) {
onOptionsChange({
...options,
webhook: newLocalWebhook,
})
return
}
_setLocalWebhook(newLocalWebhook)
await updateWebhook(newLocalWebhook.id, newLocalWebhook)
}
@@ -38,11 +45,13 @@ export const PabblyConnectSettings = ({
url,
})
const url = options.webhook?.url ?? localWebhook?.url
return (
<Stack spacing={4}>
<Alert status={localWebhook?.url ? 'success' : 'info'} rounded="md">
<Alert status={url ? 'success' : 'info'} rounded="md">
<AlertIcon />
{localWebhook?.url ? (
{url ? (
<>Your scenario is correctly configured 🚀</>
) : (
<Stack>
@@ -60,15 +69,15 @@ export const PabblyConnectSettings = ({
</Alert>
<TextInput
placeholder="Paste webhook URL..."
defaultValue={localWebhook?.url ?? ''}
defaultValue={url ?? ''}
onChange={handleUrlChange}
withVariableButton={false}
debounceTimeout={0}
/>
{localWebhook && (
{(localWebhook || options.webhook) && (
<WebhookAdvancedConfigForm
blockId={blockId}
webhook={localWebhook}
webhook={(options.webhook ?? localWebhook) as Webhook}
options={options}
onWebhookChange={setLocalWebhook}
onOptionsChange={onOptionsChange}

View File

@@ -2,7 +2,7 @@ import prisma from '@/lib/prisma'
import { canReadTypebots } from '@/helpers/databaseRules'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { Typebot, Webhook } from '@typebot.io/schemas'
import { Typebot } from '@typebot.io/schemas'
import { z } from 'zod'
import { fetchLinkedTypebots } from '@/features/blocks/logic/typebotLink/helpers/fetchLinkedTypebots'
import { parseResultExample } from '../helpers/parseResultExample'
@@ -45,20 +45,15 @@ export const getResultExample = authenticatedProcedure
groups: true,
edges: true,
variables: true,
webhooks: true,
},
})) as
| (Pick<Typebot, 'groups' | 'edges' | 'variables'> & {
webhooks: Webhook[]
})
| null
})) as Pick<Typebot, 'groups' | 'edges' | 'variables'> | null
if (!typebot)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
const block = typebot.groups
.flatMap((g) => g.blocks)
.find((s) => s.id === blockId)
.flatMap((group) => group.blocks)
.find((block) => block.id === blockId)
if (!block)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Block not found' })

View File

@@ -2,9 +2,10 @@ import prisma from '@/lib/prisma'
import { canReadTypebots } from '@/helpers/databaseRules'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { Group, Typebot, Webhook, WebhookBlock } from '@typebot.io/schemas'
import { Group, IntegrationBlockType, Typebot } from '@typebot.io/schemas'
import { byId, isWebhookBlock, parseGroupTitle } from '@typebot.io/lib'
import { z } from 'zod'
import { Webhook } from '@typebot.io/prisma'
export const listWebhookBlocks = authenticatedProcedure
.meta({
@@ -28,6 +29,12 @@ export const listWebhookBlocks = authenticatedProcedure
webhookBlocks: z.array(
z.object({
id: z.string(),
type: z.enum([
IntegrationBlockType.WEBHOOK,
IntegrationBlockType.ZAPIER,
IntegrationBlockType.MAKE_COM,
IntegrationBlockType.PABBLY_CONNECT,
]),
label: z.string(),
url: z.string().optional(),
})
@@ -46,17 +53,27 @@ export const listWebhookBlocks = authenticatedProcedure
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
const webhookBlocks = (typebot?.groups as Group[]).reduce<
{ id: string; label: string; url: string | undefined }[]
{
id: string
label: string
url: string | undefined
type:
| IntegrationBlockType.WEBHOOK
| IntegrationBlockType.ZAPIER
| IntegrationBlockType.MAKE_COM
| IntegrationBlockType.PABBLY_CONNECT
}[]
>((webhookBlocks, group) => {
const blocks = group.blocks.filter((block) =>
isWebhookBlock(block)
) as WebhookBlock[]
const blocks = group.blocks.filter(isWebhookBlock)
return [
...webhookBlocks,
...blocks.map((b) => ({
id: b.id,
label: `${parseGroupTitle(group.title)} > ${b.id}`,
url: typebot?.webhooks.find(byId(b.webhookId))?.url ?? undefined,
...blocks.map((block) => ({
id: block.id,
type: block.type,
label: `${parseGroupTitle(group.title)} > ${block.id}`,
url: block.options.webhook
? block.options.webhook.url
: typebot?.webhooks.find(byId(block.webhookId))?.url ?? undefined,
})),
]
}, [])

View File

@@ -2,9 +2,10 @@ import prisma from '@/lib/prisma'
import { canWriteTypebots } from '@/helpers/databaseRules'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { Typebot, Webhook, WebhookBlock } from '@typebot.io/schemas'
import { Typebot, WebhookBlock } from '@typebot.io/schemas'
import { byId, isWebhookBlock } from '@typebot.io/lib'
import { z } from 'zod'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums'
export const subscribeWebhook = authenticatedProcedure
.meta({
@@ -34,9 +35,8 @@ export const subscribeWebhook = authenticatedProcedure
where: canWriteTypebots(typebotId, user),
select: {
groups: true,
webhooks: true,
},
})) as (Pick<Typebot, 'groups'> & { webhooks: Webhook[] }) | null
})) as Pick<Typebot, 'groups'> | null
if (!typebot)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
@@ -51,18 +51,50 @@ export const subscribeWebhook = authenticatedProcedure
message: 'Webhook block not found',
})
await prisma.webhook.upsert({
where: { id: webhookBlock.webhookId },
update: { url, body: '{{state}}', method: 'POST' },
create: {
url,
body: '{{state}}',
method: 'POST',
typebotId,
headers: [],
queryParams: [],
},
})
const newWebhook = {
id: webhookBlock.webhookId ?? webhookBlock.id,
url,
body: '{{state}}',
method: HttpMethod.POST,
headers: [],
queryParams: [],
}
if (webhookBlock.webhookId)
await prisma.webhook.upsert({
where: { id: webhookBlock.webhookId },
update: { url, body: newWebhook.body, method: newWebhook.method },
create: {
typebotId,
...newWebhook,
},
})
else {
const updatedGroups = typebot.groups.map((group) =>
group.id !== webhookBlock.groupId
? group
: {
...group,
blocks: group.blocks.map((block) =>
block.id !== webhookBlock.id
? block
: {
...block,
options: {
...webhookBlock.options,
webhook: newWebhook,
},
}
),
}
)
await prisma.typebot.updateMany({
where: { id: typebotId },
data: {
groups: updatedGroups,
},
})
}
return {
id: blockId,

View File

@@ -50,10 +50,45 @@ export const unsubscribeWebhook = authenticatedProcedure
message: 'Webhook block not found',
})
await prisma.webhook.update({
where: { id: webhookBlock.webhookId },
data: { url: null },
})
if (webhookBlock.webhookId)
await prisma.webhook.update({
where: { id: webhookBlock.webhookId },
data: { url: null },
})
else {
if (!webhookBlock.options.webhook)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Webhook block not found',
})
const updatedGroups = typebot.groups.map((group) =>
group.id !== webhookBlock.groupId
? group
: {
...group,
blocks: group.blocks.map((block) =>
block.id !== webhookBlock.id
? block
: {
...block,
options: {
...webhookBlock.options,
webhook: {
...webhookBlock.options.webhook,
url: undefined,
},
},
}
),
}
)
await prisma.typebot.updateMany({
where: { id: typebotId },
data: {
groups: updatedGroups,
},
})
}
return {
id: blockId,

View File

@@ -16,7 +16,6 @@ import {
Text,
} from '@chakra-ui/react'
import {
HttpMethod,
KeyValue,
VariableForTest,
ResponseVariableMapping,
@@ -31,6 +30,7 @@ import { QueryParamsInputs, HeadersInputs } from './KeyValueInputs'
import { DataVariableInputs } from './ResponseMappingInputs'
import { VariableForTestInputs } from './VariableForTestInputs'
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums'
type Props = {
blockId: string
@@ -78,9 +78,11 @@ export const WebhookAdvancedConfigForm = ({
onOptionsChange({ ...options, isCustomBody })
const executeTestRequest = async () => {
if (!typebot || !webhook) return
if (!typebot) return
setIsTestResponseLoading(true)
await Promise.all([updateWebhook(webhook.id, webhook), save()])
if (!options.webhook)
await Promise.all([updateWebhook(webhook.id, webhook), save()])
else await save()
const { data, error } = await executeWebhook(
typebot.id,
convertVariablesForTestToVariables(

View File

@@ -8,10 +8,10 @@ type Props = {
block: WebhookBlock
}
export const WebhookContent = ({ block: { webhookId, options } }: Props) => {
export const WebhookContent = ({ block: { options, webhookId } }: Props) => {
const { typebot } = useTypebot()
const { webhooks } = useTypebot()
const webhook = webhooks.find(byId(webhookId))
const webhook = options.webhook ?? webhooks.find(byId(webhookId))
if (!webhook?.url) return <Text color="gray.500">Configure...</Text>
return (

View File

@@ -21,25 +21,33 @@ export const WebhookSettings = ({
)
const setLocalWebhook = async (newLocalWebhook: Webhook) => {
if (options.webhook) {
onOptionsChange({ ...options, webhook: newLocalWebhook })
return
}
_setLocalWebhook(newLocalWebhook)
await updateWebhook(newLocalWebhook.id, newLocalWebhook)
}
const updateUrl = (url?: string) =>
localWebhook && setLocalWebhook({ ...localWebhook, url: url ?? null })
const updateUrl = (url: string) => {
if (options.webhook)
onOptionsChange({ ...options, webhook: { ...options.webhook, url } })
else if (localWebhook)
setLocalWebhook({ ...localWebhook, url: url ?? undefined })
}
if (!localWebhook) return <Spinner />
if (!localWebhook && !options.webhook) return <Spinner />
return (
<Stack spacing={4}>
<TextInput
placeholder="Paste webhook URL..."
defaultValue={localWebhook.url ?? ''}
defaultValue={options.webhook?.url ?? localWebhook?.url ?? ''}
onChange={updateUrl}
/>
<WebhookAdvancedConfigForm
blockId={blockId}
webhook={localWebhook}
webhook={(options.webhook ?? localWebhook) as Webhook}
options={options}
onWebhookChange={setLocalWebhook}
onOptionsChange={onOptionsChange}

View File

@@ -3,7 +3,7 @@ import {
createWebhook,
importTypebotInDatabase,
} from '@typebot.io/lib/playwright/databaseActions'
import { HttpMethod } from '@typebot.io/schemas'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums'
import { createId } from '@paralleldrive/cuid2'
import { getTestAsset } from '@/test/utils/playwright'
import { apiToken } from '@typebot.io/lib/playwright/databaseSetup'
@@ -26,7 +26,8 @@ test.describe('Builder', () => {
)
await page.click('text=Test the request')
await expect(page.locator('div[role="textbox"] >> nth=-1')).toContainText(
`"Group #1": "answer value", "Group #2": "20", "Group #2 (1)": "Yes"`, {timeout: 10000}
`"Group #1": "answer value", "Group #2": "20", "Group #2 (1)": "Yes"`,
{ timeout: 10000 }
)
})
@@ -126,6 +127,7 @@ test.describe('API', () => {
expect(webhookBlocks[0]).toEqual({
id: 'webhookBlock',
label: 'Webhook > webhookBlock',
type: 'Webhook',
})
})

View File

@@ -9,7 +9,7 @@ type Props = {
export const ZapierContent = ({ block }: Props) => {
const { webhooks } = useTypebot()
const webhook = webhooks.find(byId(block.webhookId))
const webhook = block.options.webhook ?? webhooks.find(byId(block.webhookId))
if (isNotDefined(webhook?.body))
return <Text color="gray.500">Configure...</Text>

View File

@@ -22,10 +22,17 @@ export const ZapierSettings = ({
const setLocalWebhook = useCallback(
async (newLocalWebhook: Webhook) => {
if (options.webhook) {
onOptionsChange({
...options,
webhook: newLocalWebhook,
})
return
}
_setLocalWebhook(newLocalWebhook)
await updateWebhook(newLocalWebhook.id, newLocalWebhook)
},
[updateWebhook]
[onOptionsChange, options, updateWebhook]
)
useEffect(() => {
@@ -33,20 +40,23 @@ export const ZapierSettings = ({
!localWebhook ||
localWebhook.url ||
!webhook?.url ||
webhook.url === localWebhook.url
webhook.url === localWebhook.url ||
options.webhook
)
return
setLocalWebhook({
...localWebhook,
url: webhook?.url,
})
}, [webhook, localWebhook, setLocalWebhook])
}, [webhook, localWebhook, setLocalWebhook, options.webhook])
const url = options.webhook?.url ?? localWebhook?.url
return (
<Stack spacing={4}>
<Alert status={localWebhook?.url ? 'success' : 'info'} rounded="md">
<Alert status={url ? 'success' : 'info'} rounded="md">
<AlertIcon />
{localWebhook?.url ? (
{url ? (
<>Your zap is correctly configured 🚀</>
) : (
<Stack>
@@ -62,10 +72,10 @@ export const ZapierSettings = ({
</Stack>
)}
</Alert>
{localWebhook && (
{(localWebhook || options.webhook) && (
<WebhookAdvancedConfigForm
blockId={blockId}
webhook={localWebhook}
webhook={(options.webhook ?? localWebhook) as Webhook}
options={options}
onWebhookChange={setLocalWebhook}
onOptionsChange={onOptionsChange}