feat(integration): ✨ Add webhooks
This commit is contained in:
@ -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)
|
||||
|
Reference in New Issue
Block a user