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" - /> - - - - ))} - - - ) -} 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" - /> - - - - ))} - - - ) -} - -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" - /> - - - - ))} - - - ) -} - -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... - ) : ( - - Step image - - ) - } - 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 ( - -