From a58600a38a9be025d956dd3dbda9d0020a70b630 Mon Sep 17 00:00:00 2001
From: Baptiste Arnaud
Date: Sat, 22 Jan 2022 18:24:57 +0100
Subject: [PATCH] =?UTF-8?q?feat(integration):=20=E2=9C=A8=20Add=20webhooks?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
apps/builder/assets/icons.tsx | 6 +
apps/builder/assets/styles/codeMirror.css | 9 +
.../board/StepTypesList/StepIcon.tsx | 3 +
.../board/StepTypesList/StepTypeLabel.tsx | 2 +
.../board/graph/BlockNode/BlockNode.tsx | 2 +-
.../SettingsPopoverContent.tsx | 27 +-
.../bodies/ChoiceInputSettingsBody.tsx | 4 +-
.../ConditionSettingsBody/ComparisonsItem.tsx | 51 ++
.../ConditionSettingsBody/ComparisonsList.tsx | 160 ------
.../ConditonSettingsBody.tsx | 23 +-
.../bodies/DateInputSettingsBody.tsx | 4 +-
.../bodies/EmailInputSettingsBody.tsx | 4 +-
.../CellWithValueStack.tsx | 35 ++
.../CellWithVariableIdStack.tsx | 37 ++
.../ExtractCellList.tsx | 141 ------
.../GoogleSheetsSettingsBody.tsx | 54 ++-
.../SheetsDropdown.tsx | 2 +-
.../SpreadsheetDropdown.tsx | 2 +-
.../UpdateCellList.tsx | 141 ------
.../bodies/NumberInputSettingsBody.tsx | 4 +-
.../bodies/PhoneNumberSettingsBody.tsx | 4 +-
.../bodies/SetVariableSettingsBody.tsx | 4 +-
.../bodies/TextInputSettingsBody.tsx | 4 +-
.../bodies/UrlInputSettingsBody.tsx | 4 +-
.../bodies/WebhookSettings/KeyValueInputs.tsx | 62 +++
.../WebhookSettings/ResponseMappingInputs.tsx | 38 ++
.../WebhookSettings/VariableForTestInputs.tsx | 39 ++
.../WebhookSettings/WebhookSettings.tsx | 204 ++++++++
.../bodies/WebhookSettings/index.tsx | 1 +
.../graph/BlockNode/StepNode/StepNode.tsx | 3 +-
.../BlockNode/StepNode/StepNodeContent.tsx | 238 ---------
.../StepNodeContent/ConditionNodeContent.tsx | 52 ++
.../SetVariableNodeContent.tsx | 17 +
.../StepNodeContent/StepNodeContent.tsx | 128 +++++
.../StepNodeContent/VideoStepNodeContent.tsx | 51 ++
.../StepNodeContent/WebhookContent.tsx | 22 +
.../StepNode/StepNodeContent/index.tsx | 1 +
.../BlockNode/StepNode/StepNodeOverlay.tsx | 2 +-
.../StepNode/TextEditor/TextEditor.tsx | 4 +-
apps/builder/components/shared/CodeEditor.tsx | 58 +++
.../shared/InputWithVariableButton.tsx | 25 +-
.../components/shared/SearchableDropdown.tsx | 17 +-
apps/builder/components/shared/TableList.tsx | 114 +++++
.../components/shared/VariableSearchInput.tsx | 13 +-
.../FontSelector/FontSelector.tsx | 13 +-
.../TypebotContext/TypebotContext.tsx | 7 +-
.../contexts/TypebotContext/actions/steps.ts | 5 +-
.../TypebotContext/actions/variables.ts | 21 +-
.../TypebotContext/actions/webhooks.ts | 41 ++
.../typebots/integrations/googleSheets.json | 1 +
.../integrations/googleSheetsGet.json | 1 +
.../typebots/integrations/webhook.json | 150 ++++++
.../typebots/integrations/webhookPreview.json | 264 ++++++++++
.../typebots/integrations/webhookPreview.png | Bin 0 -> 289483 bytes
.../fixtures/typebots/logic/condition.json | 1 +
.../fixtures/typebots/logic/redirect.json | 1 +
.../fixtures/typebots/logic/setVariable.json | 1 +
.../fixtures/typebots/singleChoiceTarget.json | 1 +
apps/builder/cypress/plugins/utils.ts | 1 +
apps/builder/cypress/tests/bubbles/image.ts | 2 +-
.../cypress/tests/integrations/webhooks.ts | 96 ++++
apps/builder/cypress/tests/logic/condition.ts | 6 +-
apps/builder/package.json | 4 +
apps/builder/pages/_app.tsx | 1 +
apps/builder/pages/api/mock/webhook.ts | 26 +
.../[typebotId]/webhooks/[id]/execute.ts | 104 ++++
apps/builder/services/integrations.ts | 66 +++
apps/builder/services/typebots.ts | 6 +-
packages/bot-engine/rollup.config.js | 2 +-
.../src/components/ChatBlock/ChatBlock.tsx | 1 +
packages/bot-engine/src/index.ts | 2 +-
.../bot-engine/src/services/integration.ts | 30 ++
packages/db/package.json | 6 +-
packages/db/prisma/schema.prisma | 1 +
.../models/src/typebot/steps/integration.ts | 49 +-
packages/models/src/typebot/typebot.ts | 10 +-
packages/utils/src/utils.ts | 4 +
yarn.lock | 459 +++++++++++++++++-
78 files changed, 2399 insertions(+), 800 deletions(-)
create mode 100644 apps/builder/assets/styles/codeMirror.css
create mode 100644 apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ComparisonsItem.tsx
delete mode 100644 apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ComparisonsList.tsx
create mode 100644 apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/CellWithValueStack.tsx
create mode 100644 apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/CellWithVariableIdStack.tsx
delete mode 100644 apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/ExtractCellList.tsx
delete mode 100644 apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/UpdateCellList.tsx
create mode 100644 apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/KeyValueInputs.tsx
create mode 100644 apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/ResponseMappingInputs.tsx
create mode 100644 apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/VariableForTestInputs.tsx
create mode 100644 apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/WebhookSettings.tsx
create mode 100644 apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/index.tsx
delete mode 100644 apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent.tsx
create mode 100644 apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/ConditionNodeContent.tsx
create mode 100644 apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/SetVariableNodeContent.tsx
create mode 100644 apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/StepNodeContent.tsx
create mode 100644 apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/VideoStepNodeContent.tsx
create mode 100644 apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/WebhookContent.tsx
create mode 100644 apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/index.tsx
create mode 100644 apps/builder/components/shared/CodeEditor.tsx
create mode 100644 apps/builder/components/shared/TableList.tsx
create mode 100644 apps/builder/contexts/TypebotContext/actions/webhooks.ts
create mode 100644 apps/builder/cypress/fixtures/typebots/integrations/webhook.json
create mode 100644 apps/builder/cypress/fixtures/typebots/integrations/webhookPreview.json
create mode 100644 apps/builder/cypress/fixtures/typebots/integrations/webhookPreview.png
create mode 100644 apps/builder/cypress/tests/integrations/webhooks.ts
create mode 100644 apps/builder/pages/api/mock/webhook.ts
create mode 100644 apps/builder/pages/api/typebots/[typebotId]/webhooks/[id]/execute.ts
diff --git a/apps/builder/assets/icons.tsx b/apps/builder/assets/icons.tsx
index df5a15ce7..2ff549b86 100644
--- a/apps/builder/assets/icons.tsx
+++ b/apps/builder/assets/icons.tsx
@@ -280,3 +280,9 @@ export const FilmIcon = (props: IconProps) => (
)
+
+export const WebhookIcon = (props: IconProps) => (
+
+
+
+)
diff --git a/apps/builder/assets/styles/codeMirror.css b/apps/builder/assets/styles/codeMirror.css
new file mode 100644
index 000000000..0b40e9102
--- /dev/null
+++ b/apps/builder/assets/styles/codeMirror.css
@@ -0,0 +1,9 @@
+.cm-editor {
+ height: 100%;
+ outline: 0px solid transparent !important;
+}
+
+.cm-scroller {
+ border-radius: 5px;
+ border: 1px solid #e5e7eb;
+}
diff --git a/apps/builder/components/board/StepTypesList/StepIcon.tsx b/apps/builder/components/board/StepTypesList/StepIcon.tsx
index e72d59b4f..d496cc116 100644
--- a/apps/builder/components/board/StepTypesList/StepIcon.tsx
+++ b/apps/builder/components/board/StepTypesList/StepIcon.tsx
@@ -14,6 +14,7 @@ import {
NumberIcon,
PhoneIcon,
TextIcon,
+ WebhookIcon,
} from 'assets/icons'
import { GoogleAnalyticsLogo, GoogleSheetsLogo } from 'assets/logos'
import {
@@ -59,6 +60,8 @@ export const StepIcon = ({ type, ...props }: StepIconProps) => {
return
case IntegrationStepType.GOOGLE_ANALYTICS:
return
+ case IntegrationStepType.WEBHOOK:
+ return
case 'start':
return
default:
diff --git a/apps/builder/components/board/StepTypesList/StepTypeLabel.tsx b/apps/builder/components/board/StepTypesList/StepTypeLabel.tsx
index 279e8467c..50038598f 100644
--- a/apps/builder/components/board/StepTypesList/StepTypeLabel.tsx
+++ b/apps/builder/components/board/StepTypesList/StepTypeLabel.tsx
@@ -49,6 +49,8 @@ export const StepTypeLabel = ({ type }: Props) => {
Analytics
)
+ case IntegrationStepType.WEBHOOK:
+ return Webhook
default:
return <>>
}
diff --git a/apps/builder/components/board/graph/BlockNode/BlockNode.tsx b/apps/builder/components/board/graph/BlockNode/BlockNode.tsx
index a8362fd22..be69bbbc0 100644
--- a/apps/builder/components/board/graph/BlockNode/BlockNode.tsx
+++ b/apps/builder/components/board/graph/BlockNode/BlockNode.tsx
@@ -89,7 +89,7 @@ export const BlockNode = ({ block }: Props) => {
borderColor={
isConnecting || isOpened || isPreviewing ? 'blue.400' : 'white'
}
- minW="300px"
+ w="300px"
transition="border 300ms, box-shadow 200ms"
pos="absolute"
style={{
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/SettingsPopoverContent.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/SettingsPopoverContent.tsx
index ef766ed8c..a494fc59f 100644
--- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/SettingsPopoverContent.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/SettingsPopoverContent.tsx
@@ -16,6 +16,8 @@ import {
Step,
StepOptions,
TextBubbleStep,
+ Webhook,
+ WebhookStep,
} from 'models'
import { useRef } from 'react'
import {
@@ -32,6 +34,7 @@ import { GoogleSheetsSettingsBody } from './bodies/GoogleSheetsSettingsBody'
import { PhoneNumberSettingsBody } from './bodies/PhoneNumberSettingsBody'
import { RedirectSettings } from './bodies/RedirectSettings'
import { SetVariableSettingsBody } from './bodies/SetVariableSettingsBody'
+import { WebhookSettings } from './bodies/WebhookSettings'
type Props = {
step: Exclude
@@ -51,7 +54,7 @@ export const SettingsPopoverContent = ({ step, onExpandClick }: Props) => {
{
}
export const StepSettings = ({ step }: { step: Step }) => {
- const { updateStep } = useTypebot()
- const handleOptionsChange = (options: StepOptions) =>
+ const { updateStep, updateWebhook, typebot } = useTypebot()
+ const handleOptionsChange = (options: StepOptions) => {
updateStep(step.id, { options } as Partial)
+ }
+
+ const handleWebhookChange = (webhook: Partial) => {
+ const webhookId = (step as WebhookStep).options?.webhookId
+ if (!webhookId) return
+ updateWebhook(webhookId, webhook)
+ }
switch (step.type) {
case InputStepType.TEXT: {
@@ -176,6 +186,17 @@ export const StepSettings = ({ step }: { step: Step }) => {
/>
)
}
+ case IntegrationStepType.WEBHOOK: {
+ return (
+
+ )
+ }
default: {
return <>>
}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ChoiceInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ChoiceInputSettingsBody.tsx
index 17d24add2..dbcb09122 100644
--- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ChoiceInputSettingsBody.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ChoiceInputSettingsBody.tsx
@@ -18,8 +18,8 @@ export const ChoiceInputSettingsBody = ({
options && onOptionsChange({ ...options, isMultipleChoice })
const handleButtonLabelChange = (buttonLabel: string) =>
options && onOptionsChange({ ...options, buttonLabel })
- const handleVariableChange = (variable: Variable) =>
- options && onOptionsChange({ ...options, variableId: variable.id })
+ const handleVariableChange = (variable?: Variable) =>
+ options && onOptionsChange({ ...options, variableId: variable?.id })
return (
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ComparisonsItem.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ComparisonsItem.tsx
new file mode 100644
index 000000000..aaee21757
--- /dev/null
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ComparisonsItem.tsx
@@ -0,0 +1,51 @@
+import { Stack } from '@chakra-ui/react'
+import { DropdownList } from 'components/shared/DropdownList'
+import { InputWithVariableButton } from 'components/shared/InputWithVariableButton'
+import { TableListItemProps } from 'components/shared/TableList'
+import { VariableSearchInput } from 'components/shared/VariableSearchInput'
+import { Comparison, Variable, ComparisonOperators } from 'models'
+
+export const ComparisonItem = ({
+ item,
+ onItemChange,
+}: TableListItemProps) => {
+ const handleSelectVariable = (variable?: Variable) => {
+ if (variable?.id === item.variableId) return
+ onItemChange({ ...item, variableId: variable?.id })
+ }
+
+ const handleSelectComparisonOperator = (
+ comparisonOperator: ComparisonOperators
+ ) => {
+ if (comparisonOperator === item.comparisonOperator) return
+ onItemChange({ ...item, comparisonOperator })
+ }
+ const handleChangeValue = (value: string) => {
+ if (value === item.value) return
+ onItemChange({ ...item, value })
+ }
+
+ return (
+
+
+
+ currentItem={item.comparisonOperator}
+ onItemSelect={handleSelectComparisonOperator}
+ items={Object.values(ComparisonOperators)}
+ placeholder="Select an operator"
+ />
+ {item.comparisonOperator !== ComparisonOperators.IS_SET && (
+
+ )}
+
+ )
+}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ComparisonsList.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ComparisonsList.tsx
deleted file mode 100644
index 89ef41a71..000000000
--- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ComparisonsList.tsx
+++ /dev/null
@@ -1,160 +0,0 @@
-import { Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react'
-import { PlusIcon, TrashIcon } from 'assets/icons'
-import { DebouncedInput } from 'components/shared/DebouncedInput'
-import { DropdownList } from 'components/shared/DropdownList'
-import { VariableSearchInput } from 'components/shared/VariableSearchInput'
-import {
- Comparison,
- ComparisonOperators,
- LogicalOperator,
- Table,
- Variable,
-} from 'models'
-import React, { useEffect, useState } from 'react'
-import { generate } from 'short-uuid'
-import { useImmer } from 'use-immer'
-
-type Props = {
- initialComparisons: Table
- logicalOperator: LogicalOperator
- onLogicalOperatorChange: (logicalOperator: LogicalOperator) => void
- onComparisonsChange: (comparisons: Table) => void
-}
-
-export const ComparisonsList = ({
- initialComparisons,
- logicalOperator,
- onLogicalOperatorChange,
- onComparisonsChange,
-}: Props) => {
- const [comparisons, setComparisons] = useImmer(initialComparisons)
- const [showDeleteId, setShowDeleteId] = useState()
-
- useEffect(() => {
- onComparisonsChange(comparisons)
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [comparisons])
-
- const createComparison = () => {
- setComparisons((comparisons) => {
- const id = generate()
- comparisons.byId[id] = {
- id,
- comparisonOperator: ComparisonOperators.EQUAL,
- }
- comparisons.allIds.push(id)
- })
- }
-
- const updateComparison = (
- comparisonId: string,
- updates: Partial>
- ) =>
- setComparisons((comparisons) => {
- comparisons.byId[comparisonId] = {
- ...comparisons.byId[comparisonId],
- ...updates,
- }
- })
-
- const deleteComparison = (comparisonId: string) => () => {
- setComparisons((comparisons) => {
- delete comparisons.byId[comparisonId]
- const index = comparisons.allIds.indexOf(comparisonId)
- if (index !== -1) comparisons.allIds.splice(index, 1)
- })
- }
-
- const handleVariableSelected =
- (comparisonId: string) => (variable: Variable) => {
- updateComparison(comparisonId, { variableId: variable.id })
- }
-
- const handleComparisonOperatorSelected =
- (comparisonId: string) => (dropdownItem: ComparisonOperators) =>
- updateComparison(comparisonId, {
- comparisonOperator: dropdownItem,
- })
- const handleLogicalOperatorSelected = (dropdownItem: LogicalOperator) =>
- onLogicalOperatorChange(dropdownItem)
-
- const handleValueChange = (comparisonId: string) => (value: string) =>
- updateComparison(comparisonId, { value })
-
- const handleMouseEnter = (comparisonId: string) => () => {
- setShowDeleteId(comparisonId)
- }
-
- const handleMouseLeave = () => setShowDeleteId(undefined)
-
- return (
-
- {comparisons.allIds.map((comparisonId, idx) => (
- <>
- {idx > 0 && (
-
-
- currentItem={logicalOperator}
- onItemSelect={handleLogicalOperatorSelected}
- items={Object.values(LogicalOperator)}
- />
-
- )}
-
-
-
-
- currentItem={comparisons.byId[comparisonId].comparisonOperator}
- onItemSelect={handleComparisonOperatorSelected(comparisonId)}
- items={Object.values(ComparisonOperators)}
- />
- {comparisons.byId[comparisonId].comparisonOperator !==
- ComparisonOperators.IS_SET && (
-
- )}
-
-
- }
- aria-label="Remove comparison"
- onClick={deleteComparison(comparisonId)}
- pos="absolute"
- left="-15px"
- top="-15px"
- size="sm"
- shadow="md"
- />
-
-
- >
- ))}
- }
- onClick={createComparison}
- flexShrink={0}
- colorScheme="blue"
- >
- Add a comparison
-
-
- )
-}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ConditonSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ConditonSettingsBody.tsx
index afc8728f8..7bc447bfd 100644
--- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ConditonSettingsBody.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ConditonSettingsBody.tsx
@@ -1,6 +1,9 @@
+import { Flex } from '@chakra-ui/react'
+import { DropdownList } from 'components/shared/DropdownList'
+import { TableList } from 'components/shared/TableList'
import { Comparison, ConditionOptions, LogicalOperator, Table } from 'models'
import React from 'react'
-import { ComparisonsList } from './ComparisonsList'
+import { ComparisonItem } from './ComparisonsItem'
type ConditionSettingsBodyProps = {
options: ConditionOptions
@@ -17,11 +20,19 @@ export const ConditionSettingsBody = ({
onOptionsChange({ ...options, logicalOperator })
return (
-
+ onItemsChange={handleComparisonsChange}
+ Item={ComparisonItem}
+ ComponentBetweenItems={() => (
+
+
+ currentItem={options.logicalOperator}
+ onItemSelect={handleLogicalOperatorChange}
+ items={Object.values(LogicalOperator)}
+ />
+
+ )}
+ addLabel="Add a comparison"
/>
)
}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/DateInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/DateInputSettingsBody.tsx
index 1649f5f49..7973366ab 100644
--- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/DateInputSettingsBody.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/DateInputSettingsBody.tsx
@@ -24,8 +24,8 @@ export const DateInputSettingsBody = ({
onOptionsChange({ ...options, isRange })
const handleHasTimeChange = (hasTime: boolean) =>
onOptionsChange({ ...options, hasTime })
- const handleVariableChange = (variable: Variable) =>
- onOptionsChange({ ...options, variableId: variable.id })
+ const handleVariableChange = (variable?: Variable) =>
+ onOptionsChange({ ...options, variableId: variable?.id })
return (
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/EmailInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/EmailInputSettingsBody.tsx
index 67252c296..25d42d814 100644
--- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/EmailInputSettingsBody.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/EmailInputSettingsBody.tsx
@@ -17,8 +17,8 @@ export const EmailInputSettingsBody = ({
onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } })
const handleButtonLabelChange = (button: string) =>
onOptionsChange({ ...options, labels: { ...options?.labels, button } })
- const handleVariableChange = (variable: Variable) =>
- onOptionsChange({ ...options, variableId: variable.id })
+ const handleVariableChange = (variable?: Variable) =>
+ onOptionsChange({ ...options, variableId: variable?.id })
return (
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/CellWithValueStack.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/CellWithValueStack.tsx
new file mode 100644
index 000000000..13db46c39
--- /dev/null
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/CellWithValueStack.tsx
@@ -0,0 +1,35 @@
+import { Stack } from '@chakra-ui/react'
+import { DropdownList } from 'components/shared/DropdownList'
+import { InputWithVariableButton } from 'components/shared/InputWithVariableButton'
+import { TableListItemProps } from 'components/shared/TableList'
+import { Cell } from 'models'
+
+export const CellWithValueStack = ({
+ item,
+ onItemChange,
+ columns,
+}: TableListItemProps & { columns: string[] }) => {
+ const handleColumnSelect = (column: string) => {
+ if (item.column === column) return
+ onItemChange({ ...item, column })
+ }
+ const handleValueChange = (value: string) => {
+ if (item.value === value) return
+ onItemChange({ ...item, value })
+ }
+ return (
+
+
+ currentItem={item.column}
+ onItemSelect={handleColumnSelect}
+ items={columns}
+ placeholder="Select a column"
+ />
+
+
+ )
+}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/CellWithVariableIdStack.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/CellWithVariableIdStack.tsx
new file mode 100644
index 000000000..289416d8e
--- /dev/null
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/CellWithVariableIdStack.tsx
@@ -0,0 +1,37 @@
+import { Stack } from '@chakra-ui/react'
+import { DropdownList } from 'components/shared/DropdownList'
+import { TableListItemProps } from 'components/shared/TableList'
+import { VariableSearchInput } from 'components/shared/VariableSearchInput'
+import { ExtractingCell, Variable } from 'models'
+
+export const CellWithVariableIdStack = ({
+ item,
+ onItemChange,
+ columns,
+}: TableListItemProps & { columns: string[] }) => {
+ const handleColumnSelect = (column: string) => {
+ if (item.column === column) return
+ onItemChange({ ...item, column })
+ }
+
+ const handleVariableIdChange = (variable?: Variable) => {
+ if (item.variableId === variable?.id) return
+ onItemChange({ ...item, variableId: variable?.id })
+ }
+
+ return (
+
+
+ currentItem={item.column}
+ onItemSelect={handleColumnSelect}
+ items={columns}
+ placeholder="Select a column"
+ />
+
+
+ )
+}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/ExtractCellList.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/ExtractCellList.tsx
deleted file mode 100644
index 2121e50b5..000000000
--- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/ExtractCellList.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-import { Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react'
-import { PlusIcon, TrashIcon } from 'assets/icons'
-import { DropdownList } from 'components/shared/DropdownList'
-import { VariableSearchInput } from 'components/shared/VariableSearchInput'
-import { ExtractingCell, Table, Variable } from 'models'
-import React, { useEffect, useState } from 'react'
-import { Sheet } from 'services/integrations'
-import { generate } from 'short-uuid'
-import { useImmer } from 'use-immer'
-
-type Props = {
- sheet: Sheet
- initialCells?: Table
- onCellsChange: (cells: Table) => void
-}
-
-const id = generate()
-const defaultCells: Table = {
- byId: { [id]: {} },
- allIds: [id],
-}
-
-export const ExtractCellList = ({
- sheet,
- initialCells,
- onCellsChange,
-}: Props) => {
- const [cells, setCells] = useImmer(initialCells ?? defaultCells)
- const [showDeleteId, setShowDeleteId] = useState()
-
- useEffect(() => {
- onCellsChange(cells)
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [cells])
-
- const createCell = () => {
- setCells((cells) => {
- const id = generate()
- cells.byId[id] = {}
- cells.allIds.push(id)
- })
- }
-
- const updateCell = (cellId: string, updates: Partial) =>
- setCells((cells) => {
- cells.byId[cellId] = {
- ...cells.byId[cellId],
- ...updates,
- }
- })
-
- const deleteCell = (cellId: string) => () => {
- setCells((cells) => {
- delete cells.byId[cellId]
- const index = cells.allIds.indexOf(cellId)
- if (index !== -1) cells.allIds.splice(index, 1)
- })
- }
-
- const handleMouseEnter = (cellId: string) => () => {
- setShowDeleteId(cellId)
- }
-
- const handleCellChange = (cellId: string) => (cell: ExtractingCell) =>
- updateCell(cellId, cell)
-
- const handleMouseLeave = () => setShowDeleteId(undefined)
-
- return (
-
- {cells.allIds.map((cellId) => (
- <>
-
-
-
- }
- aria-label="Remove cell"
- onClick={deleteCell(cellId)}
- pos="absolute"
- left="-15px"
- top="-15px"
- size="sm"
- shadow="md"
- />
-
-
- >
- ))}
- }
- onClick={createCell}
- flexShrink={0}
- colorScheme="blue"
- >
- Add a value
-
-
- )
-}
-
-export const CellWithVariableIdStack = ({
- cell,
- columns,
- onCellChange,
-}: {
- cell: ExtractingCell
- columns: string[]
- onCellChange: (cell: ExtractingCell) => void
-}) => {
- const handleColumnSelect = (column: string) => {
- onCellChange({ ...cell, column })
- }
- const handleVariableIdChange = (variable: Variable) => {
- onCellChange({ ...cell, variableId: variable.id })
- }
- return (
-
-
- currentItem={cell.column}
- onItemSelect={handleColumnSelect}
- items={columns}
- placeholder="Select a column"
- />
-
-
- )
-}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/GoogleSheetsSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/GoogleSheetsSettingsBody.tsx
index 5e6fd56a3..e25f204e0 100644
--- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/GoogleSheetsSettingsBody.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/GoogleSheetsSettingsBody.tsx
@@ -1,6 +1,7 @@
import { Divider, Stack, Text } from '@chakra-ui/react'
import { CredentialsDropdown } from 'components/shared/CredentialsDropdown'
import { DropdownList } from 'components/shared/DropdownList'
+import { TableList, TableListItemProps } from 'components/shared/TableList'
import { useTypebot } from 'contexts/TypebotContext'
import { CredentialsType } from 'db'
import {
@@ -17,10 +18,10 @@ import {
useSheets,
} from 'services/integrations'
import { isDefined } from 'utils'
-import { ExtractCellList } from './ExtractCellList'
import { SheetsDropdown } from './SheetsDropdown'
import { SpreadsheetsDropdown } from './SpreadsheetDropdown'
-import { CellWithValueStack, UpdateCellList } from './UpdateCellList'
+import { CellWithValueStack } from './CellWithValueStack'
+import { CellWithVariableIdStack } from './CellWithVariableIdStack'
type Props = {
options?: GoogleSheetsOptions
@@ -132,13 +133,26 @@ const ActionOptions = ({
const handleExtractingCellsChange = (cellsToExtract: Table) =>
onOptionsChange({ ...options, cellsToExtract } as GoogleSheetsOptions)
+ const UpdatingCellItem = useMemo(
+ () => (props: TableListItemProps) =>
+ ,
+ [sheet.columns]
+ )
+
+ const ExtractingCellItem = useMemo(
+ () => (props: TableListItemProps) =>
+ ,
+ [sheet.columns]
+ )
+
switch (options.action) {
case GoogleSheetsAction.INSERT_ROW:
return (
-
+ initialItems={options.cellsToInsert}
+ onItemsChange={handleInsertColumnsChange}
+ Item={UpdatingCellItem}
+ addLabel="Add a value"
/>
)
case GoogleSheetsAction.UPDATE_ROW:
@@ -146,15 +160,17 @@ const ActionOptions = ({
Row to select
Cells to update
-
+ initialItems={options.cellsToUpsert}
+ onItemsChange={handleUpsertColumnsChange}
+ Item={UpdatingCellItem}
+ addLabel="Add a value"
/>
)
@@ -163,15 +179,17 @@ const ActionOptions = ({
Row to select
Cells to extract
-
+ initialItems={options.cellsToExtract}
+ onItemsChange={handleExtractingCellsChange}
+ Item={ExtractingCellItem}
+ addLabel="Add a value"
/>
)
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/SheetsDropdown.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/SheetsDropdown.tsx
index ac2c9c748..7da02a930 100644
--- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/SheetsDropdown.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/SheetsDropdown.tsx
@@ -29,7 +29,7 @@ export const SheetsDropdown = ({
s.name)}
- onSelectItem={handleSpreadsheetSelect}
+ onValueChange={handleSpreadsheetSelect}
placeholder={isLoading ? 'Loading...' : 'Select the sheet'}
isDisabled={isLoading}
/>
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/SpreadsheetDropdown.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/SpreadsheetDropdown.tsx
index a6ab8d768..b50218828 100644
--- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/SpreadsheetDropdown.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/SpreadsheetDropdown.tsx
@@ -27,7 +27,7 @@ export const SpreadsheetsDropdown = ({
s.name)}
- onSelectItem={handleSpreadsheetSelect}
+ onValueChange={handleSpreadsheetSelect}
placeholder={isLoading ? 'Loading...' : 'Search for spreadsheet'}
isDisabled={isLoading}
/>
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/UpdateCellList.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/UpdateCellList.tsx
deleted file mode 100644
index 28ee2e01e..000000000
--- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/UpdateCellList.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-import { Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react'
-import { PlusIcon, TrashIcon } from 'assets/icons'
-import { DropdownList } from 'components/shared/DropdownList'
-import { InputWithVariableButton } from 'components/shared/InputWithVariableButton'
-import { Cell, Table } from 'models'
-import React, { useEffect, useState } from 'react'
-import { Sheet } from 'services/integrations'
-import { generate } from 'short-uuid'
-import { useImmer } from 'use-immer'
-
-type Props = {
- sheet: Sheet
- initialCells?: Table
- onCellsChange: (cells: Table) => void
-}
-
-const id = generate()
-const defaultCells: Table = {
- byId: { [id]: {} },
- allIds: [id],
-}
-
-export const UpdateCellList = ({
- sheet,
- initialCells,
- onCellsChange,
-}: Props) => {
- const [cells, setCells] = useImmer(initialCells ?? defaultCells)
- const [showDeleteId, setShowDeleteId] = useState()
-
- useEffect(() => {
- onCellsChange(cells)
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [cells])
-
- const createCell = () => {
- setCells((cells) => {
- const id = generate()
- cells.byId[id] = {}
- cells.allIds.push(id)
- })
- }
-
- const updateCell = (cellId: string, updates: Partial) =>
- setCells((cells) => {
- cells.byId[cellId] = {
- ...cells.byId[cellId],
- ...updates,
- }
- })
-
- const deleteCell = (cellId: string) => () => {
- setCells((cells) => {
- delete cells.byId[cellId]
- const index = cells.allIds.indexOf(cellId)
- if (index !== -1) cells.allIds.splice(index, 1)
- })
- }
-
- const handleMouseEnter = (cellId: string) => () => {
- setShowDeleteId(cellId)
- }
-
- const handleCellChange = (cellId: string) => (cell: Cell) =>
- updateCell(cellId, cell)
-
- const handleMouseLeave = () => setShowDeleteId(undefined)
-
- return (
-
- {cells.allIds.map((cellId) => (
- <>
-
-
-
- }
- aria-label="Remove cell"
- onClick={deleteCell(cellId)}
- pos="absolute"
- left="-15px"
- top="-15px"
- size="sm"
- shadow="md"
- />
-
-
- >
- ))}
- }
- onClick={createCell}
- flexShrink={0}
- colorScheme="blue"
- >
- Add a value
-
-
- )
-}
-
-export const CellWithValueStack = ({
- cell,
- columns,
- onCellChange,
-}: {
- cell: Cell
- columns: string[]
- onCellChange: (column: Cell) => void
-}) => {
- const handleColumnSelect = (column: string) => {
- onCellChange({ ...cell, column })
- }
- const handleValueChange = (value: string) => {
- onCellChange({ ...cell, value })
- }
- return (
-
-
- currentItem={cell.column}
- onItemSelect={handleColumnSelect}
- items={columns}
- placeholder="Select a column"
- />
-
-
- )
-}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/NumberInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/NumberInputSettingsBody.tsx
index 404c5cca1..b91350b72 100644
--- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/NumberInputSettingsBody.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/NumberInputSettingsBody.tsx
@@ -25,8 +25,8 @@ export const NumberInputSettingsBody = ({
onOptionsChange(removeUndefinedFields({ ...options, max }))
const handleStepChange = (step?: number) =>
onOptionsChange(removeUndefinedFields({ ...options, step }))
- const handleVariableChange = (variable: Variable) =>
- onOptionsChange({ ...options, variableId: variable.id })
+ const handleVariableChange = (variable?: Variable) =>
+ onOptionsChange({ ...options, variableId: variable?.id })
return (
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/PhoneNumberSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/PhoneNumberSettingsBody.tsx
index 2961c7ebd..3a9cdd257 100644
--- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/PhoneNumberSettingsBody.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/PhoneNumberSettingsBody.tsx
@@ -17,8 +17,8 @@ export const PhoneNumberSettingsBody = ({
onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } })
const handleButtonLabelChange = (button: string) =>
onOptionsChange({ ...options, labels: { ...options?.labels, button } })
- const handleVariableChange = (variable: Variable) =>
- onOptionsChange({ ...options, variableId: variable.id })
+ const handleVariableChange = (variable?: Variable) =>
+ onOptionsChange({ ...options, variableId: variable?.id })
return (
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/SetVariableSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/SetVariableSettingsBody.tsx
index 4b4f51a2a..9aed047cc 100644
--- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/SetVariableSettingsBody.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/SetVariableSettingsBody.tsx
@@ -13,8 +13,8 @@ export const SetVariableSettingsBody = ({
options,
onOptionsChange,
}: Props) => {
- const handleVariableChange = (variable: Variable) =>
- onOptionsChange({ ...options, variableId: variable.id })
+ const handleVariableChange = (variable?: Variable) =>
+ onOptionsChange({ ...options, variableId: variable?.id })
const handleExpressionChange = (expressionToEvaluate: string) =>
onOptionsChange({ ...options, expressionToEvaluate })
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/TextInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/TextInputSettingsBody.tsx
index 2a8dfb2fc..428f78203 100644
--- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/TextInputSettingsBody.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/TextInputSettingsBody.tsx
@@ -20,8 +20,8 @@ export const TextInputSettingsBody = ({
onOptionsChange({ ...options, labels: { ...options?.labels, button } })
const handleLongChange = (isLong: boolean) =>
onOptionsChange({ ...options, isLong })
- const handleVariableChange = (variable: Variable) =>
- onOptionsChange({ ...options, variableId: variable.id })
+ const handleVariableChange = (variable?: Variable) =>
+ onOptionsChange({ ...options, variableId: variable?.id })
return (
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/UrlInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/UrlInputSettingsBody.tsx
index 063a5b262..f2c34beee 100644
--- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/UrlInputSettingsBody.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/UrlInputSettingsBody.tsx
@@ -17,8 +17,8 @@ export const UrlInputSettingsBody = ({
onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } })
const handleButtonLabelChange = (button: string) =>
onOptionsChange({ ...options, labels: { ...options?.labels, button } })
- const handleVariableChange = (variable: Variable) =>
- onOptionsChange({ ...options, variableId: variable.id })
+ const handleVariableChange = (variable?: Variable) =>
+ onOptionsChange({ ...options, variableId: variable?.id })
return (
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/KeyValueInputs.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/KeyValueInputs.tsx
new file mode 100644
index 000000000..93d8f88ab
--- /dev/null
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/KeyValueInputs.tsx
@@ -0,0 +1,62 @@
+import { Stack, FormControl, FormLabel } from '@chakra-ui/react'
+import { InputWithVariableButton } from 'components/shared/InputWithVariableButton'
+import { TableListItemProps } from 'components/shared/TableList'
+import { KeyValue } from 'models'
+
+export const QueryParamsInputs = (props: TableListItemProps) => (
+
+)
+
+export const HeadersInputs = (props: TableListItemProps) => (
+
+)
+
+export const KeyValueInputs = ({
+ id,
+ item,
+ onItemChange,
+ keyPlaceholder,
+ valuePlaceholder,
+}: TableListItemProps & {
+ keyPlaceholder?: string
+ valuePlaceholder?: string
+}) => {
+ const handleKeyChange = (key: string) => {
+ if (key === item.key) return
+ onItemChange({ ...item, key })
+ }
+ const handleValueChange = (value: string) => {
+ if (value === item.value) return
+ onItemChange({ ...item, value })
+ }
+ return (
+
+
+ Key:
+
+
+
+ Value:
+
+
+
+ )
+}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/ResponseMappingInputs.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/ResponseMappingInputs.tsx
new file mode 100644
index 000000000..9074ca766
--- /dev/null
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/ResponseMappingInputs.tsx
@@ -0,0 +1,38 @@
+import { Stack, FormControl, FormLabel } from '@chakra-ui/react'
+import { SearchableDropdown } from 'components/shared/SearchableDropdown'
+import { TableListItemProps } from 'components/shared/TableList'
+import { VariableSearchInput } from 'components/shared/VariableSearchInput'
+import { Variable, ResponseVariableMapping } from 'models'
+
+export const DataVariableInputs = ({
+ item,
+ onItemChange,
+ dataItems,
+}: TableListItemProps & { dataItems: string[] }) => {
+ const handleBodyPathChange = (bodyPath: string) =>
+ onItemChange({ ...item, bodyPath })
+ const handleVariableChange = (variable?: Variable) =>
+ onItemChange({ ...item, variableId: variable?.id })
+
+ return (
+
+
+ Data:
+
+
+
+ Set variable:
+
+
+
+ )
+}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/VariableForTestInputs.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/VariableForTestInputs.tsx
new file mode 100644
index 000000000..2d11c0dbb
--- /dev/null
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/VariableForTestInputs.tsx
@@ -0,0 +1,39 @@
+import { Stack, FormControl, FormLabel } from '@chakra-ui/react'
+import { DebouncedInput } from 'components/shared/DebouncedInput'
+import { TableListItemProps } from 'components/shared/TableList'
+import { VariableSearchInput } from 'components/shared/VariableSearchInput'
+import { VariableForTest, Variable } from 'models'
+
+export const VariableForTestInputs = ({
+ id,
+ item,
+ onItemChange,
+}: TableListItemProps) => {
+ const handleVariableSelect = (variable?: Variable) =>
+ onItemChange({ ...item, variableId: variable?.id })
+ const handleValueChange = (value: string) => {
+ if (value === item.value) return
+ onItemChange({ ...item, value })
+ }
+ return (
+
+
+ Variable name:
+
+
+
+ Test value:
+
+
+
+ )
+}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/WebhookSettings.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/WebhookSettings.tsx
new file mode 100644
index 000000000..1e48f4673
--- /dev/null
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/WebhookSettings.tsx
@@ -0,0 +1,204 @@
+import React, { useEffect, useMemo, useState } from 'react'
+import {
+ Accordion,
+ AccordionButton,
+ AccordionIcon,
+ AccordionItem,
+ AccordionPanel,
+ Button,
+ Flex,
+ Stack,
+ useToast,
+} from '@chakra-ui/react'
+import { InputWithVariableButton } from 'components/shared/InputWithVariableButton'
+import { useTypebot } from 'contexts/TypebotContext'
+import {
+ HttpMethod,
+ KeyValue,
+ Table,
+ WebhookOptions,
+ VariableForTest,
+ Webhook,
+ ResponseVariableMapping,
+} from 'models'
+import { DropdownList } from 'components/shared/DropdownList'
+import { generate } from 'short-uuid'
+import { TableList, TableListItemProps } from 'components/shared/TableList'
+import { CodeEditor } from 'components/shared/CodeEditor'
+import {
+ convertVariableForTestToVariables,
+ executeWebhook,
+ getDeepKeys,
+} from 'services/integrations'
+import { HeadersInputs, QueryParamsInputs } from './KeyValueInputs'
+import { VariableForTestInputs } from './VariableForTestInputs'
+import { DataVariableInputs } from './ResponseMappingInputs'
+
+type Props = {
+ options?: WebhookOptions
+ webhook?: Webhook
+ onOptionsChange: (options: WebhookOptions) => void
+ onWebhookChange: (webhook: Partial) => void
+}
+
+export const WebhookSettings = ({
+ options,
+ webhook,
+ onOptionsChange,
+ onWebhookChange,
+}: Props) => {
+ const { createWebhook, typebot, save } = useTypebot()
+ const [testResponse, setTestResponse] = useState()
+ const [responseKeys, setResponseKeys] = useState([])
+
+ const toast = useToast({
+ position: 'top-right',
+ status: 'error',
+ })
+
+ useEffect(() => {
+ if (options?.webhookId) return
+ const webhookId = generate()
+ createWebhook({ id: webhookId })
+ onOptionsChange({ ...options, webhookId })
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ const handleUrlChange = (url?: string) => onWebhookChange({ url })
+
+ const handleMethodChange = (method?: HttpMethod) =>
+ onWebhookChange({ method })
+
+ const handleQueryParamsChange = (queryParams: Table) =>
+ onWebhookChange({ queryParams })
+
+ const handleHeadersChange = (headers: Table) =>
+ onWebhookChange({ headers })
+
+ const handleBodyChange = (body: string) => onWebhookChange({ body })
+
+ const handleVariablesChange = (variablesForTest: Table) =>
+ onOptionsChange({ ...options, variablesForTest })
+
+ const handleResponseMappingChange = (
+ responseVariableMapping: Table
+ ) => onOptionsChange({ ...options, responseVariableMapping })
+
+ const handleTestRequestClick = async () => {
+ if (!typebot || !webhook) return
+ await save()
+ const { data, error } = await executeWebhook(
+ typebot.id,
+ webhook.id,
+ convertVariableForTestToVariables(
+ options?.variablesForTest,
+ typebot.variables
+ )
+ )
+ if (error) return toast({ title: error.name, description: error.message })
+ setTestResponse(JSON.stringify(data, undefined, 2))
+ setResponseKeys(getDeepKeys(data))
+ }
+
+ const ResponseMappingInputs = useMemo(
+ () => (props: TableListItemProps) =>
+ ,
+ [responseKeys]
+ )
+
+ return (
+
+
+
+
+ currentItem={webhook?.method ?? HttpMethod.GET}
+ onItemSelect={handleMethodChange}
+ items={Object.values(HttpMethod)}
+ />
+
+
+
+
+
+
+ Query params
+
+
+
+
+ initialItems={webhook?.queryParams}
+ onItemsChange={handleQueryParamsChange}
+ Item={QueryParamsInputs}
+ addLabel="Add a param"
+ />
+
+
+
+
+ Headers
+
+
+
+
+ initialItems={webhook?.headers}
+ onItemsChange={handleHeadersChange}
+ Item={HeadersInputs}
+ addLabel="Add a value"
+ />
+
+
+
+
+ Body
+
+
+
+
+
+
+
+
+ Variable values for test
+
+
+
+
+ initialItems={options?.variablesForTest}
+ onItemsChange={handleVariablesChange}
+ Item={VariableForTestInputs}
+ addLabel="Add an entry"
+ />
+
+
+
+
+ {testResponse && }
+ {(testResponse || options?.responseVariableMapping) && (
+
+
+
+ Save in variables
+
+
+
+
+ initialItems={options?.responseVariableMapping}
+ onItemsChange={handleResponseMappingChange}
+ Item={ResponseMappingInputs}
+ />
+
+
+
+ )}
+
+ )
+}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/index.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/index.tsx
new file mode 100644
index 000000000..0a2775b8f
--- /dev/null
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/WebhookSettings/index.tsx
@@ -0,0 +1 @@
+export { WebhookSettings } from './WebhookSettings'
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx
index 489081a7c..f495b8cfd 100644
--- a/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx
@@ -13,7 +13,7 @@ import { StepIcon } from 'components/board/StepTypesList/StepIcon'
import { isBubbleStep, isTextBubbleStep } from 'utils'
import { Coordinates } from '@dnd-kit/core/dist/types'
import { TextEditor } from './TextEditor/TextEditor'
-import { StepNodeContent } from './StepNodeContent'
+import { StepNodeContent } from './StepNodeContent/StepNodeContent'
import { useTypebot } from 'contexts/TypebotContext'
import { ContextMenu } from 'components/shared/ContextMenu'
import { SettingsPopoverContent } from './SettingsPopoverContent'
@@ -164,6 +164,7 @@ export const StepNode = ({
cursor={'pointer'}
bgColor="white"
align="flex-start"
+ w="full"
>
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent.tsx
deleted file mode 100644
index 0739718f0..000000000
--- a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent.tsx
+++ /dev/null
@@ -1,238 +0,0 @@
-import { Box, Flex, HStack, Image, Stack, Tag, Text } from '@chakra-ui/react'
-import { useTypebot } from 'contexts/TypebotContext'
-import {
- Step,
- StartStep,
- BubbleStepType,
- InputStepType,
- LogicStepType,
- SetVariableStep,
- ConditionStep,
- IntegrationStepType,
- VideoBubbleStep,
- VideoBubbleContentType,
-} from 'models'
-import { ChoiceItemsList } from './ChoiceInputStepNode/ChoiceItemsList'
-import { SourceEndpoint } from './SourceEndpoint'
-
-type Props = {
- step: Step | StartStep
- isConnectable?: boolean
-}
-export const StepNodeContent = ({ step }: Props) => {
- switch (step.type) {
- case BubbleStepType.TEXT: {
- return (
- Click to edit... | | | | | |
`
- : step.content.html,
- }}
- />
- )
- }
- case BubbleStepType.IMAGE: {
- return !step.content?.url ? (
- Click to edit...
- ) : (
-
-
-
- )
- }
- case BubbleStepType.VIDEO: {
- return
- }
- case InputStepType.TEXT: {
- return (
-
- {step.options?.labels?.placeholder ?? 'Type your answer...'}
-
- )
- }
- case InputStepType.NUMBER: {
- return (
-
- {step.options?.labels?.placeholder ?? 'Type your answer...'}
-
- )
- }
- case InputStepType.EMAIL: {
- return (
-
- {step.options?.labels?.placeholder ?? 'Type your email...'}
-
- )
- }
- case InputStepType.URL: {
- return (
-
- {step.options?.labels?.placeholder ?? 'Type your URL...'}
-
- )
- }
- case InputStepType.DATE: {
- return (
-
- {step.options?.labels?.from ?? 'Pick a date...'}
-
- )
- }
- case InputStepType.PHONE: {
- return (
-
- {step.options?.labels?.placeholder ?? 'Your phone number...'}
-
- )
- }
- case InputStepType.CHOICE: {
- return
- }
- case LogicStepType.SET_VARIABLE: {
- return
- }
- case LogicStepType.CONDITION: {
- return
- }
- case LogicStepType.REDIRECT: {
- if (!step.options) return Configure...
- return Redirect to {step.options?.url}
- }
- case IntegrationStepType.GOOGLE_SHEETS: {
- if (!step.options) return Configure...
- return {step.options?.action}
- }
- case IntegrationStepType.GOOGLE_ANALYTICS: {
- if (!step.options || !step.options.action)
- return Configure...
- return Track "{step.options?.action}"
- }
- case 'start': {
- return {step.label}
- }
- default: {
- return No input
- }
- }
-}
-
-const SetVariableNodeContent = ({ step }: { step: SetVariableStep }) => {
- const { typebot } = useTypebot()
- const variableName =
- typebot?.variables.byId[step.options?.variableId ?? '']?.name ?? ''
- const expression = step.options?.expressionToEvaluate ?? ''
- return (
-
- {variableName === '' && expression === ''
- ? 'Click to edit...'
- : `${variableName} = ${expression}`}
-
- )
-}
-
-const ConditionNodeContent = ({ step }: { step: ConditionStep }) => {
- const { typebot } = useTypebot()
- return (
-
-
- {step.options?.comparisons.allIds.map((comparisonId, idx) => {
- const comparison = step.options?.comparisons.byId[comparisonId]
- const variable = typebot?.variables.byId[comparison?.variableId ?? '']
- return (
-
- {idx > 0 && {step.options?.logicalOperator ?? ''}}
- {variable?.name && (
- {variable.name}
- )}
- {comparison.comparisonOperator && (
- {comparison?.comparisonOperator}
- )}
- {comparison?.value && (
- {comparison.value}
- )}
-
- )
- })}
-
-
-
-
- )
-}
-
-const VideoStepNodeContent = ({ step }: { step: VideoBubbleStep }) => {
- if (!step.content?.url || !step.content.type)
- return Click to edit...
- switch (step.content.type) {
- case VideoBubbleContentType.URL:
- return (
-
-
-
- )
- case VideoBubbleContentType.VIMEO:
- case VideoBubbleContentType.YOUTUBE: {
- const baseUrl =
- step.content.type === VideoBubbleContentType.VIMEO
- ? 'https://player.vimeo.com/video'
- : 'https://www.youtube.com/embed'
- return (
-
-
-
- )
- }
- }
-}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/ConditionNodeContent.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/ConditionNodeContent.tsx
new file mode 100644
index 000000000..5c9ef4bb4
--- /dev/null
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/ConditionNodeContent.tsx
@@ -0,0 +1,52 @@
+import { Flex, Stack, HStack, Tag, Text } from '@chakra-ui/react'
+import { useTypebot } from 'contexts/TypebotContext'
+import { ConditionStep } from 'models'
+import { SourceEndpoint } from '../SourceEndpoint'
+
+export const ConditionNodeContent = ({ step }: { step: ConditionStep }) => {
+ const { typebot } = useTypebot()
+ return (
+
+
+ {step.options?.comparisons.allIds.map((comparisonId, idx) => {
+ const comparison = step.options?.comparisons.byId[comparisonId]
+ const variable = typebot?.variables.byId[comparison?.variableId ?? '']
+ return (
+
+ {idx > 0 && {step.options?.logicalOperator ?? ''}}
+ {variable?.name && (
+ {variable.name}
+ )}
+ {comparison.comparisonOperator && (
+ {comparison?.comparisonOperator}
+ )}
+ {comparison?.value && (
+ {comparison.value}
+ )}
+
+ )
+ })}
+
+
+
+
+ )
+}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/SetVariableNodeContent.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/SetVariableNodeContent.tsx
new file mode 100644
index 000000000..e42460f54
--- /dev/null
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/SetVariableNodeContent.tsx
@@ -0,0 +1,17 @@
+import { Text } from '@chakra-ui/react'
+import { useTypebot } from 'contexts/TypebotContext'
+import { SetVariableStep } from 'models'
+
+export const SetVariableNodeContent = ({ step }: { step: SetVariableStep }) => {
+ const { typebot } = useTypebot()
+ const variableName =
+ typebot?.variables.byId[step.options?.variableId ?? '']?.name ?? ''
+ const expression = step.options?.expressionToEvaluate ?? ''
+ return (
+
+ {variableName === '' && expression === ''
+ ? 'Click to edit...'
+ : `${variableName} = ${expression}`}
+
+ )
+}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/StepNodeContent.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/StepNodeContent.tsx
new file mode 100644
index 000000000..0c9842682
--- /dev/null
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/StepNodeContent.tsx
@@ -0,0 +1,128 @@
+import { Box, Flex, Image, Text } from '@chakra-ui/react'
+import {
+ Step,
+ StartStep,
+ BubbleStepType,
+ InputStepType,
+ LogicStepType,
+ IntegrationStepType,
+} from 'models'
+import { ChoiceItemsList } from '../ChoiceInputStepNode/ChoiceItemsList'
+import { ConditionNodeContent } from './ConditionNodeContent'
+import { SetVariableNodeContent } from './SetVariableNodeContent'
+import { VideoStepNodeContent } from './VideoStepNodeContent'
+import { WebhookContent } from './WebhookContent'
+
+type Props = {
+ step: Step | StartStep
+ isConnectable?: boolean
+}
+export const StepNodeContent = ({ step }: Props) => {
+ switch (step.type) {
+ case BubbleStepType.TEXT: {
+ return (
+ Click to edit...`
+ : step.content.html,
+ }}
+ />
+ )
+ }
+ case BubbleStepType.IMAGE: {
+ return !step.content?.url ? (
+ Click to edit...
+ ) : (
+
+
+
+ )
+ }
+ case BubbleStepType.VIDEO: {
+ return
+ }
+ case InputStepType.TEXT: {
+ return (
+
+ {step.options?.labels?.placeholder ?? 'Type your answer...'}
+
+ )
+ }
+ case InputStepType.NUMBER: {
+ return (
+
+ {step.options?.labels?.placeholder ?? 'Type your answer...'}
+
+ )
+ }
+ case InputStepType.EMAIL: {
+ return (
+
+ {step.options?.labels?.placeholder ?? 'Type your email...'}
+
+ )
+ }
+ case InputStepType.URL: {
+ return (
+
+ {step.options?.labels?.placeholder ?? 'Type your URL...'}
+
+ )
+ }
+ case InputStepType.DATE: {
+ return (
+
+ {step.options?.labels?.from ?? 'Pick a date...'}
+
+ )
+ }
+ case InputStepType.PHONE: {
+ return (
+
+ {step.options?.labels?.placeholder ?? 'Your phone number...'}
+
+ )
+ }
+ case InputStepType.CHOICE: {
+ return
+ }
+ case LogicStepType.SET_VARIABLE: {
+ return
+ }
+ case LogicStepType.CONDITION: {
+ return
+ }
+ case LogicStepType.REDIRECT: {
+ if (!step.options) return Configure...
+ return Redirect to {step.options?.url}
+ }
+ case IntegrationStepType.GOOGLE_SHEETS: {
+ if (!step.options) return Configure...
+ return {step.options?.action}
+ }
+ case IntegrationStepType.GOOGLE_ANALYTICS: {
+ if (!step.options || !step.options.action)
+ return Configure...
+ return Track "{step.options?.action}"
+ }
+ case IntegrationStepType.WEBHOOK: {
+ return
+ }
+ case 'start': {
+ return {step.label}
+ }
+ default: {
+ return No input
+ }
+ }
+}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/VideoStepNodeContent.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/VideoStepNodeContent.tsx
new file mode 100644
index 000000000..4654c4e70
--- /dev/null
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/VideoStepNodeContent.tsx
@@ -0,0 +1,51 @@
+import { Box, Text } from '@chakra-ui/react'
+import { VideoBubbleStep, VideoBubbleContentType } from 'models'
+
+export const VideoStepNodeContent = ({ step }: { step: VideoBubbleStep }) => {
+ if (!step.content?.url || !step.content.type)
+ return Click to edit...
+ switch (step.content.type) {
+ case VideoBubbleContentType.URL:
+ return (
+
+
+
+ )
+ case VideoBubbleContentType.VIMEO:
+ case VideoBubbleContentType.YOUTUBE: {
+ const baseUrl =
+ step.content.type === VideoBubbleContentType.VIMEO
+ ? 'https://player.vimeo.com/video'
+ : 'https://www.youtube.com/embed'
+ return (
+
+
+
+ )
+ }
+ }
+}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/WebhookContent.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/WebhookContent.tsx
new file mode 100644
index 000000000..b025d28d3
--- /dev/null
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/WebhookContent.tsx
@@ -0,0 +1,22 @@
+import { Text } from '@chakra-ui/react'
+import { useTypebot } from 'contexts/TypebotContext'
+import { WebhookStep } from 'models'
+import { useMemo } from 'react'
+
+type Props = {
+ step: WebhookStep
+}
+
+export const WebhookContent = ({ step }: Props) => {
+ const { typebot } = useTypebot()
+ const webhook = useMemo(
+ () => typebot?.webhooks.byId[step.options?.webhookId ?? ''],
+ [step.options?.webhookId, typebot?.webhooks.byId]
+ )
+ if (!webhook?.url) return Configure...
+ return (
+
+ {webhook.method} {webhook.url}
+
+ )
+}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/index.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/index.tsx
new file mode 100644
index 000000000..d9ebd7e10
--- /dev/null
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent/index.tsx
@@ -0,0 +1 @@
+export { StepNodeContent } from './StepNodeContent'
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeOverlay.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeOverlay.tsx
index c25c3bc85..7532c4b96 100644
--- a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeOverlay.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeOverlay.tsx
@@ -1,7 +1,7 @@
import { StackProps, HStack } from '@chakra-ui/react'
import { StartStep, Step } from 'models'
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
-import { StepNodeContent } from './StepNodeContent'
+import { StepNodeContent } from './StepNodeContent/StepNodeContent'
export const StepNodeOverlay = ({
step,
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/TextEditor/TextEditor.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/TextEditor/TextEditor.tsx
index b349087ed..bd4126d93 100644
--- a/apps/builder/components/board/graph/BlockNode/StepNode/TextEditor/TextEditor.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/TextEditor/TextEditor.tsx
@@ -94,9 +94,9 @@ export const TextEditor = ({
e.stopPropagation()
}
- const handleVariableSelected = (variable: Variable) => {
+ const handleVariableSelected = (variable?: Variable) => {
setIsVariableDropdownOpen(false)
- if (!rememberedSelection.current) return
+ if (!rememberedSelection.current || !variable) return
Transforms.select(editor, rememberedSelection.current)
Transforms.insertText(editor, '{{' + variable.name + '}}')
ReactEditor.focus(editor as unknown as ReactEditor)
diff --git a/apps/builder/components/shared/CodeEditor.tsx b/apps/builder/components/shared/CodeEditor.tsx
new file mode 100644
index 000000000..0145de820
--- /dev/null
+++ b/apps/builder/components/shared/CodeEditor.tsx
@@ -0,0 +1,58 @@
+import { Box, BoxProps } from '@chakra-ui/react'
+import { EditorState, EditorView, basicSetup } from '@codemirror/basic-setup'
+import { json } from '@codemirror/lang-json'
+import { useEffect, useRef } from 'react'
+
+type Props = {
+ value: string
+ onChange?: (value: string) => void
+ isReadOnly?: boolean
+}
+export const CodeEditor = ({
+ value,
+ onChange,
+ isReadOnly = false,
+ ...props
+}: Props & Omit) => {
+ const editorContainer = useRef(null)
+ const editorView = useRef(null)
+
+ useEffect(() => {
+ if (!editorView.current || !isReadOnly) return
+ editorView.current.dispatch({
+ changes: { from: 0, insert: value },
+ })
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [value])
+
+ useEffect(() => {
+ if (!editorContainer.current) return
+ const updateListenerExtension = EditorView.updateListener.of((update) => {
+ if (update.docChanged && onChange)
+ onChange(update.state.doc.toJSON().join(' '))
+ })
+ const editor = new EditorView({
+ state: EditorState.create({
+ extensions: [
+ updateListenerExtension,
+ basicSetup,
+ json(),
+ EditorState.readOnly.of(isReadOnly),
+ ],
+ }),
+ parent: editorContainer.current,
+ })
+ editor.dispatch({
+ changes: { from: 0, insert: value },
+ })
+ editorView.current = editor
+ return () => {
+ editor.destroy()
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ return (
+
+ )
+}
diff --git a/apps/builder/components/shared/InputWithVariableButton.tsx b/apps/builder/components/shared/InputWithVariableButton.tsx
index f1f8723aa..4b61cfe11 100644
--- a/apps/builder/components/shared/InputWithVariableButton.tsx
+++ b/apps/builder/components/shared/InputWithVariableButton.tsx
@@ -1,4 +1,5 @@
import {
+ Flex,
HStack,
IconButton,
Input,
@@ -6,6 +7,7 @@ import {
Popover,
PopoverContent,
PopoverTrigger,
+ Tooltip,
} from '@chakra-ui/react'
import { UserIcon } from 'assets/icons'
import { Variable } from 'models'
@@ -29,12 +31,12 @@ export const InputWithVariableButton = ({
const [carretPosition, setCarretPosition] = useState(0)
useEffect(() => {
- onChange(debouncedValue)
+ if (debouncedValue !== initialValue) onChange(debouncedValue)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedValue])
- const handleVariableSelected = (variable: Variable) => {
- if (!inputRef.current) return
+ const handleVariableSelected = (variable?: Variable) => {
+ if (!inputRef.current || !variable) return
const cursorPosition = carretPosition
const textBeforeCursorPosition = inputRef.current.value.substring(
0,
@@ -67,7 +69,7 @@ export const InputWithVariableButton = ({
setValue(e.target.value)
return (
-
+
- }
- pos="relative"
- ml="2"
- />
+
+
+ }
+ pos="relative"
+ />
+
+
void
+ onValueChange?: (value: string) => void
} & InputProps
+
export const SearchableDropdown = ({
selectedItem,
items,
- onSelectItem,
+ onValueChange,
...inputProps
}: Props) => {
const { onOpen, onClose, isOpen } = useDisclosure()
- const [inputValue, setInputValue] = useState(selectedItem)
+ const [inputValue, setInputValue] = useState(selectedItem ?? '')
+ const [debouncedInputValue] = useDebounce(inputValue, 200)
const [filteredItems, setFilteredItems] = useState([
...items
.filter((item) =>
@@ -52,6 +55,13 @@ export const SearchableDropdown = ({
handler: onClose,
})
+ useEffect(() => {
+ onValueChange &&
+ debouncedInputValue !== selectedItem &&
+ onValueChange(debouncedInputValue)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [debouncedInputValue])
+
const onInputChange = (e: ChangeEvent) => {
setInputValue(e.target.value)
if (e.target.value === '') {
@@ -69,7 +79,6 @@ export const SearchableDropdown = ({
const handleItemClick = (item: string) => () => {
setInputValue(item)
- onSelectItem(item)
onClose()
}
diff --git a/apps/builder/components/shared/TableList.tsx b/apps/builder/components/shared/TableList.tsx
new file mode 100644
index 000000000..e9a38e71a
--- /dev/null
+++ b/apps/builder/components/shared/TableList.tsx
@@ -0,0 +1,114 @@
+import { Box, Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react'
+import { TrashIcon, PlusIcon } from 'assets/icons'
+import { Draft } from 'immer'
+import { Table } from 'models'
+import React, { useEffect, useState } from 'react'
+import { generate } from 'short-uuid'
+import { useImmer } from 'use-immer'
+
+export type TableListItemProps = {
+ id: string
+ item: T
+ onItemChange: (item: T) => void
+}
+
+type Props = {
+ initialItems?: Table
+ onItemsChange: (items: Table) => void
+ addLabel?: string
+ Item: (props: TableListItemProps) => JSX.Element
+ ComponentBetweenItems?: (props: unknown) => JSX.Element
+}
+
+export const TableList = ({
+ initialItems,
+ onItemsChange,
+ addLabel = 'Add',
+ Item,
+ ComponentBetweenItems = () => <>>,
+}: Props) => {
+ const [items, setItems] = useImmer(initialItems ?? { byId: {}, allIds: [] })
+ const [showDeleteId, setShowDeleteId] = useState()
+
+ useEffect(() => {
+ if (items.allIds.length === 0) createItem()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ useEffect(() => {
+ onItemsChange(items)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [items])
+
+ const createItem = () => {
+ setItems((items) => {
+ const id = generate()
+ items.byId[id] = { id } as unknown as Draft
+ items.allIds.push(id)
+ })
+ }
+
+ const updateItem = (itemId: string, updates: Partial) =>
+ setItems((items) => {
+ items.byId[itemId] = {
+ ...items.byId[itemId],
+ ...updates,
+ }
+ })
+
+ const deleteItem = (itemId: string) => () => {
+ setItems((items) => {
+ delete items.byId[itemId]
+ const index = items.allIds.indexOf(itemId)
+ if (index !== -1) items.allIds.splice(index, 1)
+ })
+ }
+
+ const handleMouseEnter = (itemId: string) => () => setShowDeleteId(itemId)
+
+ const handleCellChange = (itemId: string) => (item: T) =>
+ updateItem(itemId, item)
+
+ const handleMouseLeave = () => setShowDeleteId(undefined)
+
+ return (
+
+ {items.allIds.map((itemId, idx) => (
+
+ {idx !== 0 && }
+
+
+
+ }
+ aria-label="Remove cell"
+ onClick={deleteItem(itemId)}
+ pos="absolute"
+ left="-15px"
+ top="-15px"
+ size="sm"
+ shadow="md"
+ />
+
+
+
+ ))}
+ }
+ onClick={createItem}
+ flexShrink={0}
+ colorScheme="blue"
+ >
+ {addLabel}
+
+
+ )
+}
diff --git a/apps/builder/components/shared/VariableSearchInput.tsx b/apps/builder/components/shared/VariableSearchInput.tsx
index 7f542c8b5..5ddea4c07 100644
--- a/apps/builder/components/shared/VariableSearchInput.tsx
+++ b/apps/builder/components/shared/VariableSearchInput.tsx
@@ -15,11 +15,14 @@ import { useTypebot } from 'contexts/TypebotContext'
import { Variable } from 'models'
import React, { useState, useRef, ChangeEvent, useMemo, useEffect } from 'react'
import { generate } from 'short-uuid'
+import { useDebounce } from 'use-debounce'
import { isDefined } from 'utils'
type Props = {
initialVariableId?: string
- onSelectVariable: (variable: Pick) => void
+ onSelectVariable: (
+ variable: Pick | undefined
+ ) => void
isDefaultOpen?: boolean
} & InputProps
@@ -39,6 +42,7 @@ export const VariableSearchInput = ({
const [inputValue, setInputValue] = useState(
typebot?.variables.byId[initialVariableId ?? '']?.name ?? ''
)
+ const [debouncedInputValue] = useDebounce(inputValue, 200)
const [filteredItems, setFilteredItems] = useState(variables)
const dropdownRef = useRef(null)
const inputRef = useRef(null)
@@ -53,11 +57,18 @@ export const VariableSearchInput = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
+ useEffect(() => {
+ const variable = variables.find((v) => v.name === debouncedInputValue)
+ if (variable) onSelectVariable(variable)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [debouncedInputValue])
+
const onInputChange = (e: ChangeEvent) => {
setInputValue(e.target.value)
onOpen()
if (e.target.value === '') {
setFilteredItems([...variables.slice(0, 50)])
+ onSelectVariable(undefined)
return
}
setFilteredItems([
diff --git a/apps/builder/components/theme/GeneralContent/FontSelector/FontSelector.tsx b/apps/builder/components/theme/GeneralContent/FontSelector/FontSelector.tsx
index 2419b3ce2..90efbaafa 100644
--- a/apps/builder/components/theme/GeneralContent/FontSelector/FontSelector.tsx
+++ b/apps/builder/components/theme/GeneralContent/FontSelector/FontSelector.tsx
@@ -27,18 +27,19 @@ export const FontSelector = ({
)
}
+ const handleFontSelected = (nextFont: string) => {
+ if (nextFont == currentFont) return
+ setCurrentFont(nextFont)
+ onSelectFont(nextFont)
+ }
+
return (
Font
{
- if (nextFont !== currentFont) {
- setCurrentFont(nextFont)
- onSelectFont(nextFont)
- }
- }}
+ onValueChange={handleFontSelected}
/>
)
diff --git a/apps/builder/contexts/TypebotContext/TypebotContext.tsx b/apps/builder/contexts/TypebotContext/TypebotContext.tsx
index 7b185ecf1..1abac7612 100644
--- a/apps/builder/contexts/TypebotContext/TypebotContext.tsx
+++ b/apps/builder/contexts/TypebotContext/TypebotContext.tsx
@@ -29,6 +29,7 @@ import { stepsAction, StepsActions } from './actions/steps'
import { choiceItemsAction, ChoiceItemsActions } from './actions/choiceItems'
import { variablesAction, VariablesActions } from './actions/variables'
import { edgesAction, EdgesActions } from './actions/edges'
+import { webhooksAction, WebhooksAction } from './actions/webhooks'
type UpdateTypebotPayload = Partial<{
theme: Theme
@@ -52,7 +53,8 @@ const typebotContext = createContext<
StepsActions &
ChoiceItemsActions &
VariablesActions &
- EdgesActions
+ EdgesActions &
+ WebhooksAction
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
>({})
@@ -78,9 +80,11 @@ export const TypebotContext = ({
description: error.message,
}),
})
+
const [localTypebot, setLocalTypebot] = useImmer(
undefined
)
+
const [localPublishedTypebot, setLocalPublishedTypebot] =
useState()
const [isSavingLoading, setIsSavingLoading] = useState(false)
@@ -214,6 +218,7 @@ export const TypebotContext = ({
...choiceItemsAction(setLocalTypebot as Updater),
...variablesAction(setLocalTypebot as Updater),
...edgesAction(setLocalTypebot as Updater),
+ ...webhooksAction(setLocalTypebot as Updater),
}}
>
{children}
diff --git a/apps/builder/contexts/TypebotContext/actions/steps.ts b/apps/builder/contexts/TypebotContext/actions/steps.ts
index 7ce7a848b..61f39d902 100644
--- a/apps/builder/contexts/TypebotContext/actions/steps.ts
+++ b/apps/builder/contexts/TypebotContext/actions/steps.ts
@@ -10,8 +10,9 @@ import { Updater } from 'use-immer'
import { removeEmptyBlocks } from './blocks'
import { WritableDraft } from 'immer/dist/types/types-external'
import { createChoiceItemDraft, deleteChoiceItemDraft } from './choiceItems'
-import { isChoiceInput } from 'utils'
+import { isChoiceInput, isWebhookStep } from 'utils'
import { deleteEdgeDraft } from './edges'
+import { deleteWebhookDraft } from './webhooks'
export type StepsActions = {
createStep: (
@@ -51,6 +52,8 @@ export const stepsAction = (setTypebot: Updater): StepsActions => ({
setTypebot((typebot) => {
const step = typebot.steps.byId[stepId]
if (isChoiceInput(step)) deleteChoiceItemsInsideStep(typebot, step)
+ if (isWebhookStep(step))
+ deleteWebhookDraft(step.options?.webhookId)(typebot)
deleteAssociatedEdges(typebot, stepId)
removeStepIdFromBlock(typebot, stepId)
deleteStepDraft(typebot, stepId)
diff --git a/apps/builder/contexts/TypebotContext/actions/variables.ts b/apps/builder/contexts/TypebotContext/actions/variables.ts
index f4e2fceba..c56996963 100644
--- a/apps/builder/contexts/TypebotContext/actions/variables.ts
+++ b/apps/builder/contexts/TypebotContext/actions/variables.ts
@@ -1,10 +1,9 @@
import { Typebot, Variable } from 'models'
import { Updater } from 'use-immer'
import { WritableDraft } from 'immer/dist/types/types-external'
-import { generate } from 'short-uuid'
export type VariablesActions = {
- createVariable: (variable: Omit | Variable) => void
+ createVariable: (variable: Variable) => void
updateVariable: (
variableId: string,
updates: Partial>
@@ -15,10 +14,10 @@ export type VariablesActions = {
export const variablesAction = (
setTypebot: Updater
): VariablesActions => ({
- createVariable: (variable: Omit | Variable) => {
+ createVariable: (newVariable: Variable) => {
setTypebot((typebot) => {
- const id = createVariableDraft(typebot, variable)
- return id
+ typebot.variables.byId[newVariable.id] = newVariable
+ typebot.variables.allIds.push(newVariable.id)
})
},
updateVariable: (
@@ -46,15 +45,3 @@ export const deleteVariableDraft = (
const index = typebot.variables.allIds.indexOf(variableId)
if (index !== -1) typebot.variables.allIds.splice(index, 1)
}
-
-export const createVariableDraft = (
- typebot: WritableDraft,
- variable: Omit | Variable
-) => {
- const newVariable = {
- ...variable,
- id: 'id' in variable ? variable.id : generate(),
- }
- typebot.variables.byId[newVariable.id] = newVariable
- typebot.variables.allIds.push(newVariable.id)
-}
diff --git a/apps/builder/contexts/TypebotContext/actions/webhooks.ts b/apps/builder/contexts/TypebotContext/actions/webhooks.ts
new file mode 100644
index 000000000..efa164a41
--- /dev/null
+++ b/apps/builder/contexts/TypebotContext/actions/webhooks.ts
@@ -0,0 +1,41 @@
+import { Typebot, Webhook } from 'models'
+import { Updater } from 'use-immer'
+import { WritableDraft } from 'immer/dist/internal'
+
+export type WebhooksAction = {
+ createWebhook: (webook: Webhook) => void
+ updateWebhook: (
+ webhookId: string,
+ updates: Partial>
+ ) => void
+ deleteWebhook: (variableId: string) => void
+}
+
+export const webhooksAction = (
+ setTypebot: Updater
+): WebhooksAction => ({
+ createWebhook: (newWebhook: Webhook) => {
+ setTypebot((typebot) => {
+ typebot.webhooks.byId[newWebhook.id] = newWebhook
+ typebot.webhooks.allIds.push(newWebhook.id)
+ })
+ },
+ updateWebhook: (webhookId: string, updates: Partial>) =>
+ setTypebot((typebot) => {
+ typebot.webhooks.byId[webhookId] = {
+ ...typebot.webhooks.byId[webhookId],
+ ...updates,
+ }
+ }),
+ deleteWebhook: (webhookId: string) => {
+ setTypebot(deleteWebhookDraft(webhookId))
+ },
+})
+
+export const deleteWebhookDraft =
+ (webhookId?: string) => (typebot: WritableDraft) => {
+ if (!webhookId) return
+ delete typebot.webhooks.byId[webhookId]
+ const index = typebot.webhooks.allIds.indexOf(webhookId)
+ if (index !== -1) typebot.webhooks.allIds.splice(index, 1)
+ }
diff --git a/apps/builder/cypress/fixtures/typebots/integrations/googleSheets.json b/apps/builder/cypress/fixtures/typebots/integrations/googleSheets.json
index 2152b438d..a32e52f59 100644
--- a/apps/builder/cypress/fixtures/typebots/integrations/googleSheets.json
+++ b/apps/builder/cypress/fixtures/typebots/integrations/googleSheets.json
@@ -6,6 +6,7 @@
"ownerId": "ckylsz8yy0335z31amvq0jwtt",
"publishedTypebotId": null,
"folderId": null,
+ "webhooks": { "byId": {}, "allIds": [] },
"blocks": {
"byId": {
"j24wz82YG3rjXMgrmCiTLy": {
diff --git a/apps/builder/cypress/fixtures/typebots/integrations/googleSheetsGet.json b/apps/builder/cypress/fixtures/typebots/integrations/googleSheetsGet.json
index 0bfa40051..9ca1e2cc1 100644
--- a/apps/builder/cypress/fixtures/typebots/integrations/googleSheetsGet.json
+++ b/apps/builder/cypress/fixtures/typebots/integrations/googleSheetsGet.json
@@ -6,6 +6,7 @@
"ownerId": "ckyltekzq0533z31ad8opmacz",
"publishedTypebotId": null,
"folderId": null,
+ "webhooks": { "byId": {}, "allIds": [] },
"blocks": {
"byId": {
"kPupUcEn7TcBGKHUpgK2Q5": {
diff --git a/apps/builder/cypress/fixtures/typebots/integrations/webhook.json b/apps/builder/cypress/fixtures/typebots/integrations/webhook.json
new file mode 100644
index 000000000..d7ac6c320
--- /dev/null
+++ b/apps/builder/cypress/fixtures/typebots/integrations/webhook.json
@@ -0,0 +1,150 @@
+{
+ "id": "typebot4",
+ "createdAt": "2022-01-21T07:55:14.727Z",
+ "updatedAt": "2022-01-21T07:55:14.727Z",
+ "name": "My typebot",
+ "ownerId": "user2",
+ "publishedTypebotId": null,
+ "folderId": null,
+ "webhooks": { "byId": {}, "allIds": [] },
+ "blocks": {
+ "byId": {
+ "3kH2sUjVThQDWmqdoKnGk5": {
+ "id": "3kH2sUjVThQDWmqdoKnGk5",
+ "title": "Start",
+ "stepIds": ["oxTsU2C1RX5QHuyY8qjHAM"],
+ "graphCoordinates": { "x": 42, "y": 13 }
+ },
+ "b9mSgu7RKmK4xuiTVQP5Me8": {
+ "id": "b9mSgu7RKmK4xuiTVQP5Me8",
+ "title": "Block #3",
+ "stepIds": ["ssLd2wjExS9qWRur4tZDU1Z"],
+ "graphCoordinates": { "x": 300, "y": 550 }
+ },
+ "bdFW2HHjMoEFmqHtFre9Xi8": {
+ "id": "bdFW2HHjMoEFmqHtFre9Xi8",
+ "title": "Block #2",
+ "stepIds": ["sgkADMK25y9P9V3vjwjBaac", "ssEiEECKSFkA44dGDceHxKw"],
+ "graphCoordinates": { "x": 121, "y": 227 }
+ }
+ },
+ "allIds": [
+ "3kH2sUjVThQDWmqdoKnGk5",
+ "bdFW2HHjMoEFmqHtFre9Xi8",
+ "b9mSgu7RKmK4xuiTVQP5Me8"
+ ]
+ },
+ "steps": {
+ "byId": {
+ "oxTsU2C1RX5QHuyY8qjHAM": {
+ "id": "oxTsU2C1RX5QHuyY8qjHAM",
+ "type": "start",
+ "label": "Start",
+ "edgeId": "25yX9DnQgdafpdAjfAu5Fp",
+ "blockId": "3kH2sUjVThQDWmqdoKnGk5"
+ },
+ "sgkADMK25y9P9V3vjwjBaac": {
+ "id": "sgkADMK25y9P9V3vjwjBaac",
+ "type": "text",
+ "blockId": "bdFW2HHjMoEFmqHtFre9Xi8",
+ "content": {
+ "html": "Ready?
",
+ "richText": [{ "type": "p", "children": [{ "text": "Ready?" }] }],
+ "plainText": "Ready?"
+ }
+ },
+ "ssEiEECKSFkA44dGDceHxKw": {
+ "id": "ssEiEECKSFkA44dGDceHxKw",
+ "type": "choice input",
+ "edgeId": "oxEEtym3NfDf34NCipzjRQ",
+ "blockId": "bdFW2HHjMoEFmqHtFre9Xi8",
+ "options": { "itemIds": ["q69Ex7LacPrH9QUMeosRnB"] }
+ },
+ "ssLd2wjExS9qWRur4tZDU1Z": {
+ "id": "ssLd2wjExS9qWRur4tZDU1Z",
+ "type": "Webhook",
+ "blockId": "b9mSgu7RKmK4xuiTVQP5Me8",
+ "options": { "webhookId": "4h4Kk3Q1qGy7gFzpZtWVpU" }
+ }
+ },
+ "allIds": [
+ "oxTsU2C1RX5QHuyY8qjHAM",
+ "sgkADMK25y9P9V3vjwjBaac",
+ "ssEiEECKSFkA44dGDceHxKw",
+ "ssLd2wjExS9qWRur4tZDU1Z"
+ ]
+ },
+ "choiceItems": {
+ "byId": {
+ "q69Ex7LacPrH9QUMeosRnB": {
+ "id": "q69Ex7LacPrH9QUMeosRnB",
+ "stepId": "ssEiEECKSFkA44dGDceHxKw",
+ "content": "Go"
+ }
+ },
+ "allIds": ["q69Ex7LacPrH9QUMeosRnB"]
+ },
+ "variables": {
+ "byId": {
+ "oASkBtoLqkYNqeakcjZH4L": {
+ "id": "oASkBtoLqkYNqeakcjZH4L",
+ "name": "secret 1"
+ },
+ "4tvkRmf32wiTsXrYoqyhfr": {
+ "id": "4tvkRmf32wiTsXrYoqyhfr",
+ "name": "secret 2"
+ },
+ "jEg1FvkCU5S5owNAxXFsHL": {
+ "id": "jEg1FvkCU5S5owNAxXFsHL",
+ "name": "secret 3"
+ },
+ "rEoE1ehHzgx8X3d3UPGDHg": {
+ "id": "rEoE1ehHzgx8X3d3UPGDHg",
+ "name": "secret 4"
+ }
+ },
+ "allIds": [
+ "oASkBtoLqkYNqeakcjZH4L",
+ "4tvkRmf32wiTsXrYoqyhfr",
+ "jEg1FvkCU5S5owNAxXFsHL",
+ "rEoE1ehHzgx8X3d3UPGDHg"
+ ]
+ },
+ "webhooks": {
+ "byId": {
+ "4h4Kk3Q1qGy7gFzpZtWVpU": { "id": "4h4Kk3Q1qGy7gFzpZtWVpU", "url": "" }
+ },
+ "allIds": ["4h4Kk3Q1qGy7gFzpZtWVpU"]
+ },
+ "edges": {
+ "byId": {
+ "25yX9DnQgdafpdAjfAu5Fp": {
+ "id": "25yX9DnQgdafpdAjfAu5Fp",
+ "to": { "blockId": "bdFW2HHjMoEFmqHtFre9Xi8" },
+ "from": {
+ "stepId": "oxTsU2C1RX5QHuyY8qjHAM",
+ "blockId": "3kH2sUjVThQDWmqdoKnGk5"
+ }
+ },
+ "oxEEtym3NfDf34NCipzjRQ": {
+ "id": "oxEEtym3NfDf34NCipzjRQ",
+ "to": { "blockId": "b9mSgu7RKmK4xuiTVQP5Me8" },
+ "from": {
+ "stepId": "ssEiEECKSFkA44dGDceHxKw",
+ "blockId": "bdFW2HHjMoEFmqHtFre9Xi8"
+ }
+ }
+ },
+ "allIds": ["25yX9DnQgdafpdAjfAu5Fp", "oxEEtym3NfDf34NCipzjRQ"]
+ },
+ "theme": {
+ "general": {
+ "font": "Open Sans",
+ "background": { "type": "None", "content": "#ffffff" }
+ }
+ },
+ "settings": {
+ "typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
+ },
+ "publicId": null
+}
diff --git a/apps/builder/cypress/fixtures/typebots/integrations/webhookPreview.json b/apps/builder/cypress/fixtures/typebots/integrations/webhookPreview.json
new file mode 100644
index 000000000..631a48a17
--- /dev/null
+++ b/apps/builder/cypress/fixtures/typebots/integrations/webhookPreview.json
@@ -0,0 +1,264 @@
+{
+ "id": "typebot4",
+ "createdAt": "2022-01-21T07:55:14.727Z",
+ "updatedAt": "2022-01-21T07:55:14.727Z",
+ "name": "My typebot",
+ "ownerId": "user2",
+ "publishedTypebotId": null,
+ "folderId": null,
+ "blocks": {
+ "byId": {
+ "3kH2sUjVThQDWmqdoKnGk5": {
+ "id": "3kH2sUjVThQDWmqdoKnGk5",
+ "title": "Start",
+ "stepIds": ["oxTsU2C1RX5QHuyY8qjHAM"],
+ "graphCoordinates": { "x": 42, "y": 13 }
+ },
+ "b9mSgu7RKmK4xuiTVQP5Me8": {
+ "id": "b9mSgu7RKmK4xuiTVQP5Me8",
+ "title": "Block #3",
+ "stepIds": ["ssLd2wjExS9qWRur4tZDU1Z"],
+ "graphCoordinates": { "x": 300, "y": 550 }
+ },
+ "bdFW2HHjMoEFmqHtFre9Xi8": {
+ "id": "bdFW2HHjMoEFmqHtFre9Xi8",
+ "title": "Block #2",
+ "stepIds": ["sgkADMK25y9P9V3vjwjBaac", "ssEiEECKSFkA44dGDceHxKw"],
+ "graphCoordinates": { "x": 121, "y": 227 }
+ },
+ "bmz4rc8r19H2C6b7soxzby4": {
+ "id": "bmz4rc8r19H2C6b7soxzby4",
+ "title": "Block #4",
+ "graphCoordinates": { "x": 632, "y": 279 },
+ "stepIds": ["sgTWsRM1qF2YoYLuGo3Z3pU"]
+ }
+ },
+ "allIds": [
+ "3kH2sUjVThQDWmqdoKnGk5",
+ "bdFW2HHjMoEFmqHtFre9Xi8",
+ "b9mSgu7RKmK4xuiTVQP5Me8",
+ "bmz4rc8r19H2C6b7soxzby4"
+ ]
+ },
+ "steps": {
+ "byId": {
+ "oxTsU2C1RX5QHuyY8qjHAM": {
+ "id": "oxTsU2C1RX5QHuyY8qjHAM",
+ "type": "start",
+ "label": "Start",
+ "edgeId": "25yX9DnQgdafpdAjfAu5Fp",
+ "blockId": "3kH2sUjVThQDWmqdoKnGk5"
+ },
+ "sgkADMK25y9P9V3vjwjBaac": {
+ "id": "sgkADMK25y9P9V3vjwjBaac",
+ "type": "text",
+ "blockId": "bdFW2HHjMoEFmqHtFre9Xi8",
+ "content": {
+ "html": "Ready?
",
+ "richText": [{ "type": "p", "children": [{ "text": "Ready?" }] }],
+ "plainText": "Ready?"
+ }
+ },
+ "ssEiEECKSFkA44dGDceHxKw": {
+ "id": "ssEiEECKSFkA44dGDceHxKw",
+ "type": "choice input",
+ "edgeId": "oxEEtym3NfDf34NCipzjRQ",
+ "blockId": "bdFW2HHjMoEFmqHtFre9Xi8",
+ "options": { "itemIds": ["q69Ex7LacPrH9QUMeosRnB"] }
+ },
+ "ssLd2wjExS9qWRur4tZDU1Z": {
+ "id": "ssLd2wjExS9qWRur4tZDU1Z",
+ "type": "Webhook",
+ "blockId": "b9mSgu7RKmK4xuiTVQP5Me8",
+ "options": {
+ "webhookId": "4h4Kk3Q1qGy7gFzpZtWVpU",
+ "variablesForTest": {
+ "byId": {
+ "6pMn1xm1y3xWVSdJetMAJH": {
+ "id": "6pMn1xm1y3xWVSdJetMAJH",
+ "variableId": "oASkBtoLqkYNqeakcjZH4L",
+ "value": "secret1"
+ },
+ "ettAiB75uoFWnJyPS7gn5k": {
+ "id": "ettAiB75uoFWnJyPS7gn5k",
+ "variableId": "4tvkRmf32wiTsXrYoqyhfr",
+ "value": "secret2"
+ },
+ "kKpD3Q4YvFQ7CGWiZxJF4s": {
+ "id": "kKpD3Q4YvFQ7CGWiZxJF4s",
+ "variableId": "jEg1FvkCU5S5owNAxXFsHL",
+ "value": "secret3"
+ },
+ "xjUC5Q3msXCw9fwqpNdoSx": {
+ "id": "xjUC5Q3msXCw9fwqpNdoSx",
+ "variableId": "rEoE1ehHzgx8X3d3UPGDHg",
+ "value": "secret4"
+ }
+ },
+ "allIds": [
+ "6pMn1xm1y3xWVSdJetMAJH",
+ "ettAiB75uoFWnJyPS7gn5k",
+ "kKpD3Q4YvFQ7CGWiZxJF4s",
+ "xjUC5Q3msXCw9fwqpNdoSx"
+ ]
+ },
+ "responseVariableMapping": {
+ "byId": {
+ "o53h6M1sgHJfDTY5C3YEaT": {
+ "id": "o53h6M1sgHJfDTY5C3YEaT",
+ "bodyPath": "data[0].name",
+ "variableId": "4kVx5uf8W1XP6WsfJEvt8v"
+ }
+ },
+ "allIds": ["o53h6M1sgHJfDTY5C3YEaT"]
+ }
+ },
+ "edgeId": "81SjKnxuUgrPmXvvJJihHM"
+ },
+ "sgTWsRM1qF2YoYLuGo3Z3pU": {
+ "id": "sgTWsRM1qF2YoYLuGo3Z3pU",
+ "blockId": "bmz4rc8r19H2C6b7soxzby4",
+ "type": "text",
+ "content": {
+ "html": "His name is {{Name}}
",
+ "richText": [
+ { "type": "p", "children": [{ "text": "His name is {{Name}}" }] }
+ ],
+ "plainText": "His name is {{Name}}"
+ }
+ }
+ },
+ "allIds": [
+ "oxTsU2C1RX5QHuyY8qjHAM",
+ "sgkADMK25y9P9V3vjwjBaac",
+ "ssEiEECKSFkA44dGDceHxKw",
+ "ssLd2wjExS9qWRur4tZDU1Z",
+ "sgTWsRM1qF2YoYLuGo3Z3pU"
+ ]
+ },
+ "choiceItems": {
+ "byId": {
+ "q69Ex7LacPrH9QUMeosRnB": {
+ "id": "q69Ex7LacPrH9QUMeosRnB",
+ "stepId": "ssEiEECKSFkA44dGDceHxKw",
+ "content": "Go"
+ }
+ },
+ "allIds": ["q69Ex7LacPrH9QUMeosRnB"]
+ },
+ "webhooks": { "byId": {}, "allIds": [] },
+ "variables": {
+ "byId": {
+ "4tvkRmf32wiTsXrYoqyhfr": {
+ "id": "4tvkRmf32wiTsXrYoqyhfr",
+ "name": "secret 2",
+ "value": "secret2"
+ },
+ "jEg1FvkCU5S5owNAxXFsHL": {
+ "id": "jEg1FvkCU5S5owNAxXFsHL",
+ "name": "secret 3",
+ "value": "secret3"
+ },
+ "oASkBtoLqkYNqeakcjZH4L": {
+ "id": "oASkBtoLqkYNqeakcjZH4L",
+ "name": "secret 1",
+ "value": "secret1"
+ },
+ "rEoE1ehHzgx8X3d3UPGDHg": {
+ "id": "rEoE1ehHzgx8X3d3UPGDHg",
+ "name": "secret 4",
+ "value": "secret4"
+ },
+ "4kVx5uf8W1XP6WsfJEvt8v": {
+ "id": "4kVx5uf8W1XP6WsfJEvt8v",
+ "name": "Name"
+ }
+ },
+ "allIds": [
+ "oASkBtoLqkYNqeakcjZH4L",
+ "4tvkRmf32wiTsXrYoqyhfr",
+ "jEg1FvkCU5S5owNAxXFsHL",
+ "rEoE1ehHzgx8X3d3UPGDHg",
+ "4kVx5uf8W1XP6WsfJEvt8v"
+ ]
+ },
+ "webhooks": {
+ "byId": {
+ "4h4Kk3Q1qGy7gFzpZtWVpU": {
+ "id": "4h4Kk3Q1qGy7gFzpZtWVpU",
+ "url": "http://localhost:3000/api/mock/webhook",
+ "queryParams": {
+ "byId": {
+ "hwGB11cA7RaYnaqH7gYyuQ": {
+ "id": "hwGB11cA7RaYnaqH7gYyuQ",
+ "key": "firstParam",
+ "value": "{{secret 1}}"
+ },
+ "6ux2FZjhNc4vfqNUDuCkxn": {
+ "id": "6ux2FZjhNc4vfqNUDuCkxn",
+ "key": "secondParam",
+ "value": "{{secret 2}}"
+ }
+ },
+ "allIds": ["hwGB11cA7RaYnaqH7gYyuQ", "6ux2FZjhNc4vfqNUDuCkxn"]
+ },
+ "headers": {
+ "byId": {
+ "ayTB2cFRKMo6oH9t9KS8SA": {
+ "id": "ayTB2cFRKMo6oH9t9KS8SA",
+ "key": "Custom-Typebot",
+ "value": "{{secret 3}}"
+ }
+ },
+ "allIds": ["ayTB2cFRKMo6oH9t9KS8SA"]
+ },
+ "method": "POST",
+ "body": "{ \"customField\": \"{{secret 4}}\" }"
+ }
+ },
+ "allIds": ["4h4Kk3Q1qGy7gFzpZtWVpU"]
+ },
+ "edges": {
+ "byId": {
+ "25yX9DnQgdafpdAjfAu5Fp": {
+ "id": "25yX9DnQgdafpdAjfAu5Fp",
+ "to": { "blockId": "bdFW2HHjMoEFmqHtFre9Xi8" },
+ "from": {
+ "stepId": "oxTsU2C1RX5QHuyY8qjHAM",
+ "blockId": "3kH2sUjVThQDWmqdoKnGk5"
+ }
+ },
+ "oxEEtym3NfDf34NCipzjRQ": {
+ "id": "oxEEtym3NfDf34NCipzjRQ",
+ "to": { "blockId": "b9mSgu7RKmK4xuiTVQP5Me8" },
+ "from": {
+ "stepId": "ssEiEECKSFkA44dGDceHxKw",
+ "blockId": "bdFW2HHjMoEFmqHtFre9Xi8"
+ }
+ },
+ "81SjKnxuUgrPmXvvJJihHM": {
+ "from": {
+ "blockId": "b9mSgu7RKmK4xuiTVQP5Me8",
+ "stepId": "ssLd2wjExS9qWRur4tZDU1Z"
+ },
+ "to": { "blockId": "bmz4rc8r19H2C6b7soxzby4" },
+ "id": "81SjKnxuUgrPmXvvJJihHM"
+ }
+ },
+ "allIds": [
+ "25yX9DnQgdafpdAjfAu5Fp",
+ "oxEEtym3NfDf34NCipzjRQ",
+ "81SjKnxuUgrPmXvvJJihHM"
+ ]
+ },
+ "theme": {
+ "general": {
+ "font": "Open Sans",
+ "background": { "type": "None", "content": "#ffffff" }
+ }
+ },
+ "settings": {
+ "typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
+ },
+ "publicId": null
+}
diff --git a/apps/builder/cypress/fixtures/typebots/integrations/webhookPreview.png b/apps/builder/cypress/fixtures/typebots/integrations/webhookPreview.png
new file mode 100644
index 0000000000000000000000000000000000000000..8b98e2c9eeedaca1dec4937ae82c88da76cbd85a
GIT binary patch
literal 289483
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