2
0

♻️ (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

@@ -1,6 +1,6 @@
{ {
"id": "clf6ov7hg00001ao6q02sb8re", "id": "clf6ov7hg00001ao6q02sb8re",
"version": "3", "version": "4",
"createdAt": "2023-03-13T10:35:44.933Z", "createdAt": "2023-03-13T10:35:44.933Z",
"updatedAt": "2023-03-13T14:53:00.817Z", "updatedAt": "2023-03-13T14:53:00.817Z",
"icon": "🤖", "icon": "🤖",

View File

@@ -1,6 +1,6 @@
{ {
"id": "qcueq3ttys1ddagic7jsimp4", "id": "qcueq3ttys1ddagic7jsimp4",
"version": "3", "version": "4",
"createdAt": "2023-03-30T15:45:12.464Z", "createdAt": "2023-03-30T15:45:12.464Z",
"updatedAt": "2023-03-30T15:45:12.464Z", "updatedAt": "2023-03-30T15:45:12.464Z",
"icon": "🎭", "icon": "🎭",

View File

@@ -1,5 +1,6 @@
{ {
"id": "cl16la7p900990b1a72qjqbb3", "id": "cl16la7p900990b1a72qjqbb3",
"version": "4",
"createdAt": "2022-03-25T15:39:33.885Z", "createdAt": "2022-03-25T15:39:33.885Z",
"updatedAt": "2022-03-25T15:42:12.544Z", "updatedAt": "2022-03-25T15:42:12.544Z",
"name": "Customer Support", "name": "Customer Support",

View File

@@ -1,5 +1,6 @@
{ {
"id": "cl3u43chu40824z1acel3iw1c", "id": "cl3u43chu40824z1acel3iw1c",
"version": "4",
"createdAt": "2022-05-31T12:04:12.930Z", "createdAt": "2022-05-31T12:04:12.930Z",
"updatedAt": "2022-05-31T12:31:12.867Z", "updatedAt": "2022-05-31T12:31:12.867Z",
"icon": "🖼️", "icon": "🖼️",

View File

@@ -1,6 +1,6 @@
{ {
"id": "clh9fzu4b00031aotzr1ik4ba", "id": "clh9fzu4b00031aotzr1ik4ba",
"version": "3", "version": "4",
"createdAt": "2023-05-04T18:10:07.548Z", "createdAt": "2023-05-04T18:10:07.548Z",
"updatedAt": "2023-05-04T19:13:45.356Z", "updatedAt": "2023-05-04T19:13:45.356Z",
"icon": "🐶", "icon": "🐶",

View File

@@ -1,5 +1,6 @@
{ {
"id": "cl96ns7zc000dky099ku4bmav", "id": "cl96ns7zc000dky099ku4bmav",
"version": "4",
"createdAt": "2022-10-13T06:07:11.976Z", "createdAt": "2022-10-13T06:07:11.976Z",
"updatedAt": "2022-10-13T06:27:31.951Z", "updatedAt": "2022-10-13T06:27:31.951Z",
"icon": "💬", "icon": "💬",

View File

@@ -1,6 +1,6 @@
{ {
"id": "pjkxzxkujkbk2oml3d35w6zz", "id": "pjkxzxkujkbk2oml3d35w6zz",
"version": "3", "version": "4",
"createdAt": "2023-04-20T12:29:18.633Z", "createdAt": "2023-04-20T12:29:18.633Z",
"updatedAt": "2023-04-20T12:50:12.385Z", "updatedAt": "2023-04-20T12:50:12.385Z",
"icon": "🦾", "icon": "🦾",

View File

@@ -1,5 +1,6 @@
{ {
"id": "qgMiLSr4W1ftXocFncin1G", "id": "qgMiLSr4W1ftXocFncin1G",
"version": "4",
"createdAt": "2022-02-05T06:21:16.522Z", "createdAt": "2022-02-05T06:21:16.522Z",
"updatedAt": "2022-02-05T06:21:16.522Z", "updatedAt": "2022-02-05T06:21:16.522Z",
"name": "Lead Generation", "name": "Lead Generation",

View File

@@ -1,6 +1,6 @@
{ {
"id": "clg69gafk00011ar14aryt8ej", "id": "clg69gafk00011ar14aryt8ej",
"version": "3", "version": "4",
"createdAt": "2023-04-07T08:03:57.008Z", "createdAt": "2023-04-07T08:03:57.008Z",
"updatedAt": "2023-04-07T08:22:10.285Z", "updatedAt": "2023-04-07T08:22:10.285Z",
"icon": "🧲", "icon": "🧲",

View File

@@ -1,5 +1,6 @@
{ {
"id": "cl1seoz582103xk1at12y4ucb", "id": "cl1seoz582103xk1at12y4ucb",
"version": "4",
"createdAt": "2022-04-09T22:06:01.196Z", "createdAt": "2022-04-09T22:06:01.196Z",
"updatedAt": "2022-04-09T22:35:22.449Z", "updatedAt": "2022-04-09T22:35:22.449Z",
"icon": "🏆", "icon": "🏆",

View File

@@ -1,41 +1,41 @@
{ {
"id": "clf17krhw00011am023muokc1", "id": "d8z3y5ats5r0lyptw938re79",
"version": "3", "version": "4",
"createdAt": "2023-03-09T14:32:53.301Z", "createdAt": "2023-08-06T05:56:35.727Z",
"updatedAt": "2023-03-09T16:26:36.916Z", "updatedAt": "2023-08-06T06:39:16.019Z",
"icon": "🍿", "icon": "🍿",
"name": "Movie recommendation", "name": "Movie recommendation",
"folderId": null, "folderId": null,
"groups": [ "groups": [
{ {
"id": "kto4a8zwoqvagp14sjol0mqq", "id": "u6lpjibfjhyoqij5wjf9kvnl",
"title": "Start", "title": "Start",
"blocks": [ "blocks": [
{ {
"id": "dn2vorjxq275ulqwd97sgfjp", "id": "rha69fygov33vym1hf6z837p",
"type": "start", "type": "start",
"label": "Start", "label": "Start",
"groupId": "kto4a8zwoqvagp14sjol0mqq", "groupId": "u6lpjibfjhyoqij5wjf9kvnl",
"outgoingEdgeId": "ozfvttdxg70dy931m762ssc0" "outgoingEdgeId": "wfec8f4e1jtden2wqna6nrso"
} }
], ],
"graphCoordinates": { "x": 0, "y": 0 } "graphCoordinates": { "x": 0, "y": 0 }
}, },
{ {
"id": "vpsi7kfd4hiu38fd0txt3ndn", "id": "mjnkukpkpvf4ha2g4n5m804v",
"title": "Menu", "title": "Menu",
"blocks": [ "blocks": [
{ {
"id": "tmm2xkt5wqt5zmxzkyg2kx1q", "id": "kjlf184vxf0uorniwje28iqb",
"type": "Set variable", "type": "Set variable",
"groupId": "vpsi7kfd4hiu38fd0txt3ndn", "groupId": "mjnkukpkpvf4ha2g4n5m804v",
"options": { "options": {
"variableId": "vh5bxx07kl3016wr1undh2yb3", "variableId": "vh5bxx07kl3016wr1undh2yb3",
"expressionToEvaluate": "2f584d1ffe2b7fb082dd4e05038e9bd7" "expressionToEvaluate": "2f584d1ffe2b7fb082dd4e05038e9bd7"
} }
}, },
{ {
"id": "pd22jxxptmw66xhwddj8toos", "id": "f5rr5wi9zldun13tw79u9z2n",
"type": "text", "type": "text",
"content": { "content": {
"richText": [ "richText": [
@@ -47,18 +47,18 @@
} }
] ]
}, },
"groupId": "vpsi7kfd4hiu38fd0txt3ndn" "groupId": "mjnkukpkpvf4ha2g4n5m804v"
}, },
{ {
"id": "vwfnjnfnqnlvgraai2k4kl9e", "id": "c7swi84rmdvrul0wz5kxtplm",
"type": "image", "type": "image",
"content": { "content": {
"url": "https://media3.giphy.com/media/BElb9DVpHezcZufOhl/giphy-downsized.gif?cid=fe3852a3uwhsp1sc3j6avr625v5e94h1v8o3wfb1ii3xwswk&rid=giphy-downsized.gif&ct=g" "url": "https://media3.giphy.com/media/BElb9DVpHezcZufOhl/giphy-downsized.gif?cid=fe3852a3uwhsp1sc3j6avr625v5e94h1v8o3wfb1ii3xwswk&rid=giphy-downsized.gif&ct=g"
}, },
"groupId": "vpsi7kfd4hiu38fd0txt3ndn" "groupId": "mjnkukpkpvf4ha2g4n5m804v"
}, },
{ {
"id": "gr8qlv926y817febq85zl0y0", "id": "nlihfc4ptxnxoktqblh6mcql",
"type": "text", "type": "text",
"content": { "content": {
"richText": [ "richText": [
@@ -68,28 +68,28 @@
} }
] ]
}, },
"groupId": "vpsi7kfd4hiu38fd0txt3ndn" "groupId": "mjnkukpkpvf4ha2g4n5m804v"
}, },
{ {
"id": "fm1894ok09bdyjpqhqth1p7q", "id": "vr73urm54d9mq2oqg7ey1xh8",
"type": "image", "type": "image",
"content": { "content": {
"url": "https://www.themoviedb.org/assets/2/v4/logos/v2/blue_square_1-5bdc75aaebeb75dc7ae79426ddd9be3b2be1e342510f8202baf6bffa71d7f5c4.svg" "url": "https://www.themoviedb.org/assets/2/v4/logos/v2/blue_square_1-5bdc75aaebeb75dc7ae79426ddd9be3b2be1e342510f8202baf6bffa71d7f5c4.svg"
}, },
"groupId": "vpsi7kfd4hiu38fd0txt3ndn" "groupId": "mjnkukpkpvf4ha2g4n5m804v"
}, },
{ {
"id": "qllnzuo1zxr5ef8bq9kjdjel", "id": "g5yyuh9g75x7xa7fgqhhi1zz",
"type": "text", "type": "text",
"content": { "content": {
"richText": [ "richText": [
{ "type": "p", "children": [{ "text": "How can I we help?" }] } { "type": "p", "children": [{ "text": "How can I we help?" }] }
] ]
}, },
"groupId": "vpsi7kfd4hiu38fd0txt3ndn" "groupId": "mjnkukpkpvf4ha2g4n5m804v"
}, },
{ {
"id": "sodsq9mcigwvogmwx0t4jvil", "id": "tzf45bvd8iquoxz7qgta8v94",
"type": "choice input", "type": "choice input",
"items": [ "items": [
{ {
@@ -97,39 +97,52 @@
"type": 0, "type": 0,
"blockId": "sodsq9mcigwvogmwx0t4jvil", "blockId": "sodsq9mcigwvogmwx0t4jvil",
"content": "Select a genre 💅", "content": "Select a genre 💅",
"outgoingEdgeId": "o8n3df42rx0og3vlkzhskp7r" "outgoingEdgeId": "t8qyjpigrz7cdl8gxl1wxlwj"
}, },
{ {
"id": "i8ls2f8inq2ovuijj6l7rbcq", "id": "i8ls2f8inq2ovuijj6l7rbcq",
"type": 0, "type": 0,
"blockId": "sodsq9mcigwvogmwx0t4jvil", "blockId": "sodsq9mcigwvogmwx0t4jvil",
"content": "See what's trending 🔝", "content": "See what's trending 🔝",
"outgoingEdgeId": "cfwctrcqoucw6djnn0j2zsv0" "outgoingEdgeId": "tjn2ljosqyd4aj9dk8mnifsu"
} }
], ],
"groupId": "vpsi7kfd4hiu38fd0txt3ndn", "groupId": "mjnkukpkpvf4ha2g4n5m804v",
"options": { "buttonLabel": "Send", "isMultipleChoice": false } "options": { "buttonLabel": "Send", "isMultipleChoice": false }
} }
], ],
"graphCoordinates": { "x": 255.24609375, "y": 173.00390625 } "graphCoordinates": { "x": 255.25, "y": 172.89 }
}, },
{ {
"id": "xjwiuplgl79ezx460xfuplgr", "id": "kq1g5z6pz4buot7sawqdrr3s",
"title": "Genre", "title": "Genre",
"blocks": [ "blocks": [
{ {
"id": "vqrsszjis5qtbalo8d0fwb1j", "id": "ecwz96cghzp4ji3lyx7taxt1",
"type": "text", "type": "text",
"content": { "content": {
"richText": [{ "type": "p", "children": [{ "text": "Sure!" }] }] "richText": [{ "type": "p", "children": [{ "text": "Sure!" }] }]
}, },
"groupId": "xjwiuplgl79ezx460xfuplgr" "groupId": "kq1g5z6pz4buot7sawqdrr3s"
}, },
{ {
"id": "lmoqmlovcsg2l0uzkm9sohor", "id": "gd4lt2pcljer6zaf7v9hkr1k",
"type": "Webhook", "type": "Webhook",
"groupId": "xjwiuplgl79ezx460xfuplgr", "groupId": "kq1g5z6pz4buot7sawqdrr3s",
"options": { "options": {
"webhook": {
"id": "t4pht3ndfc8tu9geovi7czqm",
"url": "https://api.themoviedb.org/3/genre/movie/list",
"method": "GET",
"headers": [],
"queryParams": [
{
"id": "gq6m7x2k20qzrj752qi2zpmu",
"key": "api_key",
"value": "{{API Key}}"
}
]
},
"isCustomBody": false, "isCustomBody": false,
"isAdvancedConfig": true, "isAdvancedConfig": true,
"variablesForTest": [ "variablesForTest": [
@@ -151,11 +164,10 @@
"variableId": "vwc00rydyp035vtb0nlaqyzwr" "variableId": "vwc00rydyp035vtb0nlaqyzwr"
} }
] ]
}, }
"webhookId": "kbd4kqs5lk58nyeggctha9hc"
}, },
{ {
"id": "v2516de17eigf6mbccy2t6dr", "id": "qeyvu7uq5tkvo7uo8iaj87z5",
"type": "text", "type": "text",
"content": { "content": {
"richText": [ "richText": [
@@ -165,10 +177,10 @@
} }
] ]
}, },
"groupId": "xjwiuplgl79ezx460xfuplgr" "groupId": "kq1g5z6pz4buot7sawqdrr3s"
}, },
{ {
"id": "eqm19tzeh7kullwld8auoqy4", "id": "nwuk2clo78hmnh4d0g31u9xg",
"type": "choice input", "type": "choice input",
"items": [ "items": [
{ {
@@ -178,7 +190,7 @@
"content": "Click to edit" "content": "Click to edit"
} }
], ],
"groupId": "xjwiuplgl79ezx460xfuplgr", "groupId": "kq1g5z6pz4buot7sawqdrr3s",
"options": { "options": {
"variableId": "vkmbb3rb2hcfd2io1fhf7rz5x", "variableId": "vkmbb3rb2hcfd2io1fhf7rz5x",
"buttonLabel": "Send", "buttonLabel": "Send",
@@ -187,36 +199,55 @@
} }
}, },
{ {
"id": "s27x4vixvakw62rwlsx61vw6", "id": "j7pm34um4piuyabwlobjc356",
"type": "Set variable", "type": "Set variable",
"groupId": "xjwiuplgl79ezx460xfuplgr", "groupId": "kq1g5z6pz4buot7sawqdrr3s",
"options": { "options": {
"type": "Map item with same index",
"isCode": true, "isCode": true,
"variableId": "vwewa4yugqch2sswdpneszk3i", "variableId": "vwewa4yugqch2sswdpneszk3i",
"mapListItemParams": {
"baseItemVariableId": "vkmbb3rb2hcfd2io1fhf7rz5x",
"baseListVariableId": "vx0bbqzug4vk3zpc31ly8k7al",
"targetListVariableId": "vwc00rydyp035vtb0nlaqyzwr"
},
"expressionToEvaluate": "{{Genre IDs}}.at({{Genres}}.indexOf({{Selected genre}}))" "expressionToEvaluate": "{{Genre IDs}}.at({{Genres}}.indexOf({{Selected genre}}))"
}, },
"outgoingEdgeId": "vcuv47fbcqrtuelfy4i6ctqz" "outgoingEdgeId": "tfuuwjnpn7mftd5s65mbhytd"
} }
], ],
"graphCoordinates": { "x": 630.84765625, "y": 333.68359375 } "graphCoordinates": { "x": 630.85, "y": 333.68 }
}, },
{ {
"id": "dewz832l9kse7xx1vhkihbk6", "id": "d6v9lh83c7zuwrhf2mmo6nxo",
"title": "Trending", "title": "Trending",
"blocks": [ "blocks": [
{ {
"id": "yqci070mkxfjjdok5yztj1nx", "id": "edokvbp15ubqeuydw9n7wf4w",
"type": "text", "type": "text",
"content": { "content": {
"richText": [{ "type": "p", "children": [{ "text": "Sure!" }] }] "richText": [{ "type": "p", "children": [{ "text": "Sure!" }] }]
}, },
"groupId": "dewz832l9kse7xx1vhkihbk6" "groupId": "d6v9lh83c7zuwrhf2mmo6nxo"
}, },
{ {
"id": "xkd9bmjx1xf6uglewaeds8d8", "id": "pwxb57b8nc2bp764vcdstois",
"type": "Webhook", "type": "Webhook",
"groupId": "dewz832l9kse7xx1vhkihbk6", "groupId": "d6v9lh83c7zuwrhf2mmo6nxo",
"options": { "options": {
"webhook": {
"id": "a5oaijpqxo5a0mqrnjqg3tyt",
"url": "https://api.themoviedb.org/3/trending/movie/week",
"method": "GET",
"headers": [],
"queryParams": [
{
"id": "x9nxr63itm2lvbe8dmi53mi7",
"key": "api_key",
"value": "{{API Key}}"
}
]
},
"isCustomBody": false, "isCustomBody": false,
"isAdvancedConfig": true, "isAdvancedConfig": true,
"variablesForTest": [ "variablesForTest": [
@@ -238,11 +269,10 @@
"variableId": "vcmybxcoaytd2geo5sqx7v8hw" "variableId": "vcmybxcoaytd2geo5sqx7v8hw"
} }
] ]
}, }
"webhookId": "ohc5k58i0bs4i2vc5ymjxqbx"
}, },
{ {
"id": "u6q1nvxowaszfdu4g42nqz38", "id": "ruhgtbpv18cy2g5ujavljkku",
"type": "text", "type": "text",
"content": { "content": {
"richText": [ "richText": [
@@ -252,10 +282,10 @@
} }
] ]
}, },
"groupId": "dewz832l9kse7xx1vhkihbk6" "groupId": "d6v9lh83c7zuwrhf2mmo6nxo"
}, },
{ {
"id": "stlcncbupmsjxjipi86s45hy", "id": "krcvvncnqtn99v0qe1dzudrk",
"type": "choice input", "type": "choice input",
"items": [ "items": [
{ {
@@ -265,7 +295,7 @@
"content": "Click to edit" "content": "Click to edit"
} }
], ],
"groupId": "dewz832l9kse7xx1vhkihbk6", "groupId": "d6v9lh83c7zuwrhf2mmo6nxo",
"options": { "options": {
"variableId": "vulnb1om2fk8mvkcesl8s15cr", "variableId": "vulnb1om2fk8mvkcesl8s15cr",
"buttonLabel": "Send", "buttonLabel": "Send",
@@ -274,25 +304,31 @@
} }
}, },
{ {
"id": "h0mbyxa1o6kyhpv2gwbweunu", "id": "mgn6uuw2yebmengsukjramjx",
"type": "Set variable", "type": "Set variable",
"groupId": "dewz832l9kse7xx1vhkihbk6", "groupId": "d6v9lh83c7zuwrhf2mmo6nxo",
"options": { "options": {
"type": "Map item with same index",
"isCode": true, "isCode": true,
"variableId": "vzslfw8oyo1f08uo5rpkegn0x", "variableId": "vzslfw8oyo1f08uo5rpkegn0x",
"mapListItemParams": {
"baseItemVariableId": "vulnb1om2fk8mvkcesl8s15cr",
"baseListVariableId": "vkzk96oh1pmdjv2bt5ps60qc0",
"targetListVariableId": "vcmybxcoaytd2geo5sqx7v8hw"
},
"expressionToEvaluate": "{{Trending IDs}}.at({{Trending Movies}}.indexOf({{Selected Trending Movie}}))" "expressionToEvaluate": "{{Trending IDs}}.at({{Trending Movies}}.indexOf({{Selected Trending Movie}}))"
}, },
"outgoingEdgeId": "ctxcurho3eq3dkna3emyvpwr" "outgoingEdgeId": "ual6xszx6tfcxqrnihc6zrvx"
} }
], ],
"graphCoordinates": { "x": 621.875, "y": 933.359375 } "graphCoordinates": { "x": 628.03, "y": 965.53 }
}, },
{ {
"id": "a1c99wep8eqambqjw8g8yeo8", "id": "v35sky44jzz9fkwwul2qxufl",
"title": "Movies by genre", "title": "Movies by genre",
"blocks": [ "blocks": [
{ {
"id": "dbp1cdzqy7txabinmm5osi1a", "id": "g2pgwx5yr1ou9vkoy6gdwuor",
"type": "text", "type": "text",
"content": { "content": {
"richText": [ "richText": [
@@ -306,13 +342,31 @@
} }
] ]
}, },
"groupId": "a1c99wep8eqambqjw8g8yeo8" "groupId": "v35sky44jzz9fkwwul2qxufl"
}, },
{ {
"id": "ym35vqr6euzhmtj8i03yzilz", "id": "a2datk3pv8o6xgitwjsq61m2",
"type": "Webhook", "type": "Webhook",
"groupId": "a1c99wep8eqambqjw8g8yeo8", "groupId": "v35sky44jzz9fkwwul2qxufl",
"options": { "options": {
"webhook": {
"id": "em7huyvp98pd6hr25md0l6hb",
"url": "https://api.themoviedb.org/3/discover/movie",
"method": "GET",
"headers": [],
"queryParams": [
{
"id": "mgwlp399a056o9jo93tjqp02",
"key": "api_key",
"value": "{{API Key}}"
},
{
"id": "dv4wioynywqo57jq8lakq3yr",
"key": "with_genres",
"value": "{{Selected genre ID}}"
}
]
},
"isCustomBody": false, "isCustomBody": false,
"isAdvancedConfig": true, "isAdvancedConfig": true,
"variablesForTest": [ "variablesForTest": [
@@ -339,11 +393,10 @@
"variableId": "vhc2pc1sv4xc778r9od2ctooz" "variableId": "vhc2pc1sv4xc778r9od2ctooz"
} }
] ]
}, }
"webhookId": "brjx15qipztmljhh31bsxj4u"
}, },
{ {
"id": "dxp0gakw90f3ckahjuphx5ir", "id": "tr5y76tx9ca336f8ob9odfa6",
"type": "choice input", "type": "choice input",
"items": [ "items": [
{ {
@@ -353,7 +406,7 @@
"content": "Click to edit" "content": "Click to edit"
} }
], ],
"groupId": "a1c99wep8eqambqjw8g8yeo8", "groupId": "v35sky44jzz9fkwwul2qxufl",
"options": { "options": {
"variableId": "vyyr3j2pu76uzvf88laai8snl", "variableId": "vyyr3j2pu76uzvf88laai8snl",
"buttonLabel": "Send", "buttonLabel": "Send",
@@ -362,38 +415,57 @@
} }
}, },
{ {
"id": "hufag2k15sttgg2v2y1mipxz", "id": "vudr8jrv2k3x0ubemt39tv7a",
"type": "Set variable", "type": "Set variable",
"groupId": "a1c99wep8eqambqjw8g8yeo8", "groupId": "v35sky44jzz9fkwwul2qxufl",
"options": { "options": {
"type": "Map item with same index",
"isCode": true, "isCode": true,
"variableId": "vzslfw8oyo1f08uo5rpkegn0x", "variableId": "vzslfw8oyo1f08uo5rpkegn0x",
"mapListItemParams": {
"baseItemVariableId": "vyyr3j2pu76uzvf88laai8snl",
"baseListVariableId": "vad8vq3jfyybxo4la57hfd529",
"targetListVariableId": "vhc2pc1sv4xc778r9od2ctooz"
},
"expressionToEvaluate": "const movieIndex = {{Movies}}.indexOf({{Selected Movie}})\n\nreturn {{Movie IDs}}.at(movieIndex)" "expressionToEvaluate": "const movieIndex = {{Movies}}.indexOf({{Selected Movie}})\n\nreturn {{Movie IDs}}.at(movieIndex)"
}, },
"outgoingEdgeId": "wv1qglc5fmexvpaei0xbhtzm" "outgoingEdgeId": "r4wyd2185zhen98r5pmx53g9"
} }
], ],
"graphCoordinates": { "x": 977.484375, "y": 330.8515625 } "graphCoordinates": { "x": 977.48, "y": 330.85 }
}, },
{ {
"id": "xutzzlxo0f5hs9q8ga1e8tl9", "id": "uozlg88loeb8xegu6y4le6k8",
"title": "Movie details", "title": "Movie details",
"blocks": [ "blocks": [
{ {
"id": "tsferop3f80u2jq9p8ip964h", "id": "ve9m7fromxw4tbm8558n8520",
"type": "text", "type": "text",
"content": { "content": {
"richText": [ "richText": [
{ "type": "p", "children": [{ "text": "Excellent choice 🔥" }] } { "type": "p", "children": [{ "text": "Excellent choice 🔥" }] }
] ]
}, },
"groupId": "xutzzlxo0f5hs9q8ga1e8tl9" "groupId": "uozlg88loeb8xegu6y4le6k8"
}, },
{ {
"id": "sssp7cez39ndix4owjl8x09t", "id": "xag9d5i5td40kdt3poyq5g4b",
"type": "Webhook", "type": "Webhook",
"groupId": "xutzzlxo0f5hs9q8ga1e8tl9", "groupId": "uozlg88loeb8xegu6y4le6k8",
"options": { "options": {
"webhook": {
"id": "ewrjdmbrm2yksvjrbp0z9duy",
"url": "https://api.themoviedb.org/3/movie/{{Selected Movie ID}}",
"method": "GET",
"headers": [],
"queryParams": [
{
"id": "f33u3unbezibfdv54kbkien3",
"key": "api_key",
"value": "{{API Key}}"
}
]
},
"isCustomBody": false, "isCustomBody": false,
"isAdvancedConfig": true, "isAdvancedConfig": true,
"variablesForTest": [ "variablesForTest": [
@@ -425,13 +497,12 @@
"variableId": "vzf5ryexokpr4dihiur2spm8z" "variableId": "vzf5ryexokpr4dihiur2spm8z"
} }
] ]
}, }
"webhookId": "d0x75v11voy19hnrrqnjlwpv"
}, },
{ {
"id": "e4h6b2vb409zkwa0jrpyjpt0", "id": "d0rsus9shxj8iowczbcaw53i",
"type": "Set variable", "type": "Set variable",
"groupId": "xutzzlxo0f5hs9q8ga1e8tl9", "groupId": "uozlg88loeb8xegu6y4le6k8",
"options": { "options": {
"isCode": false, "isCode": false,
"variableId": "vwitf3um5uweynypc0hxxwm14", "variableId": "vwitf3um5uweynypc0hxxwm14",
@@ -439,23 +510,23 @@
} }
}, },
{ {
"id": "ze726gr9fs7elsdqekbijckv", "id": "nqnry4c1z3wwcni8rwpduuhe",
"type": "image", "type": "image",
"content": { "url": "{{Poster URL}}" }, "content": { "url": "{{Poster URL}}" },
"groupId": "xutzzlxo0f5hs9q8ga1e8tl9" "groupId": "uozlg88loeb8xegu6y4le6k8"
}, },
{ {
"id": "t21xt4nr48hyu2qk2ij0d2oe", "id": "lhljw54rdykyqtjiuh6jsl5c",
"type": "text", "type": "text",
"content": { "content": {
"richText": [ "richText": [
{ "type": "p", "children": [{ "text": "{{Movie Overview}}" }] } { "type": "p", "children": [{ "text": "{{Movie Overview}}" }] }
] ]
}, },
"groupId": "xutzzlxo0f5hs9q8ga1e8tl9" "groupId": "uozlg88loeb8xegu6y4le6k8"
}, },
{ {
"id": "nahhsfc6hsok5sufxcknopxr", "id": "yifjhiamifo1y4ay7vols0mm",
"type": "choice input", "type": "choice input",
"items": [ "items": [
{ {
@@ -463,7 +534,7 @@
"type": 0, "type": 0,
"blockId": "nahhsfc6hsok5sufxcknopxr", "blockId": "nahhsfc6hsok5sufxcknopxr",
"content": "Watch the movie", "content": "Watch the movie",
"outgoingEdgeId": "v2qrjwz43nej3nxbkbcq3kyd" "outgoingEdgeId": "fteu5frsbj8wejfhhwzuv8t1"
}, },
{ {
"id": "j9d1pf2tndax0itezys7t73c", "id": "j9d1pf2tndax0itezys7t73c",
@@ -472,44 +543,44 @@
"content": "Find something else" "content": "Find something else"
} }
], ],
"groupId": "xutzzlxo0f5hs9q8ga1e8tl9", "groupId": "uozlg88loeb8xegu6y4le6k8",
"options": { "buttonLabel": "Send", "isMultipleChoice": false }, "options": { "buttonLabel": "Send", "isMultipleChoice": false },
"outgoingEdgeId": "rddv20ku82g2yyagatt95t3r" "outgoingEdgeId": "ntef5v9bt7vn4wg8s3dfm8yl"
} }
], ],
"graphCoordinates": { "x": 1198.4140625, "y": 846.53515625 } "graphCoordinates": { "x": 1151.05, "y": 913.34 }
}, },
{ {
"id": "vgulvp3bw1bi9em7yq6jfdrv", "id": "x4d8cdsyzoqz6vzsurnb8twc",
"title": "Redirect to IMDB", "title": "Redirect to IMDB",
"blocks": [ "blocks": [
{ {
"id": "y7z3nkawu4dwh1otgcwbenyz", "id": "mw0e0bzwiokhndkkncp9niu2",
"type": "Redirect", "type": "Redirect",
"groupId": "vgulvp3bw1bi9em7yq6jfdrv", "groupId": "x4d8cdsyzoqz6vzsurnb8twc",
"options": { "options": {
"url": "https://m.imdb.com/title/{{IMDB ID}}", "url": "https://m.imdb.com/title/{{IMDB ID}}",
"isNewTab": false "isNewTab": false
} }
} }
], ],
"graphCoordinates": { "x": 1693.453125, "y": 1324.9921875 } "graphCoordinates": { "x": 1616.55, "y": 1411.44 }
}, },
{ {
"id": "trfx6m1qiu7awvwjfnhfd712", "id": "x9610wtdv125hg56wicm2qmv",
"title": "Return to menu", "title": "Return to menu",
"blocks": [ "blocks": [
{ {
"id": "jumese0l44liwke0l5glv0fs", "id": "efto9jivvcvomj3kltf57hbb",
"type": "Jump", "type": "Jump",
"groupId": "trfx6m1qiu7awvwjfnhfd712", "groupId": "x9610wtdv125hg56wicm2qmv",
"options": { "options": {
"blockId": "sodsq9mcigwvogmwx0t4jvil", "blockId": "sodsq9mcigwvogmwx0t4jvil",
"groupId": "vpsi7kfd4hiu38fd0txt3ndn" "groupId": "mjnkukpkpvf4ha2g4n5m804v"
} }
} }
], ],
"graphCoordinates": { "x": 1690.52734375, "y": 1501.76953125 } "graphCoordinates": { "x": 1619.34, "y": 1586.5 }
} }
], ],
"variables": [ "variables": [
@@ -532,70 +603,70 @@
], ],
"edges": [ "edges": [
{ {
"id": "ozfvttdxg70dy931m762ssc0", "id": "wfec8f4e1jtden2wqna6nrso",
"to": { "groupId": "vpsi7kfd4hiu38fd0txt3ndn" }, "to": { "groupId": "mjnkukpkpvf4ha2g4n5m804v" },
"from": { "from": {
"blockId": "dn2vorjxq275ulqwd97sgfjp", "blockId": "rha69fygov33vym1hf6z837p",
"groupId": "kto4a8zwoqvagp14sjol0mqq" "groupId": "u6lpjibfjhyoqij5wjf9kvnl"
} }
}, },
{ {
"id": "o8n3df42rx0og3vlkzhskp7r", "id": "t8qyjpigrz7cdl8gxl1wxlwj",
"to": { "groupId": "xjwiuplgl79ezx460xfuplgr" }, "to": { "groupId": "kq1g5z6pz4buot7sawqdrr3s" },
"from": { "from": {
"itemId": "kaimvzg9igdtktgou5m3s1bw", "itemId": "kaimvzg9igdtktgou5m3s1bw",
"blockId": "sodsq9mcigwvogmwx0t4jvil", "blockId": "tzf45bvd8iquoxz7qgta8v94",
"groupId": "vpsi7kfd4hiu38fd0txt3ndn" "groupId": "mjnkukpkpvf4ha2g4n5m804v"
} }
}, },
{ {
"id": "vcuv47fbcqrtuelfy4i6ctqz", "id": "tfuuwjnpn7mftd5s65mbhytd",
"to": { "groupId": "a1c99wep8eqambqjw8g8yeo8" }, "to": { "groupId": "v35sky44jzz9fkwwul2qxufl" },
"from": { "from": {
"blockId": "s27x4vixvakw62rwlsx61vw6", "blockId": "j7pm34um4piuyabwlobjc356",
"groupId": "xjwiuplgl79ezx460xfuplgr" "groupId": "kq1g5z6pz4buot7sawqdrr3s"
} }
}, },
{ {
"id": "wv1qglc5fmexvpaei0xbhtzm", "id": "r4wyd2185zhen98r5pmx53g9",
"to": { "groupId": "xutzzlxo0f5hs9q8ga1e8tl9" }, "to": { "groupId": "uozlg88loeb8xegu6y4le6k8" },
"from": { "from": {
"blockId": "hufag2k15sttgg2v2y1mipxz", "blockId": "vudr8jrv2k3x0ubemt39tv7a",
"groupId": "a1c99wep8eqambqjw8g8yeo8" "groupId": "v35sky44jzz9fkwwul2qxufl"
} }
}, },
{ {
"id": "v2qrjwz43nej3nxbkbcq3kyd", "id": "fteu5frsbj8wejfhhwzuv8t1",
"to": { "groupId": "vgulvp3bw1bi9em7yq6jfdrv" }, "to": { "groupId": "x4d8cdsyzoqz6vzsurnb8twc" },
"from": { "from": {
"itemId": "n4818dnrb4arw1xh5v0ot8vz", "itemId": "n4818dnrb4arw1xh5v0ot8vz",
"blockId": "nahhsfc6hsok5sufxcknopxr", "blockId": "yifjhiamifo1y4ay7vols0mm",
"groupId": "xutzzlxo0f5hs9q8ga1e8tl9" "groupId": "uozlg88loeb8xegu6y4le6k8"
} }
}, },
{ {
"id": "cfwctrcqoucw6djnn0j2zsv0", "id": "tjn2ljosqyd4aj9dk8mnifsu",
"to": { "groupId": "dewz832l9kse7xx1vhkihbk6" }, "to": { "groupId": "d6v9lh83c7zuwrhf2mmo6nxo" },
"from": { "from": {
"itemId": "i8ls2f8inq2ovuijj6l7rbcq", "itemId": "i8ls2f8inq2ovuijj6l7rbcq",
"blockId": "sodsq9mcigwvogmwx0t4jvil", "blockId": "tzf45bvd8iquoxz7qgta8v94",
"groupId": "vpsi7kfd4hiu38fd0txt3ndn" "groupId": "mjnkukpkpvf4ha2g4n5m804v"
} }
}, },
{ {
"id": "rddv20ku82g2yyagatt95t3r", "id": "ntef5v9bt7vn4wg8s3dfm8yl",
"to": { "groupId": "trfx6m1qiu7awvwjfnhfd712" }, "to": { "groupId": "x9610wtdv125hg56wicm2qmv" },
"from": { "from": {
"blockId": "nahhsfc6hsok5sufxcknopxr", "blockId": "yifjhiamifo1y4ay7vols0mm",
"groupId": "xutzzlxo0f5hs9q8ga1e8tl9" "groupId": "uozlg88loeb8xegu6y4le6k8"
} }
}, },
{ {
"id": "ctxcurho3eq3dkna3emyvpwr", "id": "ual6xszx6tfcxqrnihc6zrvx",
"to": { "groupId": "xutzzlxo0f5hs9q8ga1e8tl9" }, "to": { "groupId": "uozlg88loeb8xegu6y4le6k8" },
"from": { "from": {
"blockId": "h0mbyxa1o6kyhpv2gwbweunu", "blockId": "mgn6uuw2yebmengsukjramjx",
"groupId": "dewz832l9kse7xx1vhkihbk6" "groupId": "d6v9lh83c7zuwrhf2mmo6nxo"
} }
} }
], ],
@@ -632,7 +703,6 @@
}, },
"publicId": null, "publicId": null,
"customDomain": null, "customDomain": null,
"workspaceId": "freeWorkspace",
"resultsTablePreferences": null, "resultsTablePreferences": null,
"isArchived": false, "isArchived": false,
"isClosed": false "isClosed": false

View File

@@ -1,6 +1,6 @@
{ {
"id": "clesntjqu00011a4xkgffc3p0", "id": "clesntjqu00011a4xkgffc3p0",
"version": "3", "version": "4",
"createdAt": "2023-03-03T14:57:41.430Z", "createdAt": "2023-03-03T14:57:41.430Z",
"updatedAt": "2023-03-03T16:14:29.268Z", "updatedAt": "2023-03-03T16:14:29.268Z",
"icon": "⭐", "icon": "⭐",

View File

@@ -1,6 +1,6 @@
{ {
"id": "qkvenb8ur2y0ahlbckmx7law", "id": "qkvenb8ur2y0ahlbckmx7law",
"version": "3", "version": "4",
"createdAt": "2023-02-22T14:26:28.592Z", "createdAt": "2023-02-22T14:26:28.592Z",
"updatedAt": "2023-02-22T14:28:05.063Z", "updatedAt": "2023-02-22T14:28:05.063Z",
"icon": null, "icon": null,

View File

@@ -1,6 +1,6 @@
{ {
"id": "mdbjvgqhqypbsdd0airkzcwu", "id": "mdbjvgqhqypbsdd0airkzcwu",
"version": "3", "version": "4",
"createdAt": "2023-06-05T15:40:37.278Z", "createdAt": "2023-06-05T15:40:37.278Z",
"updatedAt": "2023-06-05T15:45:43.049Z", "updatedAt": "2023-06-05T15:45:43.049Z",
"icon": "🍫", "icon": "🍫",

View File

@@ -1,5 +1,6 @@
{ {
"id": "cl1qz4luj04007w1ai7rk1j5q", "id": "cl1qz4luj04007w1ai7rk1j5q",
"version": "4",
"createdAt": "2022-04-08T22:02:30.427Z", "createdAt": "2022-04-08T22:02:30.427Z",
"updatedAt": "2022-04-08T23:03:34.726Z", "updatedAt": "2022-04-08T23:03:34.726Z",
"icon": "🕹️", "icon": "🕹️",

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ import prisma from '@/lib/prisma'
import { canReadTypebots } from '@/helpers/databaseRules' import { canReadTypebots } from '@/helpers/databaseRules'
import { authenticatedProcedure } from '@/helpers/server/trpc' import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { Typebot, Webhook } from '@typebot.io/schemas' import { Typebot } from '@typebot.io/schemas'
import { z } from 'zod' import { z } from 'zod'
import { fetchLinkedTypebots } from '@/features/blocks/logic/typebotLink/helpers/fetchLinkedTypebots' import { fetchLinkedTypebots } from '@/features/blocks/logic/typebotLink/helpers/fetchLinkedTypebots'
import { parseResultExample } from '../helpers/parseResultExample' import { parseResultExample } from '../helpers/parseResultExample'
@@ -45,20 +45,15 @@ export const getResultExample = authenticatedProcedure
groups: true, groups: true,
edges: true, edges: true,
variables: true, variables: true,
webhooks: true,
}, },
})) as })) as Pick<Typebot, 'groups' | 'edges' | 'variables'> | null
| (Pick<Typebot, 'groups' | 'edges' | 'variables'> & {
webhooks: Webhook[]
})
| null
if (!typebot) if (!typebot)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' }) throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
const block = typebot.groups const block = typebot.groups
.flatMap((g) => g.blocks) .flatMap((group) => group.blocks)
.find((s) => s.id === blockId) .find((block) => block.id === blockId)
if (!block) if (!block)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Block not found' }) 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 { canReadTypebots } from '@/helpers/databaseRules'
import { authenticatedProcedure } from '@/helpers/server/trpc' import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server' 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 { byId, isWebhookBlock, parseGroupTitle } from '@typebot.io/lib'
import { z } from 'zod' import { z } from 'zod'
import { Webhook } from '@typebot.io/prisma'
export const listWebhookBlocks = authenticatedProcedure export const listWebhookBlocks = authenticatedProcedure
.meta({ .meta({
@@ -28,6 +29,12 @@ export const listWebhookBlocks = authenticatedProcedure
webhookBlocks: z.array( webhookBlocks: z.array(
z.object({ z.object({
id: z.string(), id: z.string(),
type: z.enum([
IntegrationBlockType.WEBHOOK,
IntegrationBlockType.ZAPIER,
IntegrationBlockType.MAKE_COM,
IntegrationBlockType.PABBLY_CONNECT,
]),
label: z.string(), label: z.string(),
url: z.string().optional(), url: z.string().optional(),
}) })
@@ -46,17 +53,27 @@ export const listWebhookBlocks = authenticatedProcedure
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' }) throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
const webhookBlocks = (typebot?.groups as Group[]).reduce< 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) => { >((webhookBlocks, group) => {
const blocks = group.blocks.filter((block) => const blocks = group.blocks.filter(isWebhookBlock)
isWebhookBlock(block)
) as WebhookBlock[]
return [ return [
...webhookBlocks, ...webhookBlocks,
...blocks.map((b) => ({ ...blocks.map((block) => ({
id: b.id, id: block.id,
label: `${parseGroupTitle(group.title)} > ${b.id}`, type: block.type,
url: typebot?.webhooks.find(byId(b.webhookId))?.url ?? undefined, 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 { canWriteTypebots } from '@/helpers/databaseRules'
import { authenticatedProcedure } from '@/helpers/server/trpc' import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server' 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 { byId, isWebhookBlock } from '@typebot.io/lib'
import { z } from 'zod' import { z } from 'zod'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums'
export const subscribeWebhook = authenticatedProcedure export const subscribeWebhook = authenticatedProcedure
.meta({ .meta({
@@ -34,9 +35,8 @@ export const subscribeWebhook = authenticatedProcedure
where: canWriteTypebots(typebotId, user), where: canWriteTypebots(typebotId, user),
select: { select: {
groups: true, groups: true,
webhooks: true,
}, },
})) as (Pick<Typebot, 'groups'> & { webhooks: Webhook[] }) | null })) as Pick<Typebot, 'groups'> | null
if (!typebot) if (!typebot)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' }) throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
@@ -51,18 +51,50 @@ export const subscribeWebhook = authenticatedProcedure
message: 'Webhook block not found', message: 'Webhook block not found',
}) })
await prisma.webhook.upsert({ const newWebhook = {
where: { id: webhookBlock.webhookId }, id: webhookBlock.webhookId ?? webhookBlock.id,
update: { url, body: '{{state}}', method: 'POST' },
create: {
url, url,
body: '{{state}}', body: '{{state}}',
method: 'POST', method: HttpMethod.POST,
typebotId,
headers: [], headers: [],
queryParams: [], 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 { return {
id: blockId, id: blockId,

View File

@@ -50,10 +50,45 @@ export const unsubscribeWebhook = authenticatedProcedure
message: 'Webhook block not found', message: 'Webhook block not found',
}) })
if (webhookBlock.webhookId)
await prisma.webhook.update({ await prisma.webhook.update({
where: { id: webhookBlock.webhookId }, where: { id: webhookBlock.webhookId },
data: { url: null }, 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 { return {
id: blockId, id: blockId,

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import {
createWebhook, createWebhook,
importTypebotInDatabase, importTypebotInDatabase,
} from '@typebot.io/lib/playwright/databaseActions' } 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 { createId } from '@paralleldrive/cuid2'
import { getTestAsset } from '@/test/utils/playwright' import { getTestAsset } from '@/test/utils/playwright'
import { apiToken } from '@typebot.io/lib/playwright/databaseSetup' import { apiToken } from '@typebot.io/lib/playwright/databaseSetup'
@@ -26,7 +26,8 @@ test.describe('Builder', () => {
) )
await page.click('text=Test the request') await page.click('text=Test the request')
await expect(page.locator('div[role="textbox"] >> nth=-1')).toContainText( 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({ expect(webhookBlocks[0]).toEqual({
id: 'webhookBlock', id: 'webhookBlock',
label: 'Webhook > webhookBlock', label: 'Webhook > webhookBlock',
type: 'Webhook',
}) })
}) })

View File

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

View File

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

View File

@@ -23,7 +23,7 @@ test.describe('Condition block', () => {
await page.click('button:has-text("Age")') await page.click('button:has-text("Age")')
await page.click('button:has-text("Select an operator")') await page.click('button:has-text("Select an operator")')
await page.click('button:has-text("Greater than")', { force: true }) await page.click('button:has-text("Greater than")', { force: true })
await page.fill('input[placeholder="Type a value..."]', '80') await page.fill('input[placeholder="Type a number..."]', '80')
await page.click('button:has-text("Add a comparison")') await page.click('button:has-text("Add a comparison")')
@@ -35,7 +35,7 @@ test.describe('Condition block', () => {
await page.click('button:has-text("Select an operator")') await page.click('button:has-text("Select an operator")')
await page.click('button:has-text("Less than")', { force: true }) await page.click('button:has-text("Less than")', { force: true })
await page.fill( await page.fill(
':nth-match(input[placeholder="Type a value..."], 2)', ':nth-match(input[placeholder="Type a number..."], 2)',
'100' '100'
) )
@@ -47,7 +47,7 @@ test.describe('Condition block', () => {
await page.click('button:has-text("Age")') await page.click('button:has-text("Age")')
await page.click('button:has-text("Select an operator")') await page.click('button:has-text("Select an operator")')
await page.click('button:has-text("Greater than")', { force: true }) await page.click('button:has-text("Greater than")', { force: true })
await page.fill('input[placeholder="Type a value..."]', '20') await page.fill('input[placeholder="Type a number..."]', '20')
await page.click('text=Preview') await page.click('text=Preview')
await page await page

View File

@@ -49,7 +49,7 @@ export const parseNewTypebot = ({
return { return {
folderId, folderId,
name, name,
version: '3', version: '4',
workspaceId, workspaceId,
groups: [startGroup], groups: [startGroup],
edges: [], edges: [],

View File

@@ -29,13 +29,19 @@ export const importTypebotQuery = async (typebot: Typebot, userPlan: Plan) => {
const webhookBlocks = typebot.groups const webhookBlocks = typebot.groups
.flatMap((b) => b.blocks) .flatMap((b) => b.blocks)
.filter(isWebhookBlock) .filter(isWebhookBlock)
.filter((block) => block.webhookId)
await Promise.all( await Promise.all(
webhookBlocks.map((s) => webhookBlocks.map((webhookBlock) =>
duplicateWebhookQuery({ duplicateWebhookQuery({
existingIds: { typebotId: typebot.id, webhookId: s.webhookId }, existingIds: {
typebotId: typebot.id,
webhookId: webhookBlock.webhookId as string,
},
newIds: { newIds: {
typebotId: newTypebot.id, typebotId: newTypebot.id,
webhookId: webhookIdsMapping.get(s.webhookId) as string, webhookId: webhookIdsMapping.get(
webhookBlock.webhookId as string
) as string,
}, },
}) })
) )
@@ -51,84 +57,98 @@ const duplicateTypebot = (
webhookIdsMapping: Map<string, string> webhookIdsMapping: Map<string, string>
} => { } => {
const groupIdsMapping = generateOldNewIdsMapping(typebot.groups) const groupIdsMapping = generateOldNewIdsMapping(typebot.groups)
const blockIdsMapping = generateOldNewIdsMapping(
typebot.groups.flatMap((group) => group.blocks)
)
const edgeIdsMapping = generateOldNewIdsMapping(typebot.edges) const edgeIdsMapping = generateOldNewIdsMapping(typebot.edges)
const webhookIdsMapping = generateOldNewIdsMapping( const webhookIdsMapping = generateOldNewIdsMapping(
typebot.groups typebot.groups
.flatMap((b) => b.blocks) .flatMap((group) => group.blocks)
.filter(isWebhookBlock) .filter(isWebhookBlock)
.map((s) => ({ id: s.webhookId })) .map((block) => ({
id: block.webhookId as string,
}))
) )
const id = createId() const id = createId()
return { return {
typebot: { typebot: {
...typebot, ...typebot,
version: '3',
id, id,
name: `${typebot.name} copy`, name: `${typebot.name} copy`,
publicId: null, publicId: null,
customDomain: null, customDomain: null,
groups: typebot.groups.map((b) => ({ groups: typebot.groups.map((group) => ({
...b, ...group,
id: groupIdsMapping.get(b.id) as string, id: groupIdsMapping.get(group.id) as string,
blocks: b.blocks.map((s) => { blocks: group.blocks.map((block) => {
const newIds = { const newIds = {
groupId: groupIdsMapping.get(s.groupId) as string, id: blockIdsMapping.get(block.id) as string,
outgoingEdgeId: s.outgoingEdgeId groupId: groupIdsMapping.get(block.groupId) as string,
? edgeIdsMapping.get(s.outgoingEdgeId) outgoingEdgeId: block.outgoingEdgeId
? edgeIdsMapping.get(block.outgoingEdgeId)
: undefined, : undefined,
} }
if ( if (
s.type === LogicBlockType.TYPEBOT_LINK && block.type === LogicBlockType.TYPEBOT_LINK &&
s.options.typebotId === 'current' && block.options.typebotId === 'current' &&
isDefined(s.options.groupId) isDefined(block.options.groupId)
) )
return { return {
...s, ...block,
...newIds,
options: { options: {
...s.options, ...block.options,
groupId: groupIdsMapping.get(s.options.groupId as string), groupId: groupIdsMapping.get(block.options.groupId as string),
}, },
} }
if (s.type === LogicBlockType.JUMP) if (block.type === LogicBlockType.JUMP)
return { return {
...s, ...block,
...newIds,
options: { options: {
...s.options, ...block.options,
groupId: groupIdsMapping.get(s.options.groupId as string), groupId: groupIdsMapping.get(block.options.groupId as string),
} satisfies JumpBlock['options'], } satisfies JumpBlock['options'],
} }
if (blockHasItems(s)) if (blockHasItems(block))
return { return {
...s, ...block,
items: s.items.map((item) => ({ ...newIds,
items: block.items.map((item) => ({
...item, ...item,
outgoingEdgeId: item.outgoingEdgeId outgoingEdgeId: item.outgoingEdgeId
? (edgeIdsMapping.get(item.outgoingEdgeId) as string) ? (edgeIdsMapping.get(item.outgoingEdgeId) as string)
: undefined, : undefined,
})), })),
...newIds,
} as ChoiceInputBlock | ConditionBlock } as ChoiceInputBlock | ConditionBlock
if (isWebhookBlock(s)) { if (isWebhookBlock(block) && block.webhookId) {
return { return {
...s, ...block,
webhookId: webhookIdsMapping.get(s.webhookId) as string,
...newIds, ...newIds,
webhookId: webhookIdsMapping.get(block.webhookId) as string,
} }
} }
return { return {
...s, ...block,
...newIds, ...newIds,
} }
}), }),
})), })),
edges: typebot.edges.map((e) => ({ edges: typebot.edges.map((edge) => ({
...e, ...edge,
id: edgeIdsMapping.get(e.id) as string, id: edgeIdsMapping.get(edge.id) as string,
from: { from: {
...e.from, ...edge.from,
groupId: groupIdsMapping.get(e.from.groupId) as string, blockId: blockIdsMapping.get(edge.from.blockId) as string,
groupId: groupIdsMapping.get(edge.from.groupId) as string,
},
to: {
...edge.to,
blockId: edge.to.blockId
? (blockIdsMapping.get(edge.to.blockId) as string)
: undefined,
groupId: groupIdsMapping.get(edge.to.groupId) as string,
}, },
to: { ...e.to, groupId: groupIdsMapping.get(e.to.groupId) as string },
})), })),
settings: settings:
typebot.settings.general.isBrandingEnabled === false && typebot.settings.general.isBrandingEnabled === false &&

View File

@@ -189,6 +189,7 @@ export const duplicateBlockDraft =
} as Block } as Block
if (isWebhookBlock(block)) { if (isWebhookBlock(block)) {
const newWebhookId = createId() const newWebhookId = createId()
if (block.webhookId)
onWebhookBlockDuplicated(block.webhookId, newWebhookId) onWebhookBlockDuplicated(block.webhookId, newWebhookId)
return { return {
...block, ...block,

View File

@@ -2,7 +2,6 @@ import { createId } from '@paralleldrive/cuid2'
import { import {
isBubbleBlockType, isBubbleBlockType,
blockTypeHasOption, blockTypeHasOption,
blockTypeHasWebhook,
blockTypeHasItems, blockTypeHasItems,
} from '@typebot.io/lib' } from '@typebot.io/lib'
import { import {
@@ -134,7 +133,7 @@ const parseDefaultBlockOptions = (type: BlockWithOptionsType): BlockOptions => {
case IntegrationBlockType.PABBLY_CONNECT: case IntegrationBlockType.PABBLY_CONNECT:
case IntegrationBlockType.MAKE_COM: case IntegrationBlockType.MAKE_COM:
case IntegrationBlockType.WEBHOOK: case IntegrationBlockType.WEBHOOK:
return defaultWebhookOptions return defaultWebhookOptions(createId())
case IntegrationBlockType.EMAIL: case IntegrationBlockType.EMAIL:
return defaultSendEmailOptions return defaultSendEmailOptions
case IntegrationBlockType.CHATWOOT: case IntegrationBlockType.CHATWOOT:
@@ -159,7 +158,6 @@ export const parseNewBlock = (
options: blockTypeHasOption(type) options: blockTypeHasOption(type)
? parseDefaultBlockOptions(type) ? parseDefaultBlockOptions(type)
: undefined, : undefined,
webhookId: blockTypeHasWebhook(type) ? createId() : undefined,
items: blockTypeHasItems(type) ? parseDefaultItems(type, id) : undefined, items: blockTypeHasItems(type) ? parseDefaultItems(type, id) : undefined,
} as DraggableBlock } as DraggableBlock
} }

View File

@@ -10,6 +10,7 @@ import { isReadTypebotForbidden } from '@/features/typebot/helpers/isReadTypebot
import { removeTypebotOldProperties } from '@/features/typebot/helpers/removeTypebotOldProperties' import { removeTypebotOldProperties } from '@/features/typebot/helpers/removeTypebotOldProperties'
import { roundGroupsCoordinate } from '@/features/typebot/helpers/roundGroupsCoordinate' import { roundGroupsCoordinate } from '@/features/typebot/helpers/roundGroupsCoordinate'
import { archiveResults } from '@typebot.io/lib/api/helpers/archiveResults' import { archiveResults } from '@typebot.io/lib/api/helpers/archiveResults'
import { migrateTypebotFromV3ToV4 } from '@typebot.io/lib/migrations/migrateTypebotFromV3ToV4'
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req, res) const user = await getAuthenticatedUser(req, res)
@@ -37,9 +38,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
collaborators.find((c) => c.userId === user.id)?.type === collaborators.find((c) => c.userId === user.id)?.type ===
CollaborationType.READ CollaborationType.READ
return res.send({ return res.send({
typebot: roundGroupsCoordinate( typebot: await migrateTypebot(typebot as Typebot),
removeTypebotOldProperties(typebot) as Typebot
),
publishedTypebot, publishedTypebot,
isReadOnly, isReadOnly,
webhooks, webhooks,
@@ -95,7 +94,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const updates = { const updates = {
...omit(data, 'id', 'createdAt', 'updatedAt'), ...omit(data, 'id', 'createdAt', 'updatedAt'),
version: '3',
theme: data.theme ?? undefined, theme: data.theme ?? undefined,
settings: data.settings ?? undefined, settings: data.settings ?? undefined,
resultsTablePreferences: data.resultsTablePreferences ?? undefined, resultsTablePreferences: data.resultsTablePreferences ?? undefined,
@@ -142,4 +140,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
return methodNotAllowed(res) return methodNotAllowed(res)
} }
const migrateTypebot = async (typebot: Typebot): Promise<Typebot> => {
if (typebot.version === '4') return typebot
const updatedTypebot = roundGroupsCoordinate(
removeTypebotOldProperties(typebot) as Typebot
)
return migrateTypebotFromV3ToV4(prisma)(updatedTypebot)
}
export default handler export default handler

View File

@@ -2797,6 +2797,83 @@
}, },
"isExecutedOnClient": { "isExecutedOnClient": {
"type": "boolean" "type": "boolean"
},
"webhook": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"queryParams": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"key": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"key": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"method": {
"type": "string",
"enum": [
"POST",
"GET",
"PUT",
"DELETE",
"PATCH",
"HEAD",
"CONNECT",
"OPTIONS",
"TRACE"
]
},
"url": {
"type": "string"
},
"body": {
"type": "string"
}
},
"required": [
"id",
"queryParams",
"headers",
"method"
],
"additionalProperties": false
} }
}, },
"required": [ "required": [
@@ -2806,15 +2883,15 @@
"additionalProperties": false "additionalProperties": false
}, },
"webhookId": { "webhookId": {
"type": "string" "type": "string",
"description": "Deprecated, use webhook.id instead"
} }
}, },
"required": [ "required": [
"id", "id",
"groupId", "groupId",
"type", "type",
"options", "options"
"webhookId"
], ],
"additionalProperties": false "additionalProperties": false
}, },
@@ -3135,6 +3212,83 @@
}, },
"isExecutedOnClient": { "isExecutedOnClient": {
"type": "boolean" "type": "boolean"
},
"webhook": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"queryParams": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"key": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"key": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"method": {
"type": "string",
"enum": [
"POST",
"GET",
"PUT",
"DELETE",
"PATCH",
"HEAD",
"CONNECT",
"OPTIONS",
"TRACE"
]
},
"url": {
"type": "string"
},
"body": {
"type": "string"
}
},
"required": [
"id",
"queryParams",
"headers",
"method"
],
"additionalProperties": false
} }
}, },
"required": [ "required": [
@@ -3144,15 +3298,15 @@
"additionalProperties": false "additionalProperties": false
}, },
"webhookId": { "webhookId": {
"type": "string" "type": "string",
"description": "Deprecated, use webhook.id instead"
} }
}, },
"required": [ "required": [
"id", "id",
"groupId", "groupId",
"type", "type",
"options", "options"
"webhookId"
], ],
"additionalProperties": false "additionalProperties": false
}, },
@@ -3303,6 +3457,83 @@
}, },
"isExecutedOnClient": { "isExecutedOnClient": {
"type": "boolean" "type": "boolean"
},
"webhook": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"queryParams": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"key": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"key": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"method": {
"type": "string",
"enum": [
"POST",
"GET",
"PUT",
"DELETE",
"PATCH",
"HEAD",
"CONNECT",
"OPTIONS",
"TRACE"
]
},
"url": {
"type": "string"
},
"body": {
"type": "string"
}
},
"required": [
"id",
"queryParams",
"headers",
"method"
],
"additionalProperties": false
} }
}, },
"required": [ "required": [
@@ -3312,15 +3543,15 @@
"additionalProperties": false "additionalProperties": false
}, },
"webhookId": { "webhookId": {
"type": "string" "type": "string",
"description": "Deprecated, now integrated in webhook block options"
} }
}, },
"required": [ "required": [
"id", "id",
"groupId", "groupId",
"type", "type",
"options", "options"
"webhookId"
], ],
"additionalProperties": false "additionalProperties": false
}, },
@@ -3395,6 +3626,83 @@
}, },
"isExecutedOnClient": { "isExecutedOnClient": {
"type": "boolean" "type": "boolean"
},
"webhook": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"queryParams": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"key": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"key": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"method": {
"type": "string",
"enum": [
"POST",
"GET",
"PUT",
"DELETE",
"PATCH",
"HEAD",
"CONNECT",
"OPTIONS",
"TRACE"
]
},
"url": {
"type": "string"
},
"body": {
"type": "string"
}
},
"required": [
"id",
"queryParams",
"headers",
"method"
],
"additionalProperties": false
} }
}, },
"required": [ "required": [
@@ -3404,15 +3712,15 @@
"additionalProperties": false "additionalProperties": false
}, },
"webhookId": { "webhookId": {
"type": "string" "type": "string",
"description": "Deprecated, use webhook.id instead"
} }
}, },
"required": [ "required": [
"id", "id",
"groupId", "groupId",
"type", "type",
"options", "options"
"webhookId"
], ],
"additionalProperties": false "additionalProperties": false
}, },
@@ -4528,6 +4836,15 @@
"id": { "id": {
"type": "string" "type": "string"
}, },
"type": {
"type": "string",
"enum": [
"Webhook",
"Zapier",
"Make.com",
"Pabbly"
]
},
"label": { "label": {
"type": "string" "type": "string"
}, },
@@ -4537,6 +4854,7 @@
}, },
"required": [ "required": [
"id", "id",
"type",
"label" "label"
], ],
"additionalProperties": false "additionalProperties": false

