feat(integration): ✨ Add webhooks
This commit is contained in:
@@ -280,3 +280,9 @@ export const FilmIcon = (props: IconProps) => (
|
||||
<line x1="17" y1="7" x2="22" y2="7"></line>
|
||||
</Icon>
|
||||
)
|
||||
|
||||
export const WebhookIcon = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon>
|
||||
</Icon>
|
||||
)
|
||||
|
||||
9
apps/builder/assets/styles/codeMirror.css
Normal file
9
apps/builder/assets/styles/codeMirror.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.cm-editor {
|
||||
height: 100%;
|
||||
outline: 0px solid transparent !important;
|
||||
}
|
||||
|
||||
.cm-scroller {
|
||||
border-radius: 5px;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
@@ -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 <GoogleSheetsLogo {...props} />
|
||||
case IntegrationStepType.GOOGLE_ANALYTICS:
|
||||
return <GoogleAnalyticsLogo {...props} />
|
||||
case IntegrationStepType.WEBHOOK:
|
||||
return <WebhookIcon />
|
||||
case 'start':
|
||||
return <FlagIcon {...props} />
|
||||
default:
|
||||
|
||||
@@ -49,6 +49,8 @@ export const StepTypeLabel = ({ type }: Props) => {
|
||||
<Text>Analytics</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
case IntegrationStepType.WEBHOOK:
|
||||
return <Text>Webhook</Text>
|
||||
default:
|
||||
return <></>
|
||||
}
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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<Step, TextBubbleStep>
|
||||
@@ -51,7 +54,7 @@ export const SettingsPopoverContent = ({ step, onExpandClick }: Props) => {
|
||||
<PopoverContent onMouseDown={handleMouseDown} pos="relative">
|
||||
<PopoverArrow />
|
||||
<PopoverBody
|
||||
p="6"
|
||||
py="6"
|
||||
overflowY="scroll"
|
||||
maxH="400px"
|
||||
ref={ref}
|
||||
@@ -74,9 +77,16 @@ 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<InputStep>)
|
||||
}
|
||||
|
||||
const handleWebhookChange = (webhook: Partial<Webhook>) => {
|
||||
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 (
|
||||
<WebhookSettings
|
||||
key={step.options?.webhookId}
|
||||
options={step.options}
|
||||
webhook={typebot?.webhooks.byId[step.options?.webhookId ?? '']}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onWebhookChange={handleWebhookChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
return <></>
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Stack spacing={4}>
|
||||
|
||||
@@ -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<Comparison>) => {
|
||||
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 (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<VariableSearchInput
|
||||
initialVariableId={item.variableId}
|
||||
onSelectVariable={handleSelectVariable}
|
||||
placeholder="Search for a variable"
|
||||
/>
|
||||
<DropdownList<ComparisonOperators>
|
||||
currentItem={item.comparisonOperator}
|
||||
onItemSelect={handleSelectComparisonOperator}
|
||||
items={Object.values(ComparisonOperators)}
|
||||
placeholder="Select an operator"
|
||||
/>
|
||||
{item.comparisonOperator !== ComparisonOperators.IS_SET && (
|
||||
<InputWithVariableButton
|
||||
delay={100}
|
||||
initialValue={item.value ?? ''}
|
||||
onChange={handleChangeValue}
|
||||
placeholder="Type a value..."
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -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<Comparison>
|
||||
logicalOperator: LogicalOperator
|
||||
onLogicalOperatorChange: (logicalOperator: LogicalOperator) => void
|
||||
onComparisonsChange: (comparisons: Table<Comparison>) => void
|
||||
}
|
||||
|
||||
export const ComparisonsList = ({
|
||||
initialComparisons,
|
||||
logicalOperator,
|
||||
onLogicalOperatorChange,
|
||||
onComparisonsChange,
|
||||
}: Props) => {
|
||||
const [comparisons, setComparisons] = useImmer(initialComparisons)
|
||||
const [showDeleteId, setShowDeleteId] = useState<string | undefined>()
|
||||
|
||||
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<Omit<Comparison, 'id'>>
|
||||
) =>
|
||||
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 (
|
||||
<Stack spacing="4" py="4">
|
||||
{comparisons.allIds.map((comparisonId, idx) => (
|
||||
<>
|
||||
{idx > 0 && (
|
||||
<Flex justify="center">
|
||||
<DropdownList<LogicalOperator>
|
||||
currentItem={logicalOperator}
|
||||
onItemSelect={handleLogicalOperatorSelected}
|
||||
items={Object.values(LogicalOperator)}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
<Flex
|
||||
pos="relative"
|
||||
onMouseEnter={handleMouseEnter(comparisonId)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<Stack
|
||||
key={comparisonId}
|
||||
p="4"
|
||||
rounded="md"
|
||||
flex="1"
|
||||
borderWidth="1px"
|
||||
>
|
||||
<VariableSearchInput
|
||||
initialVariableId={comparisons.byId[comparisonId].variableId}
|
||||
onSelectVariable={handleVariableSelected(comparisonId)}
|
||||
placeholder="Search for a variable"
|
||||
/>
|
||||
<DropdownList<ComparisonOperators>
|
||||
currentItem={comparisons.byId[comparisonId].comparisonOperator}
|
||||
onItemSelect={handleComparisonOperatorSelected(comparisonId)}
|
||||
items={Object.values(ComparisonOperators)}
|
||||
/>
|
||||
{comparisons.byId[comparisonId].comparisonOperator !==
|
||||
ComparisonOperators.IS_SET && (
|
||||
<DebouncedInput
|
||||
delay={100}
|
||||
initialValue={comparisons.byId[comparisonId].value ?? ''}
|
||||
onChange={handleValueChange(comparisonId)}
|
||||
placeholder="Type a value..."
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
<Fade in={showDeleteId === comparisonId}>
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove comparison"
|
||||
onClick={deleteComparison(comparisonId)}
|
||||
pos="absolute"
|
||||
left="-15px"
|
||||
top="-15px"
|
||||
size="sm"
|
||||
shadow="md"
|
||||
/>
|
||||
</Fade>
|
||||
</Flex>
|
||||
</>
|
||||
))}
|
||||
<Button
|
||||
leftIcon={<PlusIcon />}
|
||||
onClick={createComparison}
|
||||
flexShrink={0}
|
||||
colorScheme="blue"
|
||||
>
|
||||
Add a comparison
|
||||
</Button>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<ComparisonsList
|
||||
initialComparisons={options.comparisons}
|
||||
logicalOperator={options.logicalOperator ?? LogicalOperator.AND}
|
||||
onLogicalOperatorChange={handleLogicalOperatorChange}
|
||||
onComparisonsChange={handleComparisonsChange}
|
||||
<TableList<Comparison>
|
||||
onItemsChange={handleComparisonsChange}
|
||||
Item={ComparisonItem}
|
||||
ComponentBetweenItems={() => (
|
||||
<Flex justify="center">
|
||||
<DropdownList<LogicalOperator>
|
||||
currentItem={options.logicalOperator}
|
||||
onItemSelect={handleLogicalOperatorChange}
|
||||
items={Object.values(LogicalOperator)}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
addLabel="Add a comparison"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Stack spacing={4}>
|
||||
|
||||
@@ -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 (
|
||||
<Stack spacing={4}>
|
||||
|
||||
@@ -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<Cell> & { 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 (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<DropdownList<string>
|
||||
currentItem={item.column}
|
||||
onItemSelect={handleColumnSelect}
|
||||
items={columns}
|
||||
placeholder="Select a column"
|
||||
/>
|
||||
<InputWithVariableButton
|
||||
initialValue={item.value ?? ''}
|
||||
onChange={handleValueChange}
|
||||
placeholder="Type a value..."
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -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<ExtractingCell> & { 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 (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<DropdownList<string>
|
||||
currentItem={item.column}
|
||||
onItemSelect={handleColumnSelect}
|
||||
items={columns}
|
||||
placeholder="Select a column"
|
||||
/>
|
||||
<VariableSearchInput
|
||||
initialVariableId={item.variableId}
|
||||
onSelectVariable={handleVariableIdChange}
|
||||
placeholder="Select a variable"
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -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<ExtractingCell>
|
||||
onCellsChange: (cells: Table<ExtractingCell>) => void
|
||||
}
|
||||
|
||||
const id = generate()
|
||||
const defaultCells: Table<ExtractingCell> = {
|
||||
byId: { [id]: {} },
|
||||
allIds: [id],
|
||||
}
|
||||
|
||||
export const ExtractCellList = ({
|
||||
sheet,
|
||||
initialCells,
|
||||
onCellsChange,
|
||||
}: Props) => {
|
||||
const [cells, setCells] = useImmer(initialCells ?? defaultCells)
|
||||
const [showDeleteId, setShowDeleteId] = useState<string | undefined>()
|
||||
|
||||
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<ExtractingCell>) =>
|
||||
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 (
|
||||
<Stack spacing="4">
|
||||
{cells.allIds.map((cellId) => (
|
||||
<>
|
||||
<Flex
|
||||
pos="relative"
|
||||
onMouseEnter={handleMouseEnter(cellId)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<CellWithVariableIdStack
|
||||
key={cellId}
|
||||
cell={cells.byId[cellId]}
|
||||
columns={sheet.columns}
|
||||
onCellChange={handleCellChange(cellId)}
|
||||
/>
|
||||
<Fade in={showDeleteId === cellId}>
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove cell"
|
||||
onClick={deleteCell(cellId)}
|
||||
pos="absolute"
|
||||
left="-15px"
|
||||
top="-15px"
|
||||
size="sm"
|
||||
shadow="md"
|
||||
/>
|
||||
</Fade>
|
||||
</Flex>
|
||||
</>
|
||||
))}
|
||||
<Button
|
||||
leftIcon={<PlusIcon />}
|
||||
onClick={createCell}
|
||||
flexShrink={0}
|
||||
colorScheme="blue"
|
||||
>
|
||||
Add a value
|
||||
</Button>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<DropdownList<string>
|
||||
currentItem={cell.column}
|
||||
onItemSelect={handleColumnSelect}
|
||||
items={columns}
|
||||
placeholder="Select a column"
|
||||
/>
|
||||
<VariableSearchInput
|
||||
initialVariableId={cell.variableId}
|
||||
onSelectVariable={handleVariableIdChange}
|
||||
placeholder="Select a variable"
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -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<ExtractingCell>) =>
|
||||
onOptionsChange({ ...options, cellsToExtract } as GoogleSheetsOptions)
|
||||
|
||||
const UpdatingCellItem = useMemo(
|
||||
() => (props: TableListItemProps<Cell>) =>
|
||||
<CellWithValueStack {...props} columns={sheet.columns} />,
|
||||
[sheet.columns]
|
||||
)
|
||||
|
||||
const ExtractingCellItem = useMemo(
|
||||
() => (props: TableListItemProps<ExtractingCell>) =>
|
||||
<CellWithVariableIdStack {...props} columns={sheet.columns} />,
|
||||
[sheet.columns]
|
||||
)
|
||||
|
||||
switch (options.action) {
|
||||
case GoogleSheetsAction.INSERT_ROW:
|
||||
return (
|
||||
<UpdateCellList
|
||||
initialCells={options.cellsToInsert}
|
||||
sheet={sheet}
|
||||
onCellsChange={handleInsertColumnsChange}
|
||||
<TableList<Cell>
|
||||
initialItems={options.cellsToInsert}
|
||||
onItemsChange={handleInsertColumnsChange}
|
||||
Item={UpdatingCellItem}
|
||||
addLabel="Add a value"
|
||||
/>
|
||||
)
|
||||
case GoogleSheetsAction.UPDATE_ROW:
|
||||
@@ -146,15 +160,17 @@ const ActionOptions = ({
|
||||
<Stack>
|
||||
<Text>Row to select</Text>
|
||||
<CellWithValueStack
|
||||
cell={options.referenceCell ?? {}}
|
||||
id={'reference'}
|
||||
columns={sheet.columns}
|
||||
onCellChange={handleReferenceCellChange}
|
||||
item={options.referenceCell ?? {}}
|
||||
onItemChange={handleReferenceCellChange}
|
||||
/>
|
||||
<Text>Cells to update</Text>
|
||||
<UpdateCellList
|
||||
initialCells={options.cellsToUpsert}
|
||||
sheet={sheet}
|
||||
onCellsChange={handleUpsertColumnsChange}
|
||||
<TableList<Cell>
|
||||
initialItems={options.cellsToUpsert}
|
||||
onItemsChange={handleUpsertColumnsChange}
|
||||
Item={UpdatingCellItem}
|
||||
addLabel="Add a value"
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
@@ -163,15 +179,17 @@ const ActionOptions = ({
|
||||
<Stack>
|
||||
<Text>Row to select</Text>
|
||||
<CellWithValueStack
|
||||
cell={options.referenceCell ?? {}}
|
||||
id={'reference'}
|
||||
columns={sheet.columns}
|
||||
onCellChange={handleReferenceCellChange}
|
||||
item={options.referenceCell ?? {}}
|
||||
onItemChange={handleReferenceCellChange}
|
||||
/>
|
||||
<Text>Cells to extract</Text>
|
||||
<ExtractCellList
|
||||
initialCells={options.cellsToExtract}
|
||||
sheet={sheet}
|
||||
onCellsChange={handleExtractingCellsChange}
|
||||
<TableList<ExtractingCell>
|
||||
initialItems={options.cellsToExtract}
|
||||
onItemsChange={handleExtractingCellsChange}
|
||||
Item={ExtractingCellItem}
|
||||
addLabel="Add a value"
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ export const SheetsDropdown = ({
|
||||
<SearchableDropdown
|
||||
selectedItem={currentSheet?.name}
|
||||
items={(sheets ?? []).map((s) => s.name)}
|
||||
onSelectItem={handleSpreadsheetSelect}
|
||||
onValueChange={handleSpreadsheetSelect}
|
||||
placeholder={isLoading ? 'Loading...' : 'Select the sheet'}
|
||||
isDisabled={isLoading}
|
||||
/>
|
||||
|
||||
@@ -27,7 +27,7 @@ export const SpreadsheetsDropdown = ({
|
||||
<SearchableDropdown
|
||||
selectedItem={currentSpreadsheet?.name}
|
||||
items={(spreadsheets ?? []).map((s) => s.name)}
|
||||
onSelectItem={handleSpreadsheetSelect}
|
||||
onValueChange={handleSpreadsheetSelect}
|
||||
placeholder={isLoading ? 'Loading...' : 'Search for spreadsheet'}
|
||||
isDisabled={isLoading}
|
||||
/>
|
||||
|
||||
@@ -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<Cell>
|
||||
onCellsChange: (cells: Table<Cell>) => void
|
||||
}
|
||||
|
||||
const id = generate()
|
||||
const defaultCells: Table<Cell> = {
|
||||
byId: { [id]: {} },
|
||||
allIds: [id],
|
||||
}
|
||||
|
||||
export const UpdateCellList = ({
|
||||
sheet,
|
||||
initialCells,
|
||||
onCellsChange,
|
||||
}: Props) => {
|
||||
const [cells, setCells] = useImmer(initialCells ?? defaultCells)
|
||||
const [showDeleteId, setShowDeleteId] = useState<string | undefined>()
|
||||
|
||||
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<Cell>) =>
|
||||
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 (
|
||||
<Stack spacing="4">
|
||||
{cells.allIds.map((cellId) => (
|
||||
<>
|
||||
<Flex
|
||||
pos="relative"
|
||||
onMouseEnter={handleMouseEnter(cellId)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<CellWithValueStack
|
||||
key={cellId}
|
||||
cell={cells.byId[cellId]}
|
||||
columns={sheet.columns}
|
||||
onCellChange={handleCellChange(cellId)}
|
||||
/>
|
||||
<Fade in={showDeleteId === cellId}>
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove cell"
|
||||
onClick={deleteCell(cellId)}
|
||||
pos="absolute"
|
||||
left="-15px"
|
||||
top="-15px"
|
||||
size="sm"
|
||||
shadow="md"
|
||||
/>
|
||||
</Fade>
|
||||
</Flex>
|
||||
</>
|
||||
))}
|
||||
<Button
|
||||
leftIcon={<PlusIcon />}
|
||||
onClick={createCell}
|
||||
flexShrink={0}
|
||||
colorScheme="blue"
|
||||
>
|
||||
Add a value
|
||||
</Button>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<DropdownList<string>
|
||||
currentItem={cell.column}
|
||||
onItemSelect={handleColumnSelect}
|
||||
items={columns}
|
||||
placeholder="Select a column"
|
||||
/>
|
||||
<InputWithVariableButton
|
||||
initialValue={cell.value ?? ''}
|
||||
onChange={handleValueChange}
|
||||
placeholder="Type a value..."
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<Stack spacing={4}>
|
||||
|
||||
@@ -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 (
|
||||
<Stack spacing={4}>
|
||||
|
||||
@@ -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 })
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Stack spacing={4}>
|
||||
|
||||
@@ -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 (
|
||||
<Stack spacing={4}>
|
||||
|
||||
@@ -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<KeyValue>) => (
|
||||
<KeyValueInputs
|
||||
{...props}
|
||||
keyPlaceholder="e.g. email"
|
||||
valuePlaceholder="e.g. {{Email}}"
|
||||
/>
|
||||
)
|
||||
|
||||
export const HeadersInputs = (props: TableListItemProps<KeyValue>) => (
|
||||
<KeyValueInputs
|
||||
{...props}
|
||||
keyPlaceholder="e.g. Content-Type"
|
||||
valuePlaceholder="e.g. application/json"
|
||||
/>
|
||||
)
|
||||
|
||||
export const KeyValueInputs = ({
|
||||
id,
|
||||
item,
|
||||
onItemChange,
|
||||
keyPlaceholder,
|
||||
valuePlaceholder,
|
||||
}: TableListItemProps<KeyValue> & {
|
||||
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 (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<FormControl>
|
||||
<FormLabel htmlFor={'key' + id}>Key:</FormLabel>
|
||||
<InputWithVariableButton
|
||||
id={'key' + id}
|
||||
initialValue={item.key ?? ''}
|
||||
onChange={handleKeyChange}
|
||||
placeholder={keyPlaceholder}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor={'value' + id}>Value:</FormLabel>
|
||||
<InputWithVariableButton
|
||||
id={'value' + id}
|
||||
initialValue={item.value ?? ''}
|
||||
onChange={handleValueChange}
|
||||
placeholder={valuePlaceholder}
|
||||
/>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -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<ResponseVariableMapping> & { dataItems: string[] }) => {
|
||||
const handleBodyPathChange = (bodyPath: string) =>
|
||||
onItemChange({ ...item, bodyPath })
|
||||
const handleVariableChange = (variable?: Variable) =>
|
||||
onItemChange({ ...item, variableId: variable?.id })
|
||||
|
||||
return (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="name">Data:</FormLabel>
|
||||
<SearchableDropdown
|
||||
items={dataItems}
|
||||
value={item.bodyPath}
|
||||
onValueChange={handleBodyPathChange}
|
||||
placeholder="Select the data"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="value">Set variable:</FormLabel>
|
||||
<VariableSearchInput
|
||||
onSelectVariable={handleVariableChange}
|
||||
placeholder="Search for a variable"
|
||||
initialVariableId={item.variableId}
|
||||
/>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -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<VariableForTest>) => {
|
||||
const handleVariableSelect = (variable?: Variable) =>
|
||||
onItemChange({ ...item, variableId: variable?.id })
|
||||
const handleValueChange = (value: string) => {
|
||||
if (value === item.value) return
|
||||
onItemChange({ ...item, value })
|
||||
}
|
||||
return (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<FormControl>
|
||||
<FormLabel htmlFor={'name' + id}>Variable name:</FormLabel>
|
||||
<VariableSearchInput
|
||||
id={'name' + id}
|
||||
initialVariableId={item.variableId}
|
||||
onSelectVariable={handleVariableSelect}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor={'value' + id}>Test value:</FormLabel>
|
||||
<DebouncedInput
|
||||
id={'value' + id}
|
||||
delay={100}
|
||||
initialValue={item.value ?? ''}
|
||||
onChange={handleValueChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -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<Webhook>) => void
|
||||
}
|
||||
|
||||
export const WebhookSettings = ({
|
||||
options,
|
||||
webhook,
|
||||
onOptionsChange,
|
||||
onWebhookChange,
|
||||
}: Props) => {
|
||||
const { createWebhook, typebot, save } = useTypebot()
|
||||
const [testResponse, setTestResponse] = useState<string>()
|
||||
const [responseKeys, setResponseKeys] = useState<string[]>([])
|
||||
|
||||
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<KeyValue>) =>
|
||||
onWebhookChange({ queryParams })
|
||||
|
||||
const handleHeadersChange = (headers: Table<KeyValue>) =>
|
||||
onWebhookChange({ headers })
|
||||
|
||||
const handleBodyChange = (body: string) => onWebhookChange({ body })
|
||||
|
||||
const handleVariablesChange = (variablesForTest: Table<VariableForTest>) =>
|
||||
onOptionsChange({ ...options, variablesForTest })
|
||||
|
||||
const handleResponseMappingChange = (
|
||||
responseVariableMapping: Table<ResponseVariableMapping>
|
||||
) => 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<ResponseVariableMapping>) =>
|
||||
<DataVariableInputs {...props} dataItems={responseKeys} />,
|
||||
[responseKeys]
|
||||
)
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
<Flex>
|
||||
<DropdownList<HttpMethod>
|
||||
currentItem={webhook?.method ?? HttpMethod.GET}
|
||||
onItemSelect={handleMethodChange}
|
||||
items={Object.values(HttpMethod)}
|
||||
/>
|
||||
</Flex>
|
||||
<InputWithVariableButton
|
||||
placeholder="Your Webhook URL..."
|
||||
initialValue={webhook?.url ?? ''}
|
||||
onChange={handleUrlChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Accordion allowToggle allowMultiple>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Query params
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<KeyValue>
|
||||
initialItems={webhook?.queryParams}
|
||||
onItemsChange={handleQueryParamsChange}
|
||||
Item={QueryParamsInputs}
|
||||
addLabel="Add a param"
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Headers
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<KeyValue>
|
||||
initialItems={webhook?.headers}
|
||||
onItemsChange={handleHeadersChange}
|
||||
Item={HeadersInputs}
|
||||
addLabel="Add a value"
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Body
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<CodeEditor
|
||||
value={webhook?.body ?? ''}
|
||||
onChange={handleBodyChange}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Variable values for test
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<VariableForTest>
|
||||
initialItems={options?.variablesForTest}
|
||||
onItemsChange={handleVariablesChange}
|
||||
Item={VariableForTestInputs}
|
||||
addLabel="Add an entry"
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<Button onClick={handleTestRequestClick} colorScheme="blue">
|
||||
Test the request
|
||||
</Button>
|
||||
{testResponse && <CodeEditor isReadOnly value={testResponse} />}
|
||||
{(testResponse || options?.responseVariableMapping) && (
|
||||
<Accordion allowToggle allowMultiple>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Save in variables
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<ResponseVariableMapping>
|
||||
initialItems={options?.responseVariableMapping}
|
||||
onItemsChange={handleResponseMappingChange}
|
||||
Item={ResponseMappingInputs}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { WebhookSettings } from './WebhookSettings'
|
||||
@@ -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"
|
||||
>
|
||||
<StepIcon type={step.type} mt="1" />
|
||||
<StepNodeContent step={step} />
|
||||
|
||||
@@ -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 (
|
||||
<Flex
|
||||
flexDir={'column'}
|
||||
opacity={step.content.html === '' ? '0.5' : '1'}
|
||||
className="slate-html-container"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
step.content.html === ''
|
||||
? `<p>Click to edit...</p>`
|
||||
: step.content.html,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case BubbleStepType.IMAGE: {
|
||||
return !step.content?.url ? (
|
||||
<Text color={'gray.500'}>Click to edit...</Text>
|
||||
) : (
|
||||
<Box w="full">
|
||||
<Image
|
||||
src={step.content?.url}
|
||||
alt="Step image"
|
||||
rounded="md"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
case BubbleStepType.VIDEO: {
|
||||
return <VideoStepNodeContent step={step} />
|
||||
}
|
||||
case InputStepType.TEXT: {
|
||||
return (
|
||||
<Text color={'gray.500'}>
|
||||
{step.options?.labels?.placeholder ?? 'Type your answer...'}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
case InputStepType.NUMBER: {
|
||||
return (
|
||||
<Text color={'gray.500'}>
|
||||
{step.options?.labels?.placeholder ?? 'Type your answer...'}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
case InputStepType.EMAIL: {
|
||||
return (
|
||||
<Text color={'gray.500'}>
|
||||
{step.options?.labels?.placeholder ?? 'Type your email...'}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
case InputStepType.URL: {
|
||||
return (
|
||||
<Text color={'gray.500'}>
|
||||
{step.options?.labels?.placeholder ?? 'Type your URL...'}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
case InputStepType.DATE: {
|
||||
return (
|
||||
<Text color={'gray.500'}>
|
||||
{step.options?.labels?.from ?? 'Pick a date...'}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
case InputStepType.PHONE: {
|
||||
return (
|
||||
<Text color={'gray.500'}>
|
||||
{step.options?.labels?.placeholder ?? 'Your phone number...'}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
case InputStepType.CHOICE: {
|
||||
return <ChoiceItemsList step={step} />
|
||||
}
|
||||
case LogicStepType.SET_VARIABLE: {
|
||||
return <SetVariableNodeContent step={step} />
|
||||
}
|
||||
case LogicStepType.CONDITION: {
|
||||
return <ConditionNodeContent step={step} />
|
||||
}
|
||||
case LogicStepType.REDIRECT: {
|
||||
if (!step.options) return <Text color={'gray.500'}>Configure...</Text>
|
||||
return <Text isTruncated>Redirect to {step.options?.url}</Text>
|
||||
}
|
||||
case IntegrationStepType.GOOGLE_SHEETS: {
|
||||
if (!step.options) return <Text color={'gray.500'}>Configure...</Text>
|
||||
return <Text>{step.options?.action}</Text>
|
||||
}
|
||||
case IntegrationStepType.GOOGLE_ANALYTICS: {
|
||||
if (!step.options || !step.options.action)
|
||||
return <Text color={'gray.500'}>Configure...</Text>
|
||||
return <Text>Track "{step.options?.action}"</Text>
|
||||
}
|
||||
case 'start': {
|
||||
return <Text>{step.label}</Text>
|
||||
}
|
||||
default: {
|
||||
return <Text>No input</Text>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SetVariableNodeContent = ({ step }: { step: SetVariableStep }) => {
|
||||
const { typebot } = useTypebot()
|
||||
const variableName =
|
||||
typebot?.variables.byId[step.options?.variableId ?? '']?.name ?? ''
|
||||
const expression = step.options?.expressionToEvaluate ?? ''
|
||||
return (
|
||||
<Text color={'gray.500'}>
|
||||
{variableName === '' && expression === ''
|
||||
? 'Click to edit...'
|
||||
: `${variableName} = ${expression}`}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
const ConditionNodeContent = ({ step }: { step: ConditionStep }) => {
|
||||
const { typebot } = useTypebot()
|
||||
return (
|
||||
<Flex>
|
||||
<Stack color={'gray.500'}>
|
||||
{step.options?.comparisons.allIds.map((comparisonId, idx) => {
|
||||
const comparison = step.options?.comparisons.byId[comparisonId]
|
||||
const variable = typebot?.variables.byId[comparison?.variableId ?? '']
|
||||
return (
|
||||
<HStack key={comparisonId} spacing={1}>
|
||||
{idx > 0 && <Text>{step.options?.logicalOperator ?? ''}</Text>}
|
||||
{variable?.name && (
|
||||
<Tag bgColor="orange.400">{variable.name}</Tag>
|
||||
)}
|
||||
{comparison.comparisonOperator && (
|
||||
<Text>{comparison?.comparisonOperator}</Text>
|
||||
)}
|
||||
{comparison?.value && (
|
||||
<Tag bgColor={'green.400'}>{comparison.value}</Tag>
|
||||
)}
|
||||
</HStack>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
blockId: step.blockId,
|
||||
stepId: step.id,
|
||||
conditionType: 'true',
|
||||
}}
|
||||
pos="absolute"
|
||||
top="7px"
|
||||
right="15px"
|
||||
/>
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
blockId: step.blockId,
|
||||
stepId: step.id,
|
||||
conditionType: 'false',
|
||||
}}
|
||||
pos="absolute"
|
||||
bottom="7px"
|
||||
right="15px"
|
||||
/>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
const VideoStepNodeContent = ({ step }: { step: VideoBubbleStep }) => {
|
||||
if (!step.content?.url || !step.content.type)
|
||||
return <Text color="gray.500">Click to edit...</Text>
|
||||
switch (step.content.type) {
|
||||
case VideoBubbleContentType.URL:
|
||||
return (
|
||||
<Box w="full" h="120px" pos="relative">
|
||||
<video
|
||||
key={step.content.url}
|
||||
controls
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '0',
|
||||
borderRadius: '10px',
|
||||
}}
|
||||
>
|
||||
<source src={step.content.url} />
|
||||
</video>
|
||||
</Box>
|
||||
)
|
||||
case VideoBubbleContentType.VIMEO:
|
||||
case VideoBubbleContentType.YOUTUBE: {
|
||||
const baseUrl =
|
||||
step.content.type === VideoBubbleContentType.VIMEO
|
||||
? 'https://player.vimeo.com/video'
|
||||
: 'https://www.youtube.com/embed'
|
||||
return (
|
||||
<Box w="full" h="120px" pos="relative">
|
||||
<iframe
|
||||
src={`${baseUrl}/${step.content.id}`}
|
||||
allowFullScreen
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '0',
|
||||
borderRadius: '10px',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<Flex>
|
||||
<Stack color={'gray.500'}>
|
||||
{step.options?.comparisons.allIds.map((comparisonId, idx) => {
|
||||
const comparison = step.options?.comparisons.byId[comparisonId]
|
||||
const variable = typebot?.variables.byId[comparison?.variableId ?? '']
|
||||
return (
|
||||
<HStack key={comparisonId} spacing={1}>
|
||||
{idx > 0 && <Text>{step.options?.logicalOperator ?? ''}</Text>}
|
||||
{variable?.name && (
|
||||
<Tag bgColor="orange.400">{variable.name}</Tag>
|
||||
)}
|
||||
{comparison.comparisonOperator && (
|
||||
<Text>{comparison?.comparisonOperator}</Text>
|
||||
)}
|
||||
{comparison?.value && (
|
||||
<Tag bgColor={'green.400'}>{comparison.value}</Tag>
|
||||
)}
|
||||
</HStack>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
blockId: step.blockId,
|
||||
stepId: step.id,
|
||||
conditionType: 'true',
|
||||
}}
|
||||
pos="absolute"
|
||||
top="7px"
|
||||
right="15px"
|
||||
/>
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
blockId: step.blockId,
|
||||
stepId: step.id,
|
||||
conditionType: 'false',
|
||||
}}
|
||||
pos="absolute"
|
||||
bottom="7px"
|
||||
right="15px"
|
||||
/>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<Text color={'gray.500'}>
|
||||
{variableName === '' && expression === ''
|
||||
? 'Click to edit...'
|
||||
: `${variableName} = ${expression}`}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<Flex
|
||||
flexDir={'column'}
|
||||
opacity={step.content.html === '' ? '0.5' : '1'}
|
||||
className="slate-html-container"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
step.content.html === ''
|
||||
? `<p>Click to edit...</p>`
|
||||
: step.content.html,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case BubbleStepType.IMAGE: {
|
||||
return !step.content?.url ? (
|
||||
<Text color={'gray.500'}>Click to edit...</Text>
|
||||
) : (
|
||||
<Box w="full">
|
||||
<Image
|
||||
src={step.content?.url}
|
||||
alt="Step image"
|
||||
rounded="md"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
case BubbleStepType.VIDEO: {
|
||||
return <VideoStepNodeContent step={step} />
|
||||
}
|
||||
case InputStepType.TEXT: {
|
||||
return (
|
||||
<Text color={'gray.500'}>
|
||||
{step.options?.labels?.placeholder ?? 'Type your answer...'}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
case InputStepType.NUMBER: {
|
||||
return (
|
||||
<Text color={'gray.500'}>
|
||||
{step.options?.labels?.placeholder ?? 'Type your answer...'}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
case InputStepType.EMAIL: {
|
||||
return (
|
||||
<Text color={'gray.500'}>
|
||||
{step.options?.labels?.placeholder ?? 'Type your email...'}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
case InputStepType.URL: {
|
||||
return (
|
||||
<Text color={'gray.500'}>
|
||||
{step.options?.labels?.placeholder ?? 'Type your URL...'}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
case InputStepType.DATE: {
|
||||
return (
|
||||
<Text color={'gray.500'}>
|
||||
{step.options?.labels?.from ?? 'Pick a date...'}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
case InputStepType.PHONE: {
|
||||
return (
|
||||
<Text color={'gray.500'}>
|
||||
{step.options?.labels?.placeholder ?? 'Your phone number...'}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
case InputStepType.CHOICE: {
|
||||
return <ChoiceItemsList step={step} />
|
||||
}
|
||||
case LogicStepType.SET_VARIABLE: {
|
||||
return <SetVariableNodeContent step={step} />
|
||||
}
|
||||
case LogicStepType.CONDITION: {
|
||||
return <ConditionNodeContent step={step} />
|
||||
}
|
||||
case LogicStepType.REDIRECT: {
|
||||
if (!step.options) return <Text color={'gray.500'}>Configure...</Text>
|
||||
return <Text isTruncated>Redirect to {step.options?.url}</Text>
|
||||
}
|
||||
case IntegrationStepType.GOOGLE_SHEETS: {
|
||||
if (!step.options) return <Text color={'gray.500'}>Configure...</Text>
|
||||
return <Text>{step.options?.action}</Text>
|
||||
}
|
||||
case IntegrationStepType.GOOGLE_ANALYTICS: {
|
||||
if (!step.options || !step.options.action)
|
||||
return <Text color={'gray.500'}>Configure...</Text>
|
||||
return <Text>Track "{step.options?.action}"</Text>
|
||||
}
|
||||
case IntegrationStepType.WEBHOOK: {
|
||||
return <WebhookContent step={step} />
|
||||
}
|
||||
case 'start': {
|
||||
return <Text>{step.label}</Text>
|
||||
}
|
||||
default: {
|
||||
return <Text>No input</Text>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <Text color="gray.500">Click to edit...</Text>
|
||||
switch (step.content.type) {
|
||||
case VideoBubbleContentType.URL:
|
||||
return (
|
||||
<Box w="full" h="120px" pos="relative">
|
||||
<video
|
||||
key={step.content.url}
|
||||
controls
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '0',
|
||||
borderRadius: '10px',
|
||||
}}
|
||||
>
|
||||
<source src={step.content.url} />
|
||||
</video>
|
||||
</Box>
|
||||
)
|
||||
case VideoBubbleContentType.VIMEO:
|
||||
case VideoBubbleContentType.YOUTUBE: {
|
||||
const baseUrl =
|
||||
step.content.type === VideoBubbleContentType.VIMEO
|
||||
? 'https://player.vimeo.com/video'
|
||||
: 'https://www.youtube.com/embed'
|
||||
return (
|
||||
<Box w="full" h="120px" pos="relative">
|
||||
<iframe
|
||||
src={`${baseUrl}/${step.content.id}`}
|
||||
allowFullScreen
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '0',
|
||||
borderRadius: '10px',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <Text color="gray.500">Configure...</Text>
|
||||
return (
|
||||
<Text isTruncated pr="6">
|
||||
{webhook.method} {webhook.url}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { StepNodeContent } from './StepNodeContent'
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
58
apps/builder/components/shared/CodeEditor.tsx
Normal file
58
apps/builder/components/shared/CodeEditor.tsx
Normal file
@@ -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<BoxProps, 'onChange'>) => {
|
||||
const editorContainer = useRef<HTMLDivElement | null>(null)
|
||||
const editorView = useRef<EditorView | null>(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 (
|
||||
<Box ref={editorContainer} h="200px" data-testid="code-editor" {...props} />
|
||||
)
|
||||
}
|
||||
@@ -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<number>(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 (
|
||||
<HStack>
|
||||
<HStack spacing={0}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
onKeyUp={handleKeyUp}
|
||||
@@ -79,12 +81,15 @@ export const InputWithVariableButton = ({
|
||||
/>
|
||||
<Popover matchWidth isLazy>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
aria-label="Insert a variable"
|
||||
icon={<UserIcon />}
|
||||
pos="relative"
|
||||
ml="2"
|
||||
/>
|
||||
<Flex>
|
||||
<Tooltip label="Insert a variable">
|
||||
<IconButton
|
||||
aria-label="Insert a variable"
|
||||
icon={<UserIcon />}
|
||||
pos="relative"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w="full">
|
||||
<VariableSearchInput
|
||||
|
||||
@@ -11,20 +11,23 @@ import {
|
||||
InputProps,
|
||||
} from '@chakra-ui/react'
|
||||
import { useState, useRef, useEffect, ChangeEvent } from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
|
||||
type Props = {
|
||||
selectedItem?: string
|
||||
items: string[]
|
||||
onSelectItem: (value: string) => 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<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value)
|
||||
if (e.target.value === '') {
|
||||
@@ -69,7 +79,6 @@ export const SearchableDropdown = ({
|
||||
|
||||
const handleItemClick = (item: string) => () => {
|
||||
setInputValue(item)
|
||||
onSelectItem(item)
|
||||
onClose()
|
||||
}
|
||||
|
||||
|
||||
114
apps/builder/components/shared/TableList.tsx
Normal file
114
apps/builder/components/shared/TableList.tsx
Normal file
@@ -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<T> = {
|
||||
id: string
|
||||
item: T
|
||||
onItemChange: (item: T) => void
|
||||
}
|
||||
|
||||
type Props<T> = {
|
||||
initialItems?: Table<T>
|
||||
onItemsChange: (items: Table<T>) => void
|
||||
addLabel?: string
|
||||
Item: (props: TableListItemProps<T>) => JSX.Element
|
||||
ComponentBetweenItems?: (props: unknown) => JSX.Element
|
||||
}
|
||||
|
||||
export const TableList = <T,>({
|
||||
initialItems,
|
||||
onItemsChange,
|
||||
addLabel = 'Add',
|
||||
Item,
|
||||
ComponentBetweenItems = () => <></>,
|
||||
}: Props<T>) => {
|
||||
const [items, setItems] = useImmer(initialItems ?? { byId: {}, allIds: [] })
|
||||
const [showDeleteId, setShowDeleteId] = useState<string | undefined>()
|
||||
|
||||
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<T>
|
||||
items.allIds.push(id)
|
||||
})
|
||||
}
|
||||
|
||||
const updateItem = (itemId: string, updates: Partial<T>) =>
|
||||
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 (
|
||||
<Stack spacing="4">
|
||||
{items.allIds.map((itemId, idx) => (
|
||||
<Box key={itemId}>
|
||||
{idx !== 0 && <ComponentBetweenItems />}
|
||||
<Flex
|
||||
pos="relative"
|
||||
onMouseEnter={handleMouseEnter(itemId)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<Item
|
||||
id={itemId}
|
||||
item={items.byId[itemId]}
|
||||
onItemChange={handleCellChange(itemId)}
|
||||
/>
|
||||
<Fade in={showDeleteId === itemId}>
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove cell"
|
||||
onClick={deleteItem(itemId)}
|
||||
pos="absolute"
|
||||
left="-15px"
|
||||
top="-15px"
|
||||
size="sm"
|
||||
shadow="md"
|
||||
/>
|
||||
</Fade>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
<Button
|
||||
leftIcon={<PlusIcon />}
|
||||
onClick={createItem}
|
||||
flexShrink={0}
|
||||
colorScheme="blue"
|
||||
>
|
||||
{addLabel}
|
||||
</Button>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -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<Variable, 'id' | 'name'>) => void
|
||||
onSelectVariable: (
|
||||
variable: Pick<Variable, 'id' | 'name'> | 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<Variable[]>(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<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value)
|
||||
onOpen()
|
||||
if (e.target.value === '') {
|
||||
setFilteredItems([...variables.slice(0, 50)])
|
||||
onSelectVariable(undefined)
|
||||
return
|
||||
}
|
||||
setFilteredItems([
|
||||
|
||||
@@ -27,18 +27,19 @@ export const FontSelector = ({
|
||||
)
|
||||
}
|
||||
|
||||
const handleFontSelected = (nextFont: string) => {
|
||||
if (nextFont == currentFont) return
|
||||
setCurrentFont(nextFont)
|
||||
onSelectFont(nextFont)
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Font</Text>
|
||||
<SearchableDropdown
|
||||
selectedItem={activeFont}
|
||||
items={googleFonts}
|
||||
onSelectItem={(nextFont) => {
|
||||
if (nextFont !== currentFont) {
|
||||
setCurrentFont(nextFont)
|
||||
onSelectFont(nextFont)
|
||||
}
|
||||
}}
|
||||
onValueChange={handleFontSelected}
|
||||
/>
|
||||
</Flex>
|
||||
)
|
||||
|
||||
@@ -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<Typebot | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const [localPublishedTypebot, setLocalPublishedTypebot] =
|
||||
useState<PublicTypebot>()
|
||||
const [isSavingLoading, setIsSavingLoading] = useState(false)
|
||||
@@ -214,6 +218,7 @@ export const TypebotContext = ({
|
||||
...choiceItemsAction(setLocalTypebot as Updater<Typebot>),
|
||||
...variablesAction(setLocalTypebot as Updater<Typebot>),
|
||||
...edgesAction(setLocalTypebot as Updater<Typebot>),
|
||||
...webhooksAction(setLocalTypebot as Updater<Typebot>),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -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<Typebot>): 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)
|
||||
|
||||
@@ -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, 'id'> | Variable) => void
|
||||
createVariable: (variable: Variable) => void
|
||||
updateVariable: (
|
||||
variableId: string,
|
||||
updates: Partial<Omit<Variable, 'id'>>
|
||||
@@ -15,10 +14,10 @@ export type VariablesActions = {
|
||||
export const variablesAction = (
|
||||
setTypebot: Updater<Typebot>
|
||||
): VariablesActions => ({
|
||||
createVariable: (variable: Omit<Variable, 'id'> | 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<Typebot>,
|
||||
variable: Omit<Variable, 'id'> | Variable
|
||||
) => {
|
||||
const newVariable = {
|
||||
...variable,
|
||||
id: 'id' in variable ? variable.id : generate(),
|
||||
}
|
||||
typebot.variables.byId[newVariable.id] = newVariable
|
||||
typebot.variables.allIds.push(newVariable.id)
|
||||
}
|
||||
|
||||
41
apps/builder/contexts/TypebotContext/actions/webhooks.ts
Normal file
41
apps/builder/contexts/TypebotContext/actions/webhooks.ts
Normal file
@@ -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<Omit<Webhook, 'id'>>
|
||||
) => void
|
||||
deleteWebhook: (variableId: string) => void
|
||||
}
|
||||
|
||||
export const webhooksAction = (
|
||||
setTypebot: Updater<Typebot>
|
||||
): WebhooksAction => ({
|
||||
createWebhook: (newWebhook: Webhook) => {
|
||||
setTypebot((typebot) => {
|
||||
typebot.webhooks.byId[newWebhook.id] = newWebhook
|
||||
typebot.webhooks.allIds.push(newWebhook.id)
|
||||
})
|
||||
},
|
||||
updateWebhook: (webhookId: string, updates: Partial<Omit<Webhook, 'id'>>) =>
|
||||
setTypebot((typebot) => {
|
||||
typebot.webhooks.byId[webhookId] = {
|
||||
...typebot.webhooks.byId[webhookId],
|
||||
...updates,
|
||||
}
|
||||
}),
|
||||
deleteWebhook: (webhookId: string) => {
|
||||
setTypebot(deleteWebhookDraft(webhookId))
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteWebhookDraft =
|
||||
(webhookId?: string) => (typebot: WritableDraft<Typebot>) => {
|
||||
if (!webhookId) return
|
||||
delete typebot.webhooks.byId[webhookId]
|
||||
const index = typebot.webhooks.allIds.indexOf(webhookId)
|
||||
if (index !== -1) typebot.webhooks.allIds.splice(index, 1)
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"ownerId": "ckylsz8yy0335z31amvq0jwtt",
|
||||
"publishedTypebotId": null,
|
||||
"folderId": null,
|
||||
"webhooks": { "byId": {}, "allIds": [] },
|
||||
"blocks": {
|
||||
"byId": {
|
||||
"j24wz82YG3rjXMgrmCiTLy": {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"ownerId": "ckyltekzq0533z31ad8opmacz",
|
||||
"publishedTypebotId": null,
|
||||
"folderId": null,
|
||||
"webhooks": { "byId": {}, "allIds": [] },
|
||||
"blocks": {
|
||||
"byId": {
|
||||
"kPupUcEn7TcBGKHUpgK2Q5": {
|
||||
|
||||
150
apps/builder/cypress/fixtures/typebots/integrations/webhook.json
Normal file
150
apps/builder/cypress/fixtures/typebots/integrations/webhook.json
Normal file
@@ -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": "<div>Ready?</div>",
|
||||
"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
|
||||
}
|
||||
@@ -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": "<div>Ready?</div>",
|
||||
"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": "<div>His name is {{Name}}</div>",
|
||||
"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
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 283 KiB |
@@ -6,6 +6,7 @@
|
||||
"ownerId": "ckylsbdf60088z31ayqytest6",
|
||||
"publishedTypebotId": null,
|
||||
"folderId": null,
|
||||
"webhooks": { "byId": {}, "allIds": [] },
|
||||
"blocks": {
|
||||
"byId": {
|
||||
"2x83WHtEBkiv7pk7KgqJwZ": {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"ownerId": "ckymkff1100362z1a85juyoa8",
|
||||
"publishedTypebotId": null,
|
||||
"folderId": null,
|
||||
"webhooks": { "byId": {}, "allIds": [] },
|
||||
"blocks": {
|
||||
"byId": {
|
||||
"bsVJfEW7EZrUnAi9s5ev17": {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"ownerId": "ckylrpsmt0006fn1ah956d0z1",
|
||||
"publishedTypebotId": null,
|
||||
"folderId": null,
|
||||
"webhooks": { "byId": {}, "allIds": [] },
|
||||
"blocks": {
|
||||
"byId": {
|
||||
"kmUzhRFzSKjkaipYNcku9S": {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"ownerId": "ckylsr4fi0220z31apbinpy9d",
|
||||
"publishedTypebotId": null,
|
||||
"folderId": null,
|
||||
"webhooks": { "byId": {}, "allIds": [] },
|
||||
"blocks": {
|
||||
"byId": {
|
||||
"weeBMMXxNKwEonMfDX8Z5k": {
|
||||
|
||||
@@ -75,6 +75,7 @@ export const parseTestTypebot = ({
|
||||
publishedTypebotId: null,
|
||||
updatedAt: new Date(),
|
||||
variables: { byId: {}, allIds: [] },
|
||||
webhooks: { byId: {}, allIds: [] },
|
||||
edges: {
|
||||
byId: {
|
||||
edge1: {
|
||||
|
||||
@@ -52,7 +52,7 @@ describe('Image bubbles', () => {
|
||||
.should('include', unsplashImageSrc)
|
||||
})
|
||||
|
||||
it.only('should import giphy gifs correctly', () => {
|
||||
it('should import giphy gifs correctly', () => {
|
||||
cy.findByRole('button', { name: 'Giphy' }).click()
|
||||
cy.findAllByRole('img').eq(3).click()
|
||||
cy.findAllByRole('img')
|
||||
|
||||
96
apps/builder/cypress/tests/integrations/webhooks.ts
Normal file
96
apps/builder/cypress/tests/integrations/webhooks.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { preventUserFromRefreshing } from 'cypress/plugins/utils'
|
||||
import { getIframeBody } from 'cypress/support'
|
||||
|
||||
describe('Webhook step', () => {
|
||||
beforeEach(() => {
|
||||
cy.task('seed')
|
||||
cy.signOut()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cy.window().then((win) => {
|
||||
win.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Configuration', () => {
|
||||
it('configuration is working', () => {
|
||||
cy.loadTypebotFixtureInDatabase('typebots/integrations/webhook.json')
|
||||
cy.signIn('test2@gmail.com')
|
||||
cy.visit('/typebots/typebot4/edit')
|
||||
cy.findByText('Configure...').click()
|
||||
cy.findByRole('button', { name: 'GET' }).click()
|
||||
cy.findByRole('menuitem', { name: 'POST' }).click({ force: true })
|
||||
cy.findByPlaceholderText('Your Webhook URL...').type(
|
||||
`${Cypress.env('SITE_NAME')}/api/mock/webhook`
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Query params' }).click()
|
||||
cy.findByRole('textbox', { name: 'Key:' }).type('firstParam')
|
||||
cy.findByRole('textbox', { name: 'Value:' }).type('{{secret 1}}', {
|
||||
parseSpecialCharSequences: false,
|
||||
})
|
||||
cy.findByRole('button', { name: 'Add a param' }).click()
|
||||
cy.findAllByRole('textbox', { name: 'Key:' }).last().type('secondParam')
|
||||
cy.findAllByRole('textbox', { name: 'Value:' })
|
||||
.last()
|
||||
.type('{{secret 2}}', {
|
||||
parseSpecialCharSequences: false,
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Headers' }).click()
|
||||
cy.findAllByRole('textbox', { name: 'Key:' })
|
||||
.last()
|
||||
.type('Custom-Typebot')
|
||||
cy.findAllByRole('textbox', { name: 'Value:' })
|
||||
.last()
|
||||
.type('{{secret 3}}', {
|
||||
parseSpecialCharSequences: false,
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Body' }).click()
|
||||
cy.findByTestId('code-editor').type('{ "customField": "{{secret 4}}" }', {
|
||||
parseSpecialCharSequences: false,
|
||||
waitForAnimations: false,
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Variable values for test' }).click()
|
||||
addTestVariable('secret 1', 'secret1')
|
||||
cy.findByRole('button', { name: 'Add an entry' }).click()
|
||||
addTestVariable('secret 2', 'secret2')
|
||||
cy.findByRole('button', { name: 'Add an entry' }).click()
|
||||
addTestVariable('secret 3', 'secret3')
|
||||
cy.findByRole('button', { name: 'Add an entry' }).click()
|
||||
addTestVariable('secret 4', 'secret4')
|
||||
|
||||
cy.findByRole('button', { name: 'Test the request' }).click()
|
||||
|
||||
cy.findAllByTestId('code-editor')
|
||||
.should('have.length', 2)
|
||||
.last()
|
||||
.should('contain.text', '"statusCode": 200')
|
||||
|
||||
cy.findByRole('button', { name: 'Save in variables' }).click()
|
||||
cy.findByPlaceholderText('Select the data').click()
|
||||
cy.findByRole('menuitem', { name: 'data[0].name' }).click()
|
||||
})
|
||||
})
|
||||
describe('Preview', () => {
|
||||
it('should correctly send the request', () => {
|
||||
cy.loadTypebotFixtureInDatabase(
|
||||
'typebots/integrations/webhookPreview.json'
|
||||
)
|
||||
cy.signIn('test2@gmail.com')
|
||||
cy.visit('/typebots/typebot4/edit')
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody().findByRole('button', { name: 'Go' }).click()
|
||||
getIframeBody().findByText('His name is John').should('exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const addTestVariable = (name: string, value: string) => {
|
||||
cy.findAllByTestId('variables-input').last().click()
|
||||
cy.findByRole('menuitem', { name }).click()
|
||||
cy.findAllByRole('textbox', { name: 'Test value:' }).last().type(value)
|
||||
}
|
||||
@@ -22,14 +22,14 @@ describe('Condition step', () => {
|
||||
|
||||
cy.findByTestId('variables-input').click()
|
||||
cy.findByRole('menuitem', { name: 'Age' }).click()
|
||||
cy.findByRole('button', { name: 'Equal to' }).click()
|
||||
cy.findByRole('button', { name: 'Select an operator' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Greater than' }).click()
|
||||
cy.findByPlaceholderText('Type a value...').type('80')
|
||||
|
||||
cy.findByRole('button', { name: 'Add a comparison' }).click()
|
||||
cy.findAllByTestId('variables-input').last().click()
|
||||
cy.findByRole('menuitem', { name: 'Age' }).click()
|
||||
cy.findByRole('button', { name: 'Equal to' }).click()
|
||||
cy.findByRole('button', { name: 'Select an operator' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Less than' }).click()
|
||||
cy.findAllByPlaceholderText('Type a value...').last().type('100')
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('Condition step', () => {
|
||||
|
||||
cy.findByTestId('variables-input').click()
|
||||
cy.findByRole('menuitem', { name: 'Age' }).click()
|
||||
cy.findByRole('button', { name: 'Equal to' }).click()
|
||||
cy.findByRole('button', { name: 'Select an operator' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Greater than' }).click()
|
||||
cy.findByPlaceholderText('Type a value...').type('20')
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
"dependencies": {
|
||||
"@chakra-ui/css-reset": "^1.1.1",
|
||||
"@chakra-ui/react": "^1.7.4",
|
||||
"@codemirror/basic-setup": "^0.19.1",
|
||||
"@codemirror/lang-json": "^0.19.1",
|
||||
"@codemirror/text": "^0.19.6",
|
||||
"@dnd-kit/core": "^4.0.3",
|
||||
"@dnd-kit/sortable": "^5.1.0",
|
||||
"@emotion/react": "^11.7.1",
|
||||
@@ -36,6 +39,7 @@
|
||||
"framer-motion": "^4",
|
||||
"google-auth-library": "^7.11.0",
|
||||
"google-spreadsheet": "^3.2.0",
|
||||
"got": "^12.0.1",
|
||||
"htmlparser2": "^7.2.0",
|
||||
"immer": "^9.0.7",
|
||||
"js-video-url-parser": "^0.5.1",
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'assets/styles/routerProgressBar.css'
|
||||
import 'assets/styles/plate.css'
|
||||
import 'focus-visible/dist/focus-visible'
|
||||
import 'assets/styles/submissionsTable.css'
|
||||
import 'assets/styles/codeMirror.css'
|
||||
|
||||
const App = ({ Component, pageProps }: AppProps) => {
|
||||
useRouterProgressBar()
|
||||
|
||||
26
apps/builder/pages/api/mock/webhook.ts
Normal file
26
apps/builder/pages/api/mock/webhook.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { methodNotAllowed } from 'utils'
|
||||
|
||||
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'POST') {
|
||||
const firstParam = req.query.firstParam.toString()
|
||||
const secondParam = req.query.secondParam.toString()
|
||||
const customHeader = req.headers['custom-typebot']
|
||||
const { body } = req
|
||||
if (
|
||||
body.customField === 'secret4' &&
|
||||
customHeader === 'secret3' &&
|
||||
secondParam === 'secret2' &&
|
||||
firstParam === 'secret1'
|
||||
) {
|
||||
return res.status(200).send([
|
||||
{ name: 'John', age: 21 },
|
||||
{ name: 'Tim', age: 52 },
|
||||
])
|
||||
}
|
||||
return res.status(400).send({ message: 'Bad request' })
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default handler
|
||||
@@ -0,0 +1,104 @@
|
||||
import prisma from 'libs/prisma'
|
||||
import {
|
||||
KeyValue,
|
||||
Table,
|
||||
Typebot,
|
||||
Variable,
|
||||
Webhook,
|
||||
WebhookResponse,
|
||||
} from 'models'
|
||||
import { parseVariables } from 'bot-engine'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import got, { Method, Headers, HTTPError } from 'got'
|
||||
import { methodNotAllowed } from 'utils'
|
||||
import { stringify } from 'qs'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'POST') {
|
||||
const typebotId = req.query.typebotId.toString()
|
||||
const webhookId = req.query.id.toString()
|
||||
const variables = JSON.parse(req.body).variables as Table<Variable>
|
||||
const typebot = await prisma.typebot.findUnique({
|
||||
where: { id: typebotId },
|
||||
})
|
||||
const webhook = (typebot as Typebot).webhooks.byId[webhookId]
|
||||
const result = await executeWebhook(webhook, variables)
|
||||
return res.status(200).send(result)
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
const executeWebhook = async (
|
||||
webhook: Webhook,
|
||||
variables: Table<Variable>
|
||||
): Promise<WebhookResponse> => {
|
||||
if (!webhook.url || !webhook.method)
|
||||
return {
|
||||
statusCode: 400,
|
||||
data: { message: `Webhook doesn't have url or method` },
|
||||
}
|
||||
const headers = convertKeyValueTableToObject(webhook.headers, variables) as
|
||||
| Headers
|
||||
| undefined
|
||||
const queryParams = stringify(
|
||||
convertKeyValueTableToObject(webhook.queryParams, variables)
|
||||
)
|
||||
const contentType = headers ? headers['Content-Type'] : undefined
|
||||
try {
|
||||
const response = await got(
|
||||
parseVariables({ text: webhook.url + `?${queryParams}`, variables }),
|
||||
{
|
||||
method: webhook.method as Method,
|
||||
headers,
|
||||
json:
|
||||
contentType !== 'x-www-form-urlencoded' && webhook.body
|
||||
? JSON.parse(parseVariables({ text: webhook.body, variables }))
|
||||
: undefined,
|
||||
form:
|
||||
contentType === 'x-www-form-urlencoded' && webhook.body
|
||||
? JSON.parse(parseVariables({ text: webhook.body, variables }))
|
||||
: undefined,
|
||||
}
|
||||
)
|
||||
return {
|
||||
statusCode: response.statusCode,
|
||||
data: parseBody(response.body),
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError) {
|
||||
return {
|
||||
statusCode: error.response.statusCode,
|
||||
data: parseBody(error.response.body as string),
|
||||
}
|
||||
}
|
||||
return {
|
||||
statusCode: 500,
|
||||
data: { message: `Error from Typebot server: ${error}` },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parseBody = (body: string) => {
|
||||
try {
|
||||
return JSON.parse(body)
|
||||
} catch (err) {
|
||||
return body
|
||||
}
|
||||
}
|
||||
|
||||
const convertKeyValueTableToObject = (
|
||||
keyValues: Table<KeyValue> | undefined,
|
||||
variables: Table<Variable>
|
||||
) => {
|
||||
if (!keyValues) return
|
||||
return keyValues.allIds.reduce((object, id) => {
|
||||
const item = keyValues.byId[id]
|
||||
if (!item.key) return {}
|
||||
return {
|
||||
...object,
|
||||
[item.key]: parseVariables({ text: item.value, variables }),
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
||||
export default handler
|
||||
@@ -2,6 +2,7 @@ import { sendRequest } from 'utils'
|
||||
import { stringify } from 'qs'
|
||||
import useSWR from 'swr'
|
||||
import { fetcher } from './utils'
|
||||
import { Table, Variable, VariableForTest, WebhookResponse } from 'models'
|
||||
|
||||
export const getGoogleSheetsConsentScreenUrl = (
|
||||
redirectUrl: string,
|
||||
@@ -64,3 +65,68 @@ export const useSheets = ({
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
export const executeWebhook = (
|
||||
typebotId: string,
|
||||
webhookId: string,
|
||||
variables: Table<Variable>
|
||||
) =>
|
||||
sendRequest<WebhookResponse>({
|
||||
url: `/api/typebots/${typebotId}/webhooks/${webhookId}/execute`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
variables,
|
||||
},
|
||||
})
|
||||
|
||||
export const convertVariableForTestToVariables = (
|
||||
variablesForTest: Table<VariableForTest> | undefined,
|
||||
variables: Table<Variable>
|
||||
): Table<Variable> => {
|
||||
if (!variablesForTest) return { byId: {}, allIds: [] }
|
||||
return {
|
||||
byId: {
|
||||
...variables.byId,
|
||||
...variablesForTest.allIds.reduce((obj, id) => {
|
||||
const variableForTest = variablesForTest.byId[id]
|
||||
if (!variableForTest.variableId) return {}
|
||||
const variable = variables.byId[variableForTest.variableId ?? '']
|
||||
return {
|
||||
...obj,
|
||||
[variableForTest.variableId]: {
|
||||
...variable,
|
||||
value: variableForTest.value,
|
||||
},
|
||||
}
|
||||
}, {}),
|
||||
},
|
||||
allIds: variables.allIds,
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const getDeepKeys = (obj: any): string[] => {
|
||||
let keys: string[] = []
|
||||
for (const key in obj) {
|
||||
if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
|
||||
const subkeys = getDeepKeys(obj[key])
|
||||
keys = keys.concat(
|
||||
subkeys.map(function (subkey) {
|
||||
return key + '.' + subkey
|
||||
})
|
||||
)
|
||||
} else if (Array.isArray(obj[key])) {
|
||||
for (let i = 0; i < obj[key].length; i++) {
|
||||
const subkeys = getDeepKeys(obj[key][i])
|
||||
keys = keys.concat(
|
||||
subkeys.map(function (subkey) {
|
||||
return key + '[' + i + ']' + '.' + subkey
|
||||
})
|
||||
)
|
||||
}
|
||||
} else {
|
||||
keys.push(key)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
@@ -198,7 +198,10 @@ export const parseNewTypebot = ({
|
||||
ownerId: string
|
||||
folderId: string | null
|
||||
name: string
|
||||
}): Partial<Typebot> => {
|
||||
}): Omit<
|
||||
Typebot,
|
||||
'createdAt' | 'updatedAt' | 'id' | 'publishedTypebotId' | 'publicId'
|
||||
> => {
|
||||
const startBlockId = shortId.generate()
|
||||
const startStepId = shortId.generate()
|
||||
const startStep: StartStep = {
|
||||
@@ -235,6 +238,7 @@ export const parseNewTypebot = ({
|
||||
choiceItems: { byId: {}, allIds: [] },
|
||||
variables: { byId: {}, allIds: [] },
|
||||
edges: { byId: {}, allIds: [] },
|
||||
webhooks: { byId: {}, allIds: [] },
|
||||
theme,
|
||||
settings,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user