View File

@@ -2365,6 +2365,83 @@
}, },
"isExecutedOnClient": { "isExecutedOnClient": {
"type": "boolean" "type": "boolean"
},
"webhook": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"queryParams": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"key": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"key": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"method": {
"type": "string",
"enum": [
"POST",
"GET",
"PUT",
"DELETE",
"PATCH",
"HEAD",
"CONNECT",
"OPTIONS",
"TRACE"
]
},
"url": {
"type": "string"
},
"body": {
"type": "string"
}
},
"required": [
"id",
"queryParams",
"headers",
"method"
],
"additionalProperties": false
} }
}, },
"required": [ "required": [
@@ -2374,15 +2451,15 @@
"additionalProperties": false "additionalProperties": false
}, },
"webhookId": { "webhookId": {
"type": "string" "type": "string",
"description": "Deprecated, use webhook.id instead"
} }
}, },
"required": [ "required": [
"id", "id",
"groupId", "groupId",
"type", "type",
"options", "options"
"webhookId"
], ],
"additionalProperties": false "additionalProperties": false
}, },
@@ -2703,6 +2780,83 @@
}, },
"isExecutedOnClient": { "isExecutedOnClient": {
"type": "boolean" "type": "boolean"
},
"webhook": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"queryParams": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"key": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"key": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"method": {
"type": "string",
"enum": [
"POST",
"GET",
"PUT",
"DELETE",
"PATCH",
"HEAD",
"CONNECT",
"OPTIONS",
"TRACE"
]
},
"url": {
"type": "string"
},
"body": {
"type": "string"
}
},
"required": [
"id",
"queryParams",
"headers",
"method"
],
"additionalProperties": false
} }
}, },
"required": [ "required": [
@@ -2712,15 +2866,15 @@
"additionalProperties": false "additionalProperties": false
}, },
"webhookId": { "webhookId": {
"type": "string" "type": "string",
"description": "Deprecated, use webhook.id instead"
} }
}, },
"required": [ "required": [
"id", "id",
"groupId", "groupId",
"type", "type",
"options", "options"
"webhookId"
], ],
"additionalProperties": false "additionalProperties": false
}, },
@@ -2871,6 +3025,83 @@
}, },
"isExecutedOnClient": { "isExecutedOnClient": {
"type": "boolean" "type": "boolean"
},
"webhook": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"queryParams": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"key": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"key": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"method": {
"type": "string",
"enum": [
"POST",
"GET",
"PUT",
"DELETE",
"PATCH",
"HEAD",
"CONNECT",
"OPTIONS",
"TRACE"
]
},
"url": {
"type": "string"
},
"body": {
"type": "string"
}
},
"required": [
"id",
"queryParams",
"headers",
"method"
],
"additionalProperties": false
} }
}, },
"required": [ "required": [
@@ -2880,15 +3111,15 @@
"additionalProperties": false "additionalProperties": false
}, },
"webhookId": { "webhookId": {
"type": "string" "type": "string",
"description": "Deprecated, now integrated in webhook block options"
} }
}, },
"required": [ "required": [
"id", "id",
"groupId", "groupId",
"type", "type",
"options", "options"
"webhookId"
], ],
"additionalProperties": false "additionalProperties": false
}, },
@@ -2963,6 +3194,83 @@
}, },
"isExecutedOnClient": { "isExecutedOnClient": {
"type": "boolean" "type": "boolean"
},
"webhook": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"queryParams": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"key": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"key": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"method": {
"type": "string",
"enum": [
"POST",
"GET",
"PUT",
"DELETE",
"PATCH",
"HEAD",
"CONNECT",
"OPTIONS",
"TRACE"
]
},
"url": {
"type": "string"
},
"body": {
"type": "string"
}
},
"required": [
"id",
"queryParams",
"headers",
"method"
],
"additionalProperties": false
} }
}, },
"required": [ "required": [
@@ -2972,15 +3280,15 @@
"additionalProperties": false "additionalProperties": false
}, },
"webhookId": { "webhookId": {
"type": "string" "type": "string",
"description": "Deprecated, use webhook.id instead"
} }
}, },
"required": [ "required": [
"id", "id",
"groupId", "groupId",
"type", "type",
"options", "options"
"webhookId"
], ],
"additionalProperties": false "additionalProperties": false
}, },
@@ -3524,7 +3832,7 @@
}, },
"isPreview": { "isPreview": {
"type": "boolean", "type": "boolean",
"description": "If set to `true`, it will start a Preview session with the unpublished bot and it won't be saved in the Results tab. You need to be authenticated for this to work." "description": "If set to `true`, it will start a Preview session with the unpublished bot and it won't be saved in the Results tab. You need to be authenticated with a bearer token for this to work."
}, },
"resultId": { "resultId": {
"type": "string", "type": "string",
@@ -5808,7 +6116,7 @@
} }
} }
}, },
"/session/{sessionId}/updateTypebot": { "/sessions/{sessionId}/updateTypebot": {
"post": { "post": {
"operationId": "updateTypebotInSession", "operationId": "updateTypebotInSession",
"summary": "Update typebot in session", "summary": "Update typebot in session",

View File

@@ -76,11 +76,40 @@ const nextConfig = {
})) }))
) )
: [] : []
).concat({ ).concat([
{
source: '/api/typebots/:typebotId/blocks/:blockId/storage/upload-url', source: '/api/typebots/:typebotId/blocks/:blockId/storage/upload-url',
destination: destination:
'/api/v1/typebots/:typebotId/blocks/:blockId/storage/upload-url', '/api/v1/typebots/:typebotId/blocks/:blockId/storage/upload-url',
}), },
{
source:
'/api/typebots/:typebotId/blocks/:blockId/steps/:stepId/sampleResult',
destination: `${process.env.NEXTAUTH_URL}/api/v1/typebots/:typebotId/webhookBlocks/:blockId/getResultExample`,
},
{
source: '/api/typebots/:typebotId/blocks/:blockId/sampleResult',
destination: `${process.env.NEXTAUTH_URL}/api/v1/typebots/:typebotId/webhookBlocks/:blockId/getResultExample`,
},
{
source:
'/api/typebots/:typebotId/blocks/:blockId/steps/:stepId/unsubscribeWebhook',
destination: `${process.env.NEXTAUTH_URL}/api/v1/typebots/:typebotId/webhookBlocks/:blockId/unsubscribe`,
},
{
source: '/api/typebots/:typebotId/blocks/:blockId/unsubscribeWebhook',
destination: `${process.env.NEXTAUTH_URL}/api/v1/typebots/:typebotId/webhookBlocks/:blockId/unsubscribe`,
},
{
source:
'/api/typebots/:typebotId/blocks/:blockId/steps/:stepId/subscribeWebhook',
destination: `${process.env.NEXTAUTH_URL}/api/v1/typebots/:typebotId/webhookBlocks/:blockId/subscribe`,
},
{
source: '/api/typebots/:typebotId/blocks/:blockId/subscribeWebhook',
destination: `${process.env.NEXTAUTH_URL}/api/v1/typebots/:typebotId/webhookBlocks/:blockId/subscribe`,
},
]),
} }
}, },
} }

View File

@@ -11,7 +11,6 @@ import {
WebhookResponse, WebhookResponse,
WebhookOptions, WebhookOptions,
defaultWebhookAttributes, defaultWebhookAttributes,
HttpMethod,
PublicTypebot, PublicTypebot,
KeyValue, KeyValue,
ReplyLog, ReplyLog,
@@ -26,6 +25,7 @@ import { parseSampleResult } from './parseSampleResult'
import { ExecuteIntegrationResponse } from '@/features/chat/types' import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { parseVariables } from '@/features/variables/parseVariables' import { parseVariables } from '@/features/variables/parseVariables'
import { resumeWebhookExecution } from './resumeWebhookExecution' import { resumeWebhookExecution } from './resumeWebhookExecution'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums'
type ParsedWebhook = ExecutableWebhook & { type ParsedWebhook = ExecutableWebhook & {
basicAuth: { username?: string; password?: string } basicAuth: { username?: string; password?: string }
@@ -38,9 +38,11 @@ export const executeWebhookBlock = async (
): Promise<ExecuteIntegrationResponse> => { ): Promise<ExecuteIntegrationResponse> => {
const { typebot, result } = state const { typebot, result } = state
const logs: ReplyLog[] = [] const logs: ReplyLog[] = []
const webhook = (await prisma.webhook.findUnique({ const webhook =
block.options.webhook ??
((await prisma.webhook.findUnique({
where: { id: block.webhookId }, where: { id: block.webhookId },
})) as Webhook | null })) as Webhook | null)
if (!webhook) { if (!webhook) {
logs.push({ logs.push({
status: 'error', status: 'error',

View File

@@ -1,6 +1,6 @@
import test, { expect } from '@playwright/test' import test, { expect } from '@playwright/test'
import { createId } from '@paralleldrive/cuid2' import { createId } from '@paralleldrive/cuid2'
import { HttpMethod } from '@typebot.io/schemas' import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums'
import { import {
createWebhook, createWebhook,
importTypebotInDatabase, importTypebotInDatabase,

View File

@@ -2,13 +2,14 @@ import { getTestAsset } from '@/test/utils/playwright'
import test, { expect } from '@playwright/test' import test, { expect } from '@playwright/test'
import { createId } from '@paralleldrive/cuid2' import { createId } from '@paralleldrive/cuid2'
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { HttpMethod, SendMessageInput } from '@typebot.io/schemas' import { SendMessageInput } from '@typebot.io/schemas'
import { import {
createWebhook, createWebhook,
deleteTypebots, deleteTypebots,
deleteWebhooks, deleteWebhooks,
importTypebotInDatabase, importTypebotInDatabase,
} from '@typebot.io/lib/playwright/databaseActions' } from '@typebot.io/lib/playwright/databaseActions'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums'
test.afterEach(async () => { test.afterEach(async () => {
await deleteWebhooks(['chat-webhook-id']) await deleteWebhooks(['chat-webhook-id'])

View File

@@ -162,7 +162,7 @@ const App = ({
return <NotFoundPage /> return <NotFoundPage />
if (publishedTypebot.typebot.isClosed) if (publishedTypebot.typebot.isClosed)
return <ErrorPage error={new Error('This bot is now closed')} /> return <ErrorPage error={new Error('This bot is now closed')} />
return publishedTypebot.version === '3' ? ( return publishedTypebot.version ? (
<TypebotPageV3 <TypebotPageV3
url={props.url} url={props.url}
typebot={{ typebot={{

View File

@@ -9,7 +9,6 @@ import {
WebhookOptions, WebhookOptions,
WebhookResponse, WebhookResponse,
WebhookBlock, WebhookBlock,
HttpMethod,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import got, { Method, Headers, HTTPError } from 'got' import got, { Method, Headers, HTTPError } from 'got'
@@ -25,6 +24,7 @@ import { fetchLinkedTypebots } from '@/features/blocks/logic/typebotLink/fetchLi
import { getPreviouslyLinkedTypebots } from '@/features/blocks/logic/typebotLink/getPreviouslyLinkedTypebots' import { getPreviouslyLinkedTypebots } from '@/features/blocks/logic/typebotLink/getPreviouslyLinkedTypebots'
import { saveErrorLog } from '@/features/logs/saveErrorLog' import { saveErrorLog } from '@/features/logs/saveErrorLog'
import { saveSuccessLog } from '@/features/logs/saveSuccessLog' import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums'
const cors = initMiddleware(Cors()) const cors = initMiddleware(Cors())
@@ -49,7 +49,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const block = typebot.groups const block = typebot.groups
.flatMap((g) => g.blocks) .flatMap((g) => g.blocks)
.find(byId(blockId)) as WebhookBlock .find(byId(blockId)) as WebhookBlock
const webhook = typebot.webhooks.find(byId(block.webhookId)) const webhook =
block.options.webhook ?? typebot.webhooks.find(byId(block.webhookId))
if (!webhook) if (!webhook)
return res return res
.status(404) .status(404)

View File

@@ -1,38 +0,0 @@
import { authenticateUser } from '@/helpers/authenticateUser'
import prisma from '@/lib/prisma'
import { Typebot } from '@typebot.io/schemas'
import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed } from '@typebot.io/lib/api'
import { getPreviouslyLinkedTypebots } from '@/features/blocks/logic/typebotLink/getPreviouslyLinkedTypebots'
import { parseSampleResult } from '@/features/blocks/integrations/webhook/parseSampleResult'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await authenticateUser(req)
if (!user) return res.status(401).json({ message: 'Not authenticated' })
if (req.method === 'GET') {
const typebotId = req.query.typebotId as string
const blockId = req.query.blockId as string
const typebot = (await prisma.typebot.findFirst({
where: {
id: typebotId,
workspace: { members: { some: { userId: user.id } } },
},
})) as unknown as Typebot | undefined
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
const block = typebot.groups
.flatMap((g) => g.blocks)
.find((s) => s.id === blockId)
if (!block) return res.status(404).send({ message: 'Group not found' })
const linkedTypebots = await getPreviouslyLinkedTypebots({
isPreview: true,
typebots: [typebot],
user,
})([])
return res.send(
await parseSampleResult(typebot, linkedTypebots)(block.groupId, [])
)
}
methodNotAllowed(res)
}
export default handler

View File

@@ -1,72 +0,0 @@
import prisma from '@/lib/prisma'
import {
defaultWebhookAttributes,
ResultValues,
Typebot,
Variable,
Webhook,
WebhookOptions,
WebhookBlock,
} from '@typebot.io/schemas'
import { NextApiRequest, NextApiResponse } from 'next'
import { initMiddleware, methodNotAllowed, notFound } from '@typebot.io/lib/api'
import { byId } from '@typebot.io/lib'
import Cors from 'cors'
import { executeWebhook } from '../../executeWebhook'
const cors = initMiddleware(Cors())
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await cors(req, res)
if (req.method === 'POST') {
const typebotId = req.query.typebotId as string
const groupId = req.query.groupId as string
const blockId = req.query.blockId as string
const resultId = req.query.resultId as string | undefined
const { resultValues, variables, parentTypebotIds } = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as {
resultValues: ResultValues
variables: Variable[]
parentTypebotIds: string[]
}
const typebot = (await prisma.typebot.findUnique({
where: { id: typebotId },
include: { webhooks: true },
})) as unknown as (Typebot & { webhooks: Webhook[] }) | null
if (!typebot) return notFound(res)
const block = typebot.groups
.find(byId(groupId))
?.blocks.find(byId(blockId)) as WebhookBlock
const webhook = typebot.webhooks.find(byId(block.webhookId))
if (!webhook)
return res
.status(404)
.send({ statusCode: 404, data: { message: `Couldn't find webhook` } })
const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
const result = await executeWebhook(typebot)({
webhook: preparedWebhook,
variables,
groupId,
resultValues,
resultId,
parentTypebotIds,
})
return res.status(200).send(result)
}
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
}
export default handler

View File

@@ -1,34 +0,0 @@
import prisma from '@/lib/prisma'
import { Typebot } from '@typebot.io/schemas'
import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed } from '@typebot.io/lib/api'
import { parseSampleResult } from '@/features/blocks/integrations/webhook/parseSampleResult'
import { getPreviouslyLinkedTypebots } from '@/features/blocks/logic/typebotLink/getPreviouslyLinkedTypebots'
import { authenticateUser } from '@/helpers/authenticateUser'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await authenticateUser(req)
if (!user) return res.status(401).json({ message: 'Not authenticated' })
if (req.method === 'GET') {
const typebotId = req.query.typebotId as string
const groupId = req.query.groupId as string
const typebot = (await prisma.typebot.findFirst({
where: {
id: typebotId,
workspace: { members: { some: { userId: user.id } } },
},
})) as unknown as Typebot | undefined
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
const linkedTypebots = await getPreviouslyLinkedTypebots({
isPreview: true,
typebots: [typebot],
user,
})([])
return res.send(
await parseSampleResult(typebot, linkedTypebots)(groupId, [])
)
}
methodNotAllowed(res)
}
export default handler

View File

@@ -1,53 +0,0 @@
import { Typebot, WebhookBlock } from '@typebot.io/schemas'
import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed } from '@typebot.io/lib/api'
import { byId } from '@typebot.io/lib'
import prisma from '@/lib/prisma'
import { authenticateUser } from '@/helpers/authenticateUser'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await authenticateUser(req)
if (!user) return res.status(401).json({ message: 'Not authenticated' })
if (req.method === 'POST') {
const body = req.body as Record<string, string>
if (!('url' in body))
return res.status(403).send({ message: 'url is missing in body' })
const { url } = body
const typebotId = req.query.typebotId as string
const groupId = req.query.groupId as string
const blockId = req.query.blockId as string
const typebot = (await prisma.typebot.findFirst({
where: {
id: typebotId,
workspace: { members: { some: { userId: user.id } } },
},
})) as unknown as Typebot | undefined
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
try {
const { webhookId } = typebot.groups
.find(byId(groupId))
?.blocks.find(byId(blockId)) as WebhookBlock
await prisma.webhook.upsert({
where: { id: webhookId },
update: { url, body: '{{state}}', method: 'POST' },
create: {
url,
body: '{{state}}',
method: 'POST',
typebotId,
headers: [],
queryParams: [],
},
})
return res.send({ message: 'success' })
} catch (err) {
return res
.status(400)
.send({ message: "blockId doesn't point to a Webhook block" })
}
}
return methodNotAllowed(res)
}
export default handler

View File

@@ -1,41 +0,0 @@
import { Typebot, WebhookBlock } from '@typebot.io/schemas'
import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed } from '@typebot.io/lib/api'
import { byId } from '@typebot.io/lib'
import { authenticateUser } from '@/helpers/authenticateUser'
import prisma from '@/lib/prisma'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await authenticateUser(req)
if (!user) return res.status(401).json({ message: 'Not authenticated' })
if (req.method === 'POST') {
const typebotId = req.query.typebotId as string
const groupId = req.query.groupId as string
const blockId = req.query.blockId as string
const typebot = (await prisma.typebot.findFirst({
where: {
id: typebotId,
workspace: { members: { some: { userId: user.id } } },
},
})) as unknown as Typebot | undefined
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
try {
const { webhookId } = typebot.groups
.find(byId(groupId))
?.blocks.find(byId(blockId)) as WebhookBlock
await prisma.webhook.updateMany({
where: { id: webhookId },
data: { url: null },
})
return res.send({ message: 'success' })
} catch (err) {
return res
.status(400)
.send({ message: "blockId doesn't point to a Webhook block" })
}
}
return methodNotAllowed(res)
}
export default handler

View File

@@ -1,52 +0,0 @@
import { authenticateUser } from '@/helpers/authenticateUser'
import prisma from '@/lib/prisma'
import { Typebot, WebhookBlock } from '@typebot.io/schemas'
import { NextApiRequest, NextApiResponse } from 'next'
import { byId } from '@typebot.io/lib'
import { methodNotAllowed } from '@typebot.io/lib/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await authenticateUser(req)
if (!user) return res.status(401).json({ message: 'Not authenticated' })
if (req.method === 'POST') {
const body = req.body as Record<string, string>
if (!('url' in body))
return res.status(403).send({ message: 'url is missing in body' })
const { url } = body
const typebotId = req.query.typebotId as string
const blockId = req.query.blockId as string
const typebot = (await prisma.typebot.findFirst({
where: {
id: typebotId,
workspace: { members: { some: { userId: user.id } } },
},
})) as unknown as Typebot | undefined
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
try {
const { webhookId } = typebot.groups
.flatMap((g) => g.blocks)
.find(byId(blockId)) as WebhookBlock
await prisma.webhook.upsert({
where: { id: webhookId },
update: { url, body: '{{state}}', method: 'POST' },
create: {
url,
body: '{{state}}',
method: 'POST',
typebotId,
headers: [],
queryParams: [],
},
})
return res.send({ message: 'success' })
} catch (err) {
return res
.status(400)
.send({ message: "groupId doesn't point to a Webhook block" })
}
}
return methodNotAllowed(res)
}
export default handler

View File

@@ -1,40 +0,0 @@
import { authenticateUser } from '@/helpers/authenticateUser'
import prisma from '@/lib/prisma'
import { Typebot, WebhookBlock } from '@typebot.io/schemas'
import { NextApiRequest, NextApiResponse } from 'next'
import { byId } from '@typebot.io/lib'
import { methodNotAllowed } from '@typebot.io/lib/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await authenticateUser(req)
if (!user) return res.status(401).json({ message: 'Not authenticated' })
if (req.method === 'POST') {
const typebotId = req.query.typebotId as string
const blockId = req.query.blockId as string
const typebot = (await prisma.typebot.findFirst({
where: {
id: typebotId,
workspace: { members: { some: { userId: user.id } } },
},
})) as unknown as Typebot | undefined
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
try {
const { webhookId } = typebot.groups
.flatMap((g) => g.blocks)
.find(byId(blockId)) as WebhookBlock
await prisma.webhook.updateMany({
where: { id: webhookId },
data: { url: null },
})
return res.send({ message: 'success' })
} catch (err) {
return res
.status(400)
.send({ message: "groupId doesn't point to a Webhook block" })
}
}
return methodNotAllowed(res)
}
export default handler

View File

@@ -105,7 +105,6 @@ export const ConversationContainer = (props: Props) => {
}) })
const streamMessage = (content: string) => { const streamMessage = (content: string) => {
console.log('STREAM', content)
setIsSending(false) setIsSending(false)
const lastChunk = [...chatChunks()].pop() const lastChunk = [...chatChunks()].pop()
if (!lastChunk) return if (!lastChunk) return

View File

@@ -0,0 +1,63 @@
import { PrismaClient, Webhook as WebhookFromDb } from '@typebot.io/prisma'
import {
Block,
Typebot,
Webhook,
defaultWebhookAttributes,
} from '@typebot.io/schemas'
import { isWebhookBlock } from '../utils'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums'
export const migrateTypebotFromV3ToV4 =
(prisma: PrismaClient) =>
async (
typebot: Typebot
): Promise<Omit<Typebot, 'version'> & { version: '4' }> => {
if (typebot.version === '4')
return typebot as Omit<Typebot, 'version'> & { version: '4' }
const webhookBlocks = typebot.groups
.flatMap((group) => group.blocks)
.filter(isWebhookBlock)
const webhooks = await prisma.webhook.findMany({
where: {
id: {
in: webhookBlocks.map((block) => block.webhookId as string),
},
},
})
return {
...typebot,
version: '4',
groups: typebot.groups.map((group) => ({
...group,
blocks: group.blocks.map(migrateWebhookBlock(webhooks)),
})),
}
}
const migrateWebhookBlock =
(webhooks: WebhookFromDb[]) =>
(block: Block): Block => {
if (!isWebhookBlock(block)) return block
const webhook = webhooks.find((webhook) => webhook.id === block.webhookId)
return {
...block,
webhookId: undefined,
options: {
...block.options,
webhook: webhook
? {
id: webhook.id,
url: webhook.url ?? undefined,
method: (webhook.method as Webhook['method']) ?? HttpMethod.POST,
headers: (webhook.headers as Webhook['headers']) ?? [],
queryParams: (webhook.queryParams as Webhook['headers']) ?? [],
body: webhook.body ?? undefined,
}
: {
...defaultWebhookAttributes,
id: block.webhookId ?? '',
},
},
}
}

View File

@@ -125,16 +125,6 @@ export const blockTypeHasOption = (
.concat(Object.values(IntegrationBlockType)) .concat(Object.values(IntegrationBlockType))
.includes(type) .includes(type)
export const blockTypeHasWebhook = (
type: BlockType
): type is IntegrationBlockType.WEBHOOK =>
Object.values([
IntegrationBlockType.WEBHOOK,
IntegrationBlockType.ZAPIER,
IntegrationBlockType.MAKE_COM,
IntegrationBlockType.PABBLY_CONNECT,
] as string[]).includes(type)
export const blockTypeHasItems = ( export const blockTypeHasItems = (
type: BlockType type: BlockType
): type is ): type is

View File

@@ -5,7 +5,7 @@ export * from './googleSheets'
export * from './makeCom' export * from './makeCom'
export * from './pabblyConnect' export * from './pabblyConnect'
export * from './sendEmail' export * from './sendEmail'
export * from './webhook' export * from './webhook/schemas'
export * from './zapier' export * from './zapier'
export * from './pixel/schemas' export * from './pixel/schemas'
export * from './pixel/constants' export * from './pixel/constants'

View File

@@ -1,13 +1,16 @@
import { z } from 'zod' import { z } from 'zod'
import { blockBaseSchema } from '../baseSchemas' import { blockBaseSchema } from '../baseSchemas'
import { IntegrationBlockType } from './enums' import { IntegrationBlockType } from './enums'
import { webhookOptionsSchema } from './webhook' import { webhookOptionsSchema } from './webhook/schemas'
export const makeComBlockSchema = blockBaseSchema.merge( export const makeComBlockSchema = blockBaseSchema.merge(
z.object({ z.object({
type: z.enum([IntegrationBlockType.MAKE_COM]), type: z.enum([IntegrationBlockType.MAKE_COM]),
options: webhookOptionsSchema, options: webhookOptionsSchema,
webhookId: z.string(), webhookId: z
.string()
.describe('Deprecated, use webhook.id instead')
.optional(),
}) })
) )

View File

@@ -1,13 +1,16 @@
import { z } from 'zod' import { z } from 'zod'
import { blockBaseSchema } from '../baseSchemas' import { blockBaseSchema } from '../baseSchemas'
import { IntegrationBlockType } from './enums' import { IntegrationBlockType } from './enums'
import { webhookOptionsSchema } from './webhook' import { webhookOptionsSchema } from './webhook/schemas'
export const pabblyConnectBlockSchema = blockBaseSchema.merge( export const pabblyConnectBlockSchema = blockBaseSchema.merge(
z.object({ z.object({
type: z.enum([IntegrationBlockType.PABBLY_CONNECT]), type: z.enum([IntegrationBlockType.PABBLY_CONNECT]),
options: webhookOptionsSchema, options: webhookOptionsSchema,
webhookId: z.string(), webhookId: z
.string()
.describe('Deprecated, use webhook.id instead')
.optional(),
}) })
) )

View File

@@ -1,45 +0,0 @@
import { z } from 'zod'
import { blockBaseSchema } from '../baseSchemas'
import { IntegrationBlockType } from './enums'
const variableForTestSchema = z.object({
id: z.string(),
variableId: z.string().optional(),
value: z.string().optional(),
})
const responseVariableMappingSchema = z.object({
id: z.string(),
variableId: z.string().optional(),
bodyPath: z.string().optional(),
})
export const webhookOptionsSchema = z.object({
variablesForTest: z.array(variableForTestSchema),
responseVariableMapping: z.array(responseVariableMappingSchema),
isAdvancedConfig: z.boolean().optional(),
isCustomBody: z.boolean().optional(),
isExecutedOnClient: z.boolean().optional(),
})
export const webhookBlockSchema = blockBaseSchema.merge(
z.object({
type: z.enum([IntegrationBlockType.WEBHOOK]),
options: webhookOptionsSchema,
webhookId: z.string(),
})
)
export const defaultWebhookOptions: Omit<WebhookOptions, 'webhookId'> = {
responseVariableMapping: [],
variablesForTest: [],
isAdvancedConfig: false,
isCustomBody: false,
}
export type WebhookBlock = z.infer<typeof webhookBlockSchema>
export type WebhookOptions = z.infer<typeof webhookOptionsSchema>
export type ResponseVariableMapping = z.infer<
typeof responseVariableMappingSchema
>
export type VariableForTest = z.infer<typeof variableForTestSchema>

View File

@@ -0,0 +1,11 @@
export enum HttpMethod {
POST = 'POST',
GET = 'GET',
PUT = 'PUT',
DELETE = 'DELETE',
PATCH = 'PATCH',
HEAD = 'HEAD',
CONNECT = 'CONNECT',
OPTIONS = 'OPTIONS',
TRACE = 'TRACE',
}

View File

@@ -0,0 +1,2 @@
export * from './enums'
export * from './schemas'

View File

@@ -0,0 +1,95 @@
import { z } from 'zod'
import { blockBaseSchema } from '../../baseSchemas'
import { IntegrationBlockType } from '../enums'
import { HttpMethod } from './enums'
const variableForTestSchema = z.object({
id: z.string(),
variableId: z.string().optional(),
value: z.string().optional(),
})
const responseVariableMappingSchema = z.object({
id: z.string(),
variableId: z.string().optional(),
bodyPath: z.string().optional(),
})
const keyValueSchema = z.object({
id: z.string(),
key: z.string().optional(),
value: z.string().optional(),
})
export const webhookSchema = z.object({
id: z.string(),
queryParams: keyValueSchema.array(),
headers: keyValueSchema.array(),
method: z.nativeEnum(HttpMethod),
url: z.string().optional(),
body: z.string().optional(),
})
export const webhookOptionsSchema = z.object({
variablesForTest: z.array(variableForTestSchema),
responseVariableMapping: z.array(responseVariableMappingSchema),
isAdvancedConfig: z.boolean().optional(),
isCustomBody: z.boolean().optional(),
isExecutedOnClient: z.boolean().optional(),
webhook: webhookSchema.optional(),
})
export const webhookBlockSchema = blockBaseSchema.merge(
z.object({
type: z.enum([IntegrationBlockType.WEBHOOK]),
options: webhookOptionsSchema,
webhookId: z
.string()
.describe('Deprecated, now integrated in webhook block options')
.optional(),
})
)
export const defaultWebhookAttributes: Omit<
Webhook,
'id' | 'body' | 'url' | 'typebotId' | 'createdAt' | 'updatedAt'
> = {
method: HttpMethod.POST,
headers: [],
queryParams: [],
}
export const defaultWebhookOptions = (webhookId: string): WebhookOptions => ({
responseVariableMapping: [],
variablesForTest: [],
isAdvancedConfig: false,
isCustomBody: false,
webhook: {
id: webhookId,
...defaultWebhookAttributes,
},
})
export const executableWebhookSchema = z.object({
url: z.string(),
headers: z.record(z.string()).optional(),
body: z.unknown().optional(),
method: z.nativeEnum(HttpMethod).optional(),
})
export type KeyValue = { id: string; key?: string; value?: string }
export type WebhookResponse = {
statusCode: number
data?: unknown
}
export type ExecutableWebhook = z.infer<typeof executableWebhookSchema>
export type Webhook = z.infer<typeof webhookSchema>
export type WebhookBlock = z.infer<typeof webhookBlockSchema>
export type WebhookOptions = z.infer<typeof webhookOptionsSchema>
export type ResponseVariableMapping = z.infer<
typeof responseVariableMappingSchema
>
export type VariableForTest = z.infer<typeof variableForTestSchema>

View File

@@ -1,13 +1,16 @@
import { z } from 'zod' import { z } from 'zod'
import { blockBaseSchema } from '../baseSchemas' import { blockBaseSchema } from '../baseSchemas'
import { IntegrationBlockType } from './enums' import { IntegrationBlockType } from './enums'
import { webhookOptionsSchema } from './webhook' import { webhookOptionsSchema } from './webhook/schemas'
export const zapierBlockSchema = blockBaseSchema.merge( export const zapierBlockSchema = blockBaseSchema.merge(
z.object({ z.object({
type: z.enum([IntegrationBlockType.ZAPIER]), type: z.enum([IntegrationBlockType.ZAPIER]),
options: webhookOptionsSchema, options: webhookOptionsSchema,
webhookId: z.string(), webhookId: z
.string()
.describe('Deprecated, use webhook.id instead')
.optional(),
}) })
) )

View File

@@ -1,5 +1,6 @@
import { z } from 'zod' import { z } from 'zod'
import { import {
executableWebhookSchema,
googleAnalyticsOptionsSchema, googleAnalyticsOptionsSchema,
paymentInputRuntimeOptionsSchema, paymentInputRuntimeOptionsSchema,
pixelOptionsSchema, pixelOptionsSchema,
@@ -19,7 +20,6 @@ import { answerSchema } from './answer'
import { BubbleBlockType } from './blocks/bubbles/enums' import { BubbleBlockType } from './blocks/bubbles/enums'
import { inputBlockSchemas } from './blocks/schemas' import { inputBlockSchemas } from './blocks/schemas'
import { chatCompletionMessageSchema } from './blocks/integrations/openai' import { chatCompletionMessageSchema } from './blocks/integrations/openai'
import { executableWebhookSchema } from './webhooks'
const typebotInSessionStateSchema = publicTypebotSchema.pick({ const typebotInSessionStateSchema = publicTypebotSchema.pick({
id: true, id: true,

View File

@@ -11,7 +11,7 @@ import { z } from 'zod'
export const publicTypebotSchema = z.object({ export const publicTypebotSchema = z.object({
id: z.string(), id: z.string(),
version: z.enum(['3']).nullable(), version: z.enum(['3', '4']).nullable(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
typebotId: z.string(), typebotId: z.string(),

View File

@@ -39,7 +39,7 @@ const resultsTablePreferencesSchema = z.object({
}) })
export const typebotSchema = z.object({ export const typebotSchema = z.object({
version: z.enum(['3']).nullable(), version: z.enum(['3', '4']).nullable(),
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
groups: z.array(groupSchema), groups: z.array(groupSchema),

View File

@@ -1,48 +0,0 @@
import { Webhook as WebhookFromPrisma } from '@typebot.io/prisma'
import { z } from 'zod'
export enum HttpMethod {
POST = 'POST',
GET = 'GET',
PUT = 'PUT',
DELETE = 'DELETE',
PATCH = 'PATCH',
HEAD = 'HEAD',
CONNECT = 'CONNECT',
OPTIONS = 'OPTIONS',
TRACE = 'TRACE',
}
export type KeyValue = { id: string; key?: string; value?: string }
export type Webhook = Omit<
WebhookFromPrisma,
'queryParams' | 'headers' | 'method' | 'createdAt' | 'updatedAt'
> & {
queryParams: KeyValue[]
headers: KeyValue[]
method: HttpMethod
}
export type WebhookResponse = {
statusCode: number
data?: unknown
}
export const defaultWebhookAttributes: Omit<
Webhook,
'id' | 'body' | 'url' | 'typebotId' | 'createdAt' | 'updatedAt'
> = {
method: HttpMethod.POST,
headers: [],
queryParams: [],
}
export const executableWebhookSchema = z.object({
url: z.string(),
headers: z.record(z.string()).optional(),
body: z.unknown().optional(),
method: z.nativeEnum(HttpMethod).optional(),
})
export type ExecutableWebhook = z.infer<typeof executableWebhookSchema>

View File

@@ -5,7 +5,6 @@ export * from './features/result'
export * from './features/answer' export * from './features/answer'
export * from './features/utils' export * from './features/utils'
export * from './features/credentials' export * from './features/credentials'
export * from './features/webhooks'
export * from './features/chat' export * from './features/chat'
export * from './features/workspace' export * from './features/workspace'
export * from './features/items' export * from './features/items'