feat(inputs): ✨ Add Set variable step
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
import { Button, ButtonProps, Flex, HStack } from '@chakra-ui/react'
|
||||
import { BubbleStepType, InputStepType, StepType } from 'models'
|
||||
import { BubbleStepType, InputStepType, StepType, LogicStepType } from 'models'
|
||||
import { useDnd } from 'contexts/DndContext'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { StepIcon } from './StepIcon'
|
||||
@ -9,10 +9,10 @@ export const StepCard = ({
|
||||
type,
|
||||
onMouseDown,
|
||||
}: {
|
||||
type: BubbleStepType | InputStepType
|
||||
type: BubbleStepType | InputStepType | LogicStepType
|
||||
onMouseDown: (
|
||||
e: React.MouseEvent,
|
||||
type: BubbleStepType | InputStepType
|
||||
type: BubbleStepType | InputStepType | LogicStepType
|
||||
) => void
|
||||
}) => {
|
||||
const { draggedStepType } = useDnd()
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
CalendarIcon,
|
||||
ChatIcon,
|
||||
CheckSquareIcon,
|
||||
EditIcon,
|
||||
EmailIcon,
|
||||
FlagIcon,
|
||||
GlobeIcon,
|
||||
@ -10,7 +11,7 @@ import {
|
||||
PhoneIcon,
|
||||
TextIcon,
|
||||
} from 'assets/icons'
|
||||
import { BubbleStepType, InputStepType, StepType } from 'models'
|
||||
import { BubbleStepType, InputStepType, LogicStepType, StepType } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type StepIconProps = { type: StepType } & IconProps
|
||||
@ -41,6 +42,9 @@ export const StepIcon = ({ type, ...props }: StepIconProps) => {
|
||||
case InputStepType.CHOICE: {
|
||||
return <CheckSquareIcon {...props} />
|
||||
}
|
||||
case LogicStepType.SET_VARIABLE: {
|
||||
return <EditIcon {...props} />
|
||||
}
|
||||
case 'start': {
|
||||
return <FlagIcon {...props} />
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { BubbleStepType, InputStepType, StepType } from 'models'
|
||||
import { BubbleStepType, InputStepType, LogicStepType, StepType } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type Props = { type: StepType }
|
||||
@ -28,6 +28,9 @@ export const StepTypeLabel = ({ type }: Props) => {
|
||||
case InputStepType.CHOICE: {
|
||||
return <Text>Button</Text>
|
||||
}
|
||||
case LogicStepType.SET_VARIABLE: {
|
||||
return <Text>Set variable</Text>
|
||||
}
|
||||
default: {
|
||||
return <></>
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
SimpleGrid,
|
||||
useEventListener,
|
||||
} from '@chakra-ui/react'
|
||||
import { BubbleStepType, InputStepType } from 'models'
|
||||
import { BubbleStepType, InputStepType, LogicStepType } from 'models'
|
||||
import { useDnd } from 'contexts/DndContext'
|
||||
import React, { useState } from 'react'
|
||||
import { StepCard, StepCardOverlay } from './StepCard'
|
||||
@ -31,7 +31,7 @@ export const StepTypesList = () => {
|
||||
|
||||
const handleMouseDown = (
|
||||
e: React.MouseEvent,
|
||||
type: BubbleStepType | InputStepType
|
||||
type: BubbleStepType | InputStepType | LogicStepType
|
||||
) => {
|
||||
const element = e.currentTarget as HTMLDivElement
|
||||
const rect = element.getBoundingClientRect()
|
||||
@ -85,6 +85,15 @@ export const StepTypesList = () => {
|
||||
<StepCard key={type} type={type} onMouseDown={handleMouseDown} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
<Text fontSize="sm" fontWeight="semibold" color="gray.600">
|
||||
Logic
|
||||
</Text>
|
||||
<SimpleGrid columns={2} spacing="2">
|
||||
{Object.values(LogicStepType).map((type) => (
|
||||
<StepCard key={type} type={type} onMouseDown={handleMouseDown} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
{draggedStepType && (
|
||||
<StepCardOverlay
|
||||
type={draggedStepType}
|
||||
|
@ -4,6 +4,9 @@ import {
|
||||
ChoiceInputOptions,
|
||||
InputStep,
|
||||
InputStepType,
|
||||
LogicStepType,
|
||||
SetVariableOptions,
|
||||
Step,
|
||||
TextInputOptions,
|
||||
} from 'models'
|
||||
import {
|
||||
@ -15,10 +18,12 @@ import {
|
||||
} from './bodies'
|
||||
import { ChoiceInputSettingsBody } from './bodies/ChoiceInputSettingsBody'
|
||||
import { PhoneNumberSettingsBody } from './bodies/PhoneNumberSettingsBody'
|
||||
import { SetVariableSettingsBody } from './bodies/SetVariableSettingsBody'
|
||||
|
||||
type Props = {
|
||||
step: InputStep
|
||||
step: Step
|
||||
}
|
||||
|
||||
export const SettingsPopoverContent = ({ step }: Props) => {
|
||||
const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation()
|
||||
|
||||
@ -35,7 +40,7 @@ export const SettingsPopoverContent = ({ step }: Props) => {
|
||||
const SettingsPopoverBodyContent = ({ step }: Props) => {
|
||||
const { updateStep } = useTypebot()
|
||||
const handleOptionsChange = (
|
||||
options: TextInputOptions | ChoiceInputOptions
|
||||
options: TextInputOptions | ChoiceInputOptions | SetVariableOptions
|
||||
) => updateStep(step.id, { options } as Partial<InputStep>)
|
||||
|
||||
switch (step.type) {
|
||||
@ -95,6 +100,14 @@ const SettingsPopoverBodyContent = ({ step }: Props) => {
|
||||
/>
|
||||
)
|
||||
}
|
||||
case LogicStepType.SET_VARIABLE: {
|
||||
return (
|
||||
<SetVariableSettingsBody
|
||||
options={step.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
return <></>
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { DebouncedInput } from 'components/shared/DebouncedInput'
|
||||
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||
import { ChoiceInputOptions } from 'models'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { ChoiceInputOptions, Variable } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type ChoiceInputSettingsBodyProps = {
|
||||
@ -17,6 +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 })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
@ -39,6 +42,15 @@ export const ChoiceInputSettingsBody = ({
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable">
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options?.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { DebouncedInput } from 'components/shared/DebouncedInput'
|
||||
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||
import { DateInputOptions } from 'models'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { DateInputOptions, Variable } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type DateInputSettingsBodyProps = {
|
||||
@ -23,6 +24,8 @@ export const DateInputSettingsBody = ({
|
||||
onOptionsChange({ ...options, isRange })
|
||||
const handleHasTimeChange = (hasTime: boolean) =>
|
||||
onOptionsChange({ ...options, hasTime })
|
||||
const handleVariableChange = (variable: Variable) =>
|
||||
onOptionsChange({ ...options, variableId: variable.id })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
@ -75,6 +78,15 @@ export const DateInputSettingsBody = ({
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable">
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options?.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { DebouncedInput } from 'components/shared/DebouncedInput'
|
||||
import { EmailInputOptions } from 'models'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { EmailInputOptions, Variable } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type EmailInputSettingsBodyProps = {
|
||||
@ -16,6 +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 })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
@ -41,6 +44,15 @@ export const EmailInputSettingsBody = ({
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable">
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options?.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { FormLabel, HStack, Stack } from '@chakra-ui/react'
|
||||
import { SmartNumberInput } from 'components/settings/SmartNumberInput'
|
||||
import { DebouncedInput } from 'components/shared/DebouncedInput'
|
||||
import { NumberInputOptions } from 'models'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { NumberInputOptions, Variable } from 'models'
|
||||
import React from 'react'
|
||||
import { removeUndefinedFields } from 'services/utils'
|
||||
|
||||
@ -24,6 +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 })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
@ -79,6 +82,15 @@ export const NumberInputSettingsBody = ({
|
||||
onValueChange={handleStepChange}
|
||||
/>
|
||||
</HStack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable">
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options?.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { DebouncedInput } from 'components/shared/DebouncedInput'
|
||||
import { EmailInputOptions } from 'models'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { EmailInputOptions, Variable } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type PhoneNumberSettingsBodyProps = {
|
||||
@ -16,6 +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 })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
@ -41,6 +44,15 @@ export const PhoneNumberSettingsBody = ({
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable">
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options?.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,46 @@
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { DebouncedTextarea } from 'components/shared/DebouncedTextarea'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { SetVariableOptions, Variable } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
options?: SetVariableOptions
|
||||
onOptionsChange: (options: SetVariableOptions) => void
|
||||
}
|
||||
|
||||
export const SetVariableSettingsBody = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
}: Props) => {
|
||||
const handleVariableChange = (variable: Variable) =>
|
||||
onOptionsChange({ ...options, variableId: variable.id })
|
||||
const handleExpressionChange = (expressionToEvaluate: string) =>
|
||||
onOptionsChange({ ...options, expressionToEvaluate })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable-search">
|
||||
Search or create variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
onSelectVariable={handleVariableChange}
|
||||
initialVariableId={options?.variableId}
|
||||
id="variable-search"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="expression">
|
||||
Value / Expression:
|
||||
</FormLabel>
|
||||
<DebouncedTextarea
|
||||
id="expression"
|
||||
initialValue={options?.expressionToEvaluate ?? ''}
|
||||
delay={100}
|
||||
onChange={handleExpressionChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { DebouncedInput } from 'components/shared/DebouncedInput'
|
||||
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||
import { TextInputOptions } from 'models'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { TextInputOptions, Variable } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type TextInputSettingsBodyProps = {
|
||||
@ -19,6 +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 })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
@ -50,6 +53,15 @@ export const TextInputSettingsBody = ({
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable">
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options?.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { DebouncedInput } from 'components/shared/DebouncedInput'
|
||||
import { UrlInputOptions } from 'models'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { UrlInputOptions, Variable } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type UrlInputSettingsBodyProps = {
|
||||
@ -16,6 +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 })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
@ -41,6 +44,15 @@ export const UrlInputSettingsBody = ({
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable">
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options?.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
@ -10,7 +10,13 @@ import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { Block, Step } from 'models'
|
||||
import { useGraph } from 'contexts/GraphContext'
|
||||
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
|
||||
import { isChoiceInput, isDefined, isInputStep, isTextBubbleStep } from 'utils'
|
||||
import {
|
||||
isChoiceInput,
|
||||
isDefined,
|
||||
isInputStep,
|
||||
isLogicStep,
|
||||
isTextBubbleStep,
|
||||
} from 'utils'
|
||||
import { Coordinates } from '@dnd-kit/core/dist/types'
|
||||
import { TextEditor } from './TextEditor/TextEditor'
|
||||
import { StepNodeContent } from './StepNodeContent'
|
||||
@ -212,7 +218,9 @@ export const StepNode = ({
|
||||
)}
|
||||
</Flex>
|
||||
</PopoverTrigger>
|
||||
{isInputStep(step) && <SettingsPopoverContent step={step} />}
|
||||
{(isInputStep(step) || isLogicStep(step)) && (
|
||||
<SettingsPopoverContent step={step} />
|
||||
)}
|
||||
</Popover>
|
||||
)}
|
||||
</ContextMenu>
|
||||
|
@ -1,5 +1,13 @@
|
||||
import { Flex, Text } from '@chakra-ui/react'
|
||||
import { Step, StartStep, BubbleStepType, InputStepType } from 'models'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import {
|
||||
Step,
|
||||
StartStep,
|
||||
BubbleStepType,
|
||||
InputStepType,
|
||||
LogicStepType,
|
||||
SetVariableStep,
|
||||
} from 'models'
|
||||
import { ChoiceItemsList } from './ChoiceInputStepNode/ChoiceItemsList'
|
||||
|
||||
type Props = {
|
||||
@ -68,6 +76,9 @@ export const StepNodeContent = ({ step }: Props) => {
|
||||
case InputStepType.CHOICE: {
|
||||
return <ChoiceItemsList step={step} />
|
||||
}
|
||||
case LogicStepType.SET_VARIABLE: {
|
||||
return <SetVariableNodeContent step={step} />
|
||||
}
|
||||
case 'start': {
|
||||
return <Text>{step.label}</Text>
|
||||
}
|
||||
@ -76,3 +87,17 @@ export const StepNodeContent = ({ step }: Props) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Stack, useOutsideClick } from '@chakra-ui/react'
|
||||
import { Flex, Stack, useOutsideClick } from '@chakra-ui/react'
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
Plate,
|
||||
@ -10,10 +10,12 @@ import {
|
||||
import { editorStyle, platePlugins } from 'libs/plate'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { createEditor } from 'slate'
|
||||
import { BaseSelection, createEditor, Transforms } from 'slate'
|
||||
import { ToolBar } from './ToolBar'
|
||||
import { parseHtmlStringToPlainText } from 'services/utils'
|
||||
import { TextStep } from 'models'
|
||||
import { TextStep, Variable } from 'models'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { ReactEditor } from 'slate-react'
|
||||
|
||||
type TextEditorProps = {
|
||||
stepId: string
|
||||
@ -26,14 +28,20 @@ export const TextEditor = ({
|
||||
stepId,
|
||||
onClose,
|
||||
}: TextEditorProps) => {
|
||||
const randomEditorId = useMemo(() => Math.random().toString(), [])
|
||||
const editor = useMemo(
|
||||
() => withPlate(createEditor(), { id: stepId, plugins: platePlugins }),
|
||||
() =>
|
||||
withPlate(createEditor(), { id: randomEditorId, plugins: platePlugins }),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
)
|
||||
const { updateStep } = useTypebot()
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [debouncedValue] = useDebounce(value, 500)
|
||||
const varDropdownRef = useRef<HTMLDivElement | null>(null)
|
||||
const rememberedSelection = useRef<BaseSelection | null>(null)
|
||||
const [isVariableDropdownOpen, setIsVariableDropdownOpen] = useState(false)
|
||||
|
||||
const textEditorRef = useRef<HTMLDivElement>(null)
|
||||
useOutsideClick({
|
||||
ref: textEditorRef,
|
||||
@ -48,6 +56,29 @@ export const TextEditor = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedValue])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVariableDropdownOpen) return
|
||||
const el = varDropdownRef.current
|
||||
if (!el) return
|
||||
const { top, left } = computeTargetCoord()
|
||||
el.style.top = `${top}px`
|
||||
el.style.left = `${left}px`
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isVariableDropdownOpen])
|
||||
|
||||
const computeTargetCoord = () => {
|
||||
const selection = window.getSelection()
|
||||
const relativeParent = textEditorRef.current
|
||||
if (!selection || !relativeParent) return { top: 0, left: 0 }
|
||||
const range = selection.getRangeAt(0)
|
||||
const selectionBoundingRect = range.getBoundingClientRect()
|
||||
const relativeRect = relativeParent.getBoundingClientRect()
|
||||
return {
|
||||
top: selectionBoundingRect.bottom - relativeRect.top,
|
||||
left: selectionBoundingRect.left - relativeRect.left,
|
||||
}
|
||||
}
|
||||
|
||||
const save = (value: unknown[]) => {
|
||||
if (value.length === 0) return
|
||||
const html = serializeHtml(editor, {
|
||||
@ -65,6 +96,19 @@ export const TextEditor = ({
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
const handleVariableSelected = (variable: Variable) => {
|
||||
setIsVariableDropdownOpen(false)
|
||||
if (!rememberedSelection.current) return
|
||||
Transforms.select(editor, rememberedSelection.current)
|
||||
Transforms.insertText(editor, '{{' + variable.name + '}}')
|
||||
ReactEditor.focus(editor as unknown as ReactEditor)
|
||||
}
|
||||
|
||||
const handleChangeEditorContent = (val: unknown[]) => {
|
||||
setValue(val)
|
||||
setIsVariableDropdownOpen(false)
|
||||
}
|
||||
return (
|
||||
<Stack
|
||||
flex="1"
|
||||
@ -73,11 +117,12 @@ export const TextEditor = ({
|
||||
borderColor="blue.500"
|
||||
rounded="md"
|
||||
onMouseDown={handleMouseDown}
|
||||
pos="relative"
|
||||
spacing={0}
|
||||
>
|
||||
<ToolBar />
|
||||
<ToolBar onVariablesButtonClick={() => setIsVariableDropdownOpen(true)} />
|
||||
<Plate
|
||||
id={stepId}
|
||||
id={randomEditorId}
|
||||
editableProps={{
|
||||
style: editorStyle,
|
||||
autoFocus: true,
|
||||
@ -88,15 +133,34 @@ export const TextEditor = ({
|
||||
})
|
||||
},
|
||||
'aria-label': 'Text editor',
|
||||
onBlur: () => {
|
||||
rememberedSelection.current = editor.selection
|
||||
},
|
||||
}}
|
||||
initialValue={
|
||||
initialValue.length === 0
|
||||
? [{ type: 'p', children: [{ text: '' }] }]
|
||||
: initialValue
|
||||
}
|
||||
onChange={setValue}
|
||||
onChange={handleChangeEditorContent}
|
||||
editor={editor}
|
||||
/>
|
||||
{isVariableDropdownOpen && (
|
||||
<Flex
|
||||
pos="absolute"
|
||||
ref={varDropdownRef}
|
||||
shadow="lg"
|
||||
rounded="md"
|
||||
bgColor="white"
|
||||
w="250px"
|
||||
>
|
||||
<VariableSearchInput
|
||||
onSelectVariable={handleVariableSelected}
|
||||
placeholder="Search for a variable"
|
||||
isDefaultOpen
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
@ -9,8 +9,14 @@ import { LinkToolbarButton } from '@udecode/plate-ui-link'
|
||||
import { MarkToolbarButton } from '@udecode/plate-ui-toolbar'
|
||||
import { BoldIcon, ItalicIcon, UnderlineIcon, LinkIcon } from 'assets/icons'
|
||||
|
||||
export const ToolBar = (props: StackProps) => {
|
||||
type Props = { onVariablesButtonClick: () => void } & StackProps
|
||||
export const ToolBar = (props: Props) => {
|
||||
const editor = usePlateEditorRef()
|
||||
|
||||
const handleVariablesButtonMouseDown = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
props.onVariablesButtonClick()
|
||||
}
|
||||
return (
|
||||
<HStack
|
||||
bgColor={'white'}
|
||||
@ -21,7 +27,9 @@ export const ToolBar = (props: StackProps) => {
|
||||
borderBottomWidth={1}
|
||||
{...props}
|
||||
>
|
||||
<Button size="sm">Variables</Button>
|
||||
<Button size="sm" onMouseDown={handleVariablesButtonMouseDown}>
|
||||
Variables
|
||||
</Button>
|
||||
<span data-testid="bold-button">
|
||||
<MarkToolbarButton
|
||||
type={getPluginType(editor, MARK_BOLD)}
|
||||
|
@ -26,7 +26,7 @@ export const PreviewDrawer = () => {
|
||||
const [restartKey, setRestartKey] = useState(0)
|
||||
|
||||
const publicTypebot = useMemo(
|
||||
() => (typebot ? parseTypebotToPublicTypebot(typebot) : undefined),
|
||||
() => (typebot ? { ...parseTypebotToPublicTypebot(typebot) } : undefined),
|
||||
[typebot]
|
||||
)
|
||||
|
||||
|
38
apps/builder/components/shared/DebouncedTextarea.tsx
Normal file
38
apps/builder/components/shared/DebouncedTextarea.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { Textarea, TextareaProps } from '@chakra-ui/react'
|
||||
import { ChangeEvent, useEffect, useState } from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
|
||||
type Props = Omit<TextareaProps, 'onChange' | 'value'> & {
|
||||
delay: number
|
||||
initialValue: string
|
||||
onChange: (debouncedValue: string) => void
|
||||
}
|
||||
|
||||
export const DebouncedTextarea = ({
|
||||
delay,
|
||||
onChange,
|
||||
initialValue,
|
||||
...props
|
||||
}: Props) => {
|
||||
const [currentValue, setCurrentValue] = useState(initialValue)
|
||||
const [currentValueDebounced] = useDebounce(currentValue, delay)
|
||||
|
||||
useEffect(() => {
|
||||
if (currentValueDebounced === initialValue) return
|
||||
onChange(currentValueDebounced)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentValueDebounced])
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setCurrentValue(e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
{...props}
|
||||
value={currentValue}
|
||||
onChange={handleChange}
|
||||
resize={'vertical'}
|
||||
/>
|
||||
)
|
||||
}
|
170
apps/builder/components/shared/VariableSearchInput.tsx
Normal file
170
apps/builder/components/shared/VariableSearchInput.tsx
Normal file
@ -0,0 +1,170 @@
|
||||
import {
|
||||
useDisclosure,
|
||||
useOutsideClick,
|
||||
Flex,
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
Input,
|
||||
PopoverContent,
|
||||
Button,
|
||||
InputProps,
|
||||
IconButton,
|
||||
} from '@chakra-ui/react'
|
||||
import { PlusIcon, TrashIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { Variable } from 'models'
|
||||
import React, { useState, useRef, ChangeEvent, useMemo, useEffect } from 'react'
|
||||
import { generate } from 'short-uuid'
|
||||
import { isDefined } from 'utils'
|
||||
|
||||
type Props = {
|
||||
initialVariableId?: string
|
||||
onSelectVariable: (variable: Pick<Variable, 'id' | 'name'>) => void
|
||||
isDefaultOpen?: boolean
|
||||
} & InputProps
|
||||
|
||||
export const VariableSearchInput = ({
|
||||
initialVariableId,
|
||||
onSelectVariable,
|
||||
isDefaultOpen,
|
||||
...inputProps
|
||||
}: Props) => {
|
||||
const { onOpen, onClose, isOpen } = useDisclosure()
|
||||
const { typebot, createVariable, deleteVariable } = useTypebot()
|
||||
const variables = useMemo(
|
||||
() =>
|
||||
typebot?.variables.allIds.map((id) => typebot.variables.byId[id]) ?? [],
|
||||
[typebot?.variables]
|
||||
)
|
||||
const [inputValue, setInputValue] = useState(
|
||||
typebot?.variables.byId[initialVariableId ?? '']?.name ?? ''
|
||||
)
|
||||
const [filteredItems, setFilteredItems] = useState<Variable[]>(variables)
|
||||
const dropdownRef = useRef(null)
|
||||
const inputRef = useRef(null)
|
||||
|
||||
useOutsideClick({
|
||||
ref: dropdownRef,
|
||||
handler: onClose,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefaultOpen) onOpen()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value)
|
||||
onOpen()
|
||||
if (e.target.value === '') {
|
||||
setFilteredItems([...variables.slice(0, 50)])
|
||||
return
|
||||
}
|
||||
setFilteredItems([
|
||||
...variables
|
||||
.filter((item) =>
|
||||
item.name.toLowerCase().includes((e.target.value ?? '').toLowerCase())
|
||||
)
|
||||
.slice(0, 50),
|
||||
])
|
||||
}
|
||||
|
||||
const handleVariableNameClick = (variable: Variable) => () => {
|
||||
setInputValue(variable.name)
|
||||
onSelectVariable(variable)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleCreateNewVariableClick = () => {
|
||||
if (!inputValue || inputValue === '') return
|
||||
const id = generate()
|
||||
createVariable({ id, name: inputValue })
|
||||
onSelectVariable({ id, name: inputValue })
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleDeleteVariableClick =
|
||||
(variable: Variable) => (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
deleteVariable(variable.id)
|
||||
setFilteredItems(filteredItems.filter((item) => item.id !== variable.id))
|
||||
if (variable.name === inputValue) setInputValue('')
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex ref={dropdownRef} w="full">
|
||||
<Popover
|
||||
isOpen={isOpen}
|
||||
initialFocusRef={inputRef}
|
||||
matchWidth
|
||||
offset={[0, 0]}
|
||||
isLazy
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<Input
|
||||
data-testid="variables-input"
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={onInputChange}
|
||||
onClick={onOpen}
|
||||
{...inputProps}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
maxH="35vh"
|
||||
overflowY="scroll"
|
||||
spacing="0"
|
||||
role="menu"
|
||||
w="inherit"
|
||||
shadow="lg"
|
||||
>
|
||||
{(inputValue?.length ?? 0) > 0 &&
|
||||
!isDefined(variables.find((v) => v.name === inputValue)) && (
|
||||
<Button
|
||||
role="menuitem"
|
||||
minH="40px"
|
||||
onClick={handleCreateNewVariableClick}
|
||||
fontSize="16px"
|
||||
fontWeight="normal"
|
||||
rounded="none"
|
||||
colorScheme="gray"
|
||||
variant="ghost"
|
||||
justifyContent="flex-start"
|
||||
leftIcon={<PlusIcon />}
|
||||
>
|
||||
Create "{inputValue}"
|
||||
</Button>
|
||||
)}
|
||||
{filteredItems.length > 0 && (
|
||||
<>
|
||||
{filteredItems.map((item, idx) => {
|
||||
return (
|
||||
<Button
|
||||
role="menuitem"
|
||||
minH="40px"
|
||||
key={idx}
|
||||
onClick={handleVariableNameClick(item)}
|
||||
fontSize="16px"
|
||||
fontWeight="normal"
|
||||
rounded="none"
|
||||
colorScheme="gray"
|
||||
variant="ghost"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
{item.name}
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove variable"
|
||||
size="xs"
|
||||
onClick={handleDeleteVariableClick(item)}
|
||||
/>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Flex>
|
||||
)
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Text, Flex } from '@chakra-ui/react'
|
||||
import { SearchableDropdown } from './SearchableDropdown'
|
||||
import { SearchableDropdown } from '../../../shared/SearchableDropdown'
|
||||
|
||||
type FontSelectorProps = {
|
||||
activeFont?: string
|
||||
|
@ -4,6 +4,8 @@ import {
|
||||
ChoiceItem,
|
||||
InputStep,
|
||||
InputStepType,
|
||||
LogicStepType,
|
||||
LogicStep,
|
||||
} from 'models'
|
||||
import {
|
||||
createContext,
|
||||
@ -14,8 +16,8 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
export type DraggableStep = BubbleStep | InputStep
|
||||
export type DraggableStepType = BubbleStepType | InputStepType
|
||||
export type DraggableStep = BubbleStep | InputStep | LogicStep
|
||||
export type DraggableStepType = BubbleStepType | InputStepType | LogicStepType
|
||||
|
||||
const dndContext = createContext<{
|
||||
draggedStepType?: DraggableStepType
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { useToast } from '@chakra-ui/react'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { PublicTypebot, Settings, Theme, Typebot } from 'models'
|
||||
import { useRouter } from 'next/router'
|
||||
import {
|
||||
@ -28,6 +27,7 @@ import { BlocksActions, blocksActions } from './actions/blocks'
|
||||
import { useImmer, Updater } from 'use-immer'
|
||||
import { stepsAction, StepsActions } from './actions/steps'
|
||||
import { choiceItemsAction, ChoiceItemsActions } from './actions/choiceItems'
|
||||
import { variablesAction, VariablesActions } from './actions/variables'
|
||||
|
||||
type UpdateTypebotPayload = Partial<{
|
||||
theme: Theme
|
||||
@ -48,7 +48,8 @@ const typebotContext = createContext<
|
||||
publishTypebot: () => void
|
||||
} & BlocksActions &
|
||||
StepsActions &
|
||||
ChoiceItemsActions
|
||||
ChoiceItemsActions &
|
||||
VariablesActions
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
>({})
|
||||
@ -86,9 +87,10 @@ export const TypebotContext = ({
|
||||
() =>
|
||||
isDefined(typebot) &&
|
||||
isDefined(localTypebot) &&
|
||||
!deepEqual(localTypebot, typebot),
|
||||
!checkIfTypebotsAreEqual(localTypebot, typebot),
|
||||
[typebot, localTypebot]
|
||||
)
|
||||
|
||||
const isPublished = useMemo(
|
||||
() =>
|
||||
isDefined(typebot) &&
|
||||
@ -205,6 +207,7 @@ export const TypebotContext = ({
|
||||
...blocksActions(setLocalTypebot as Updater<Typebot>),
|
||||
...stepsAction(setLocalTypebot as Updater<Typebot>),
|
||||
...choiceItemsAction(setLocalTypebot as Updater<Typebot>),
|
||||
...variablesAction(setLocalTypebot as Updater<Typebot>),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -1,13 +1,22 @@
|
||||
import { Coordinates } from 'contexts/GraphContext'
|
||||
import { WritableDraft } from 'immer/dist/internal'
|
||||
import { Block, BubbleStepType, InputStepType, Step, Typebot } from 'models'
|
||||
import {
|
||||
Block,
|
||||
BubbleStepType,
|
||||
InputStepType,
|
||||
LogicStepType,
|
||||
Step,
|
||||
Typebot,
|
||||
} from 'models'
|
||||
import { parseNewBlock } from 'services/typebots'
|
||||
import { Updater } from 'use-immer'
|
||||
import { createStepDraft, deleteStepDraft } from './steps'
|
||||
|
||||
export type BlocksActions = {
|
||||
createBlock: (
|
||||
props: Coordinates & { step: BubbleStepType | InputStepType | Step }
|
||||
props: Coordinates & {
|
||||
step: BubbleStepType | InputStepType | LogicStepType | Step
|
||||
}
|
||||
) => void
|
||||
updateBlock: (blockId: string, updates: Partial<Omit<Block, 'id'>>) => void
|
||||
deleteBlock: (blockId: string) => void
|
||||
@ -18,7 +27,9 @@ export const blocksActions = (setTypebot: Updater<Typebot>): BlocksActions => ({
|
||||
x,
|
||||
y,
|
||||
step,
|
||||
}: Coordinates & { step: BubbleStepType | InputStepType | Step }) => {
|
||||
}: Coordinates & {
|
||||
step: BubbleStepType | InputStepType | LogicStepType | Step
|
||||
}) => {
|
||||
setTypebot((typebot) => {
|
||||
const newBlock = parseNewBlock({
|
||||
totalBlocks: typebot.blocks.allIds.length,
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
InputStepType,
|
||||
Step,
|
||||
Typebot,
|
||||
LogicStepType,
|
||||
} from 'models'
|
||||
import { parseNewStep } from 'services/typebots'
|
||||
import { Updater } from 'use-immer'
|
||||
@ -15,7 +16,7 @@ import { isChoiceInput } from 'utils'
|
||||
export type StepsActions = {
|
||||
createStep: (
|
||||
blockId: string,
|
||||
step: BubbleStepType | InputStepType | Step,
|
||||
step: BubbleStepType | InputStepType | LogicStepType | Step,
|
||||
index?: number
|
||||
) => void
|
||||
updateStep: (
|
||||
@ -29,7 +30,7 @@ export type StepsActions = {
|
||||
export const stepsAction = (setTypebot: Updater<Typebot>): StepsActions => ({
|
||||
createStep: (
|
||||
blockId: string,
|
||||
step: BubbleStepType | InputStepType | Step,
|
||||
step: BubbleStepType | InputStepType | LogicStepType | Step,
|
||||
index?: number
|
||||
) => {
|
||||
setTypebot((typebot) => {
|
||||
@ -75,7 +76,7 @@ export const deleteStepDraft = (
|
||||
|
||||
export const createStepDraft = (
|
||||
typebot: WritableDraft<Typebot>,
|
||||
step: BubbleStepType | InputStepType | Step,
|
||||
step: BubbleStepType | InputStepType | LogicStepType | Step,
|
||||
blockId: string,
|
||||
index?: number
|
||||
) => {
|
||||
|
60
apps/builder/contexts/TypebotContext/actions/variables.ts
Normal file
60
apps/builder/contexts/TypebotContext/actions/variables.ts
Normal file
@ -0,0 +1,60 @@
|
||||
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
|
||||
updateVariable: (
|
||||
variableId: string,
|
||||
updates: Partial<Omit<Variable, 'id'>>
|
||||
) => void
|
||||
deleteVariable: (variableId: string) => void
|
||||
}
|
||||
|
||||
export const variablesAction = (
|
||||
setTypebot: Updater<Typebot>
|
||||
): VariablesActions => ({
|
||||
createVariable: (variable: Omit<Variable, 'id'> | Variable) => {
|
||||
setTypebot((typebot) => {
|
||||
const id = createVariableDraft(typebot, variable)
|
||||
return id
|
||||
})
|
||||
},
|
||||
updateVariable: (
|
||||
variableId: string,
|
||||
updates: Partial<Omit<Variable, 'id'>>
|
||||
) =>
|
||||
setTypebot((typebot) => {
|
||||
typebot.variables.byId[variableId] = {
|
||||
...typebot.variables.byId[variableId],
|
||||
...updates,
|
||||
}
|
||||
}),
|
||||
deleteVariable: (itemId: string) => {
|
||||
setTypebot((typebot) => {
|
||||
deleteVariableDraft(typebot, itemId)
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteVariableDraft = (
|
||||
typebot: WritableDraft<Typebot>,
|
||||
variableId: string
|
||||
) => {
|
||||
delete typebot.variables.byId[variableId]
|
||||
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)
|
||||
}
|
129
apps/builder/cypress/fixtures/typebots/logic/setVariable.json
Normal file
129
apps/builder/cypress/fixtures/typebots/logic/setVariable.json
Normal file
@ -0,0 +1,129 @@
|
||||
{
|
||||
"id": "typebot4",
|
||||
"createdAt": "2022-01-13T08:10:06.705Z",
|
||||
"updatedAt": "2022-01-13T08:10:06.705Z",
|
||||
"name": "My typebot",
|
||||
"ownerId": "ckybcurfh1612li1a62gqojvj",
|
||||
"publishedTypebotId": null,
|
||||
"folderId": null,
|
||||
"blocks": {
|
||||
"byId": {
|
||||
"eXpkU5dMsjRPeY3WverF45": {
|
||||
"id": "eXpkU5dMsjRPeY3WverF45",
|
||||
"title": "Start",
|
||||
"stepIds": ["vWwzs6EmChn2PJcJQMr2gT"],
|
||||
"graphCoordinates": { "x": 0, "y": 0 }
|
||||
},
|
||||
"b87iNgPqGvKrybzHcTD4Sze": {
|
||||
"id": "b87iNgPqGvKrybzHcTD4Sze",
|
||||
"title": "Block #4",
|
||||
"stepIds": ["s8a2ASVaM3PoaD3y9amHrtT", "sc7FNu4BUCmmhmP14hk2Hij"],
|
||||
"graphCoordinates": { "x": 584, "y": 389 }
|
||||
},
|
||||
"bg22czQixaBBY8M8LJ9wEm1": {
|
||||
"id": "bg22czQixaBBY8M8LJ9wEm1",
|
||||
"title": "Block #3",
|
||||
"stepIds": ["set-var-1", "set-var-2"],
|
||||
"graphCoordinates": { "x": 581, "y": 97 }
|
||||
},
|
||||
"bvGtcdRjZtbbaimKyUD3NAW": {
|
||||
"id": "bvGtcdRjZtbbaimKyUD3NAW",
|
||||
"title": "Block #2",
|
||||
"stepIds": ["sgFoKA5Y4MXBkqFzCGCWGeg", "number-step"],
|
||||
"graphCoordinates": { "x": 190, "y": 222 }
|
||||
}
|
||||
},
|
||||
"allIds": [
|
||||
"eXpkU5dMsjRPeY3WverF45",
|
||||
"bvGtcdRjZtbbaimKyUD3NAW",
|
||||
"bg22czQixaBBY8M8LJ9wEm1",
|
||||
"b87iNgPqGvKrybzHcTD4Sze"
|
||||
]
|
||||
},
|
||||
"steps": {
|
||||
"byId": {
|
||||
"vWwzs6EmChn2PJcJQMr2gT": {
|
||||
"id": "vWwzs6EmChn2PJcJQMr2gT",
|
||||
"type": "start",
|
||||
"label": "Start",
|
||||
"target": { "blockId": "bvGtcdRjZtbbaimKyUD3NAW" },
|
||||
"blockId": "eXpkU5dMsjRPeY3WverF45"
|
||||
},
|
||||
"set-var-2": {
|
||||
"id": "set-var-2",
|
||||
"type": "Set variable",
|
||||
"target": { "blockId": "b87iNgPqGvKrybzHcTD4Sze" },
|
||||
"blockId": "bg22czQixaBBY8M8LJ9wEm1"
|
||||
},
|
||||
"s8a2ASVaM3PoaD3y9amHrtT": {
|
||||
"id": "s8a2ASVaM3PoaD3y9amHrtT",
|
||||
"type": "text",
|
||||
"blockId": "b87iNgPqGvKrybzHcTD4Sze",
|
||||
"content": {
|
||||
"html": "<div>Total: {{Total}}</div>",
|
||||
"richText": [
|
||||
{ "type": "p", "children": [{ "text": "Total: {{Total}}" }] }
|
||||
],
|
||||
"plainText": "Total: {{Total}}"
|
||||
}
|
||||
},
|
||||
"sgFoKA5Y4MXBkqFzCGCWGeg": {
|
||||
"id": "sgFoKA5Y4MXBkqFzCGCWGeg",
|
||||
"type": "text",
|
||||
"blockId": "bvGtcdRjZtbbaimKyUD3NAW",
|
||||
"content": {
|
||||
"html": "<div>1000 * ?</div>",
|
||||
"richText": [{ "type": "p", "children": [{ "text": "1000 * ?" }] }],
|
||||
"plainText": "1000 * ?"
|
||||
}
|
||||
},
|
||||
"number-step": {
|
||||
"id": "number-step",
|
||||
"type": "number input",
|
||||
"target": { "blockId": "bg22czQixaBBY8M8LJ9wEm1" },
|
||||
"blockId": "bvGtcdRjZtbbaimKyUD3NAW"
|
||||
},
|
||||
"set-var-1": {
|
||||
"id": "set-var-1",
|
||||
"blockId": "bg22czQixaBBY8M8LJ9wEm1",
|
||||
"type": "Set variable"
|
||||
},
|
||||
"sc7FNu4BUCmmhmP14hk2Hij": {
|
||||
"id": "sc7FNu4BUCmmhmP14hk2Hij",
|
||||
"blockId": "b87iNgPqGvKrybzHcTD4Sze",
|
||||
"type": "text",
|
||||
"content": {
|
||||
"html": "<div>Custom var: {{Custom var}}</div>",
|
||||
"richText": [
|
||||
{
|
||||
"type": "p",
|
||||
"children": [{ "text": "Custom var: {{Custom var}}" }]
|
||||
}
|
||||
],
|
||||
"plainText": "Custom var: {{Custom var}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"allIds": [
|
||||
"vWwzs6EmChn2PJcJQMr2gT",
|
||||
"s8a2ASVaM3PoaD3y9amHrtT",
|
||||
"set-var-2",
|
||||
"sgFoKA5Y4MXBkqFzCGCWGeg",
|
||||
"number-step",
|
||||
"set-var-1",
|
||||
"sc7FNu4BUCmmhmP14hk2Hij"
|
||||
]
|
||||
},
|
||||
"choiceItems": { "byId": {}, "allIds": [] },
|
||||
"variables": { "byId": {}, "allIds": [] },
|
||||
"theme": {
|
||||
"general": {
|
||||
"font": "Open Sans",
|
||||
"background": { "type": "None", "content": "#ffffff" }
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
|
||||
},
|
||||
"publicId": null
|
||||
}
|
@ -126,5 +126,6 @@
|
||||
"settings": {
|
||||
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
|
||||
},
|
||||
"variables": { "byId": {}, "allIds": [] },
|
||||
"publicId": null
|
||||
}
|
||||
|
@ -129,6 +129,7 @@ const parseTypebotToPublicTypebot = (
|
||||
settings: typebot.settings,
|
||||
publicId: typebot.publicId,
|
||||
choiceItems: typebot.choiceItems,
|
||||
variables: typebot.variables,
|
||||
})
|
||||
|
||||
export const loadRawTypebotInDatabase = (typebot: Typebot) =>
|
||||
|
@ -74,6 +74,7 @@ export const parseTestTypebot = ({
|
||||
publicId: null,
|
||||
publishedTypebotId: null,
|
||||
updatedAt: new Date(),
|
||||
variables: { byId: {}, allIds: [] },
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,6 +45,14 @@ Cypress.on('uncaught:exception', (err) => {
|
||||
}
|
||||
})
|
||||
|
||||
export const getIframeBody = () => {
|
||||
return cy
|
||||
.get('#typebot-iframe')
|
||||
.its('0.contentDocument.body')
|
||||
.should('not.be.empty')
|
||||
.then(cy.wrap)
|
||||
}
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import '@testing-library/cypress/add-commands'
|
||||
import 'cypress-file-upload'
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
parseTestTypebot,
|
||||
preventUserFromRefreshing,
|
||||
} from 'cypress/plugins/utils'
|
||||
import { getIframeBody } from 'cypress/support'
|
||||
import { InputStep, InputStepType } from 'models'
|
||||
|
||||
describe('Text input', () => {
|
||||
@ -214,7 +215,7 @@ describe('Button input', () => {
|
||||
cy.signOut()
|
||||
})
|
||||
|
||||
it.only('Can edit choice items', () => {
|
||||
it('Can edit choice items', () => {
|
||||
cy.signIn('test2@gmail.com')
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.findByDisplayValue('Click to edit').type('Item 1{enter}')
|
||||
@ -275,11 +276,11 @@ const createTypebotWithStep = (step: Omit<InputStep, 'id' | 'blockId'>) => {
|
||||
...step,
|
||||
id: 'step1',
|
||||
blockId: 'block1',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
options:
|
||||
step.type === InputStepType.CHOICE
|
||||
? { itemIds: ['item1'] }
|
||||
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
{ itemIds: ['item1'] }
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
@ -306,11 +307,3 @@ const createTypebotWithStep = (step: Omit<InputStep, 'id' | 'blockId'>) => {
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const getIframeBody = () => {
|
||||
return cy
|
||||
.get('#typebot-iframe')
|
||||
.its('0.contentDocument.body')
|
||||
.should('not.be.empty')
|
||||
.then(cy.wrap)
|
||||
}
|
||||
|
45
apps/builder/cypress/tests/logic.ts
Normal file
45
apps/builder/cypress/tests/logic.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { preventUserFromRefreshing } from 'cypress/plugins/utils'
|
||||
import { getIframeBody } from 'cypress/support'
|
||||
|
||||
describe('Set variables', () => {
|
||||
beforeEach(() => {
|
||||
cy.task('seed')
|
||||
cy.signOut()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cy.window().then((win) => {
|
||||
win.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
})
|
||||
})
|
||||
|
||||
it('options should work', () => {
|
||||
cy.loadTypebotFixtureInDatabase('typebots/logic/setVariable.json')
|
||||
cy.signIn('test2@gmail.com')
|
||||
cy.visit('/typebots/typebot4/edit')
|
||||
cy.findByTestId('step-number-step').click()
|
||||
createNewVar('Num')
|
||||
cy.findByTestId('step-set-var-1').click()
|
||||
createNewVar('Total')
|
||||
cy.findByRole('textbox', { name: 'Value / Expression:' }).type(
|
||||
'1000 * {{Num}}',
|
||||
{ parseSpecialCharSequences: false }
|
||||
)
|
||||
cy.findByTestId('step-set-var-2').click()
|
||||
createNewVar('Custom var')
|
||||
cy.findByRole('textbox', { name: 'Value / Expression:' }).type(
|
||||
'Custom value'
|
||||
)
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.findByPlaceholderText('Type your answer...')
|
||||
.type('365{enter}')
|
||||
getIframeBody().findByText('Total: 365000').should('exist')
|
||||
getIframeBody().findByText('Custom var: Custom value')
|
||||
})
|
||||
})
|
||||
|
||||
const createNewVar = (name: string) => {
|
||||
cy.findByTestId('variables-input').type(name)
|
||||
cy.findByRole('menuitem', { name: `Create "${name}"` }).click()
|
||||
}
|
@ -60,6 +60,11 @@ const components = {
|
||||
focusBorderColor: 'blue.200',
|
||||
},
|
||||
},
|
||||
Textarea: {
|
||||
defaultProps: {
|
||||
focusBorderColor: 'blue.200',
|
||||
},
|
||||
},
|
||||
Popover: {
|
||||
baseStyle: {
|
||||
popper: {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Link } from '@chakra-ui/react'
|
||||
import {
|
||||
AutoformatRule,
|
||||
createAutoformatPlugin,
|
||||
@ -11,7 +12,7 @@ import {
|
||||
createUnderlinePlugin,
|
||||
} from '@udecode/plate-basic-marks'
|
||||
import { createPlugins } from '@udecode/plate-core'
|
||||
import { createLinkPlugin } from '@udecode/plate-link'
|
||||
import { createLinkPlugin, ELEMENT_LINK } from '@udecode/plate-link'
|
||||
|
||||
export const editorStyle: React.CSSProperties = {
|
||||
flex: 1,
|
||||
@ -43,7 +44,8 @@ export const autoFormatRules: AutoformatRule[] = [
|
||||
},
|
||||
]
|
||||
|
||||
export const platePlugins = createPlugins([
|
||||
export const platePlugins = createPlugins(
|
||||
[
|
||||
createBoldPlugin(),
|
||||
createItalicPlugin(),
|
||||
createUnderlinePlugin(),
|
||||
@ -53,4 +55,6 @@ export const platePlugins = createPlugins([
|
||||
rules: autoFormatRules,
|
||||
},
|
||||
}),
|
||||
])
|
||||
],
|
||||
{ components: { [ELEMENT_LINK]: Link } }
|
||||
)
|
||||
|
@ -18,6 +18,7 @@ export const parseTypebotToPublicTypebot = (
|
||||
settings: typebot.settings,
|
||||
publicId: typebot.publicId,
|
||||
choiceItems: typebot.choiceItems,
|
||||
variables: typebot.variables,
|
||||
})
|
||||
|
||||
export const createPublishedTypebot = async (
|
||||
|
@ -11,6 +11,8 @@ import {
|
||||
BubbleStepType,
|
||||
InputStepType,
|
||||
ChoiceInputStep,
|
||||
LogicStepType,
|
||||
LogicStep,
|
||||
} from 'models'
|
||||
import shortId from 'short-uuid'
|
||||
import { Typebot } from 'models'
|
||||
@ -107,9 +109,9 @@ export const parseNewBlock = ({
|
||||
}
|
||||
|
||||
export const parseNewStep = (
|
||||
type: BubbleStepType | InputStepType,
|
||||
type: BubbleStepType | InputStepType | LogicStepType,
|
||||
blockId: string
|
||||
): BubbleStep | InputStep => {
|
||||
): BubbleStep | InputStep | LogicStep => {
|
||||
const id = `s${shortId.generate()}`
|
||||
switch (type) {
|
||||
case BubbleStepType.TEXT: {
|
||||
@ -144,17 +146,10 @@ export const parseNewStep = (
|
||||
}
|
||||
}
|
||||
|
||||
export const checkIfTypebotsAreEqual = (
|
||||
firstChatbot: Typebot,
|
||||
secondChatbot: Typebot
|
||||
) =>
|
||||
export const checkIfTypebotsAreEqual = (typebotA: Typebot, typebotB: Typebot) =>
|
||||
deepEqual(
|
||||
{
|
||||
...firstChatbot,
|
||||
},
|
||||
{
|
||||
...secondChatbot,
|
||||
}
|
||||
JSON.parse(JSON.stringify(typebotA)),
|
||||
JSON.parse(JSON.stringify(typebotB))
|
||||
)
|
||||
|
||||
export const checkIfPublished = (
|
||||
@ -214,6 +209,7 @@ export const parseNewTypebot = ({
|
||||
blocks: { byId: { [startBlockId]: startBlock }, allIds: [startBlockId] },
|
||||
steps: { byId: { [startStepId]: startStep }, allIds: [startStepId] },
|
||||
choiceItems: { byId: {}, allIds: [] },
|
||||
variables: { byId: {}, allIds: [] },
|
||||
theme,
|
||||
settings,
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Parser } from 'htmlparser2'
|
||||
import { Step } from 'models'
|
||||
|
||||
export const fetcher = async (input: RequestInfo, init?: RequestInit) => {
|
||||
const res = await fetch(input, init)
|
||||
@ -115,3 +116,5 @@ export const removeUndefinedFields = <T>(obj: T): T =>
|
||||
: { ...acc, [key]: obj[key as keyof T] },
|
||||
{} as T
|
||||
)
|
||||
|
||||
export const stepHasOptions = (step: Step) => 'options' in step
|
||||
|
@ -4,9 +4,19 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||
import { ChatStep } from './ChatStep'
|
||||
import { AvatarSideContainer } from './AvatarSideContainer'
|
||||
import { HostAvatarsContext } from '../../contexts/HostAvatarsContext'
|
||||
import { ChoiceInputStep, Step } from 'models'
|
||||
import { ChoiceInputStep, LogicStep, Step } from 'models'
|
||||
import { useTypebot } from '../../contexts/TypebotContext'
|
||||
import { isChoiceInput } from 'utils'
|
||||
import {
|
||||
isChoiceInput,
|
||||
isInputStep,
|
||||
isLogicStep,
|
||||
isTextBubbleStep,
|
||||
} from 'utils'
|
||||
import {
|
||||
evaluateExpression,
|
||||
isMathFormula,
|
||||
parseVariables,
|
||||
} from 'services/variable'
|
||||
|
||||
type ChatBlockProps = {
|
||||
stepIds: string[]
|
||||
@ -14,15 +24,20 @@ type ChatBlockProps = {
|
||||
}
|
||||
|
||||
export const ChatBlock = ({ stepIds, onBlockEnd }: ChatBlockProps) => {
|
||||
const { typebot } = useTypebot()
|
||||
const { typebot, updateVariableValue } = useTypebot()
|
||||
const [displayedSteps, setDisplayedSteps] = useState<Step[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayedSteps([typebot.steps.byId[stepIds[0]]])
|
||||
displayNextStep()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
autoScrollToBottom()
|
||||
const currentStep = [...displayedSteps].pop()
|
||||
if (currentStep && isLogicStep(currentStep)) {
|
||||
executeLogic(currentStep)
|
||||
displayNextStep()
|
||||
}
|
||||
}, [displayedSteps])
|
||||
|
||||
const autoScrollToBottom = () => {
|
||||
@ -34,7 +49,14 @@ export const ChatBlock = ({ stepIds, onBlockEnd }: ChatBlockProps) => {
|
||||
|
||||
const displayNextStep = (answerContent?: string) => {
|
||||
const currentStep = [...displayedSteps].pop()
|
||||
if (!currentStep) throw new Error('currentStep should exist')
|
||||
if (currentStep) {
|
||||
if (
|
||||
isInputStep(currentStep) &&
|
||||
currentStep.options?.variableId &&
|
||||
answerContent
|
||||
) {
|
||||
updateVariableValue(currentStep.options.variableId, answerContent)
|
||||
}
|
||||
const isSingleChoiceStep =
|
||||
isChoiceInput(currentStep) && !currentStep.options.isMultipleChoice
|
||||
if (isSingleChoiceStep)
|
||||
@ -44,10 +66,20 @@ export const ChatBlock = ({ stepIds, onBlockEnd }: ChatBlockProps) => {
|
||||
displayedSteps.length === stepIds.length
|
||||
)
|
||||
return onBlockEnd(currentStep?.target?.blockId)
|
||||
}
|
||||
const nextStep = typebot.steps.byId[stepIds[displayedSteps.length]]
|
||||
if (nextStep) setDisplayedSteps([...displayedSteps, nextStep])
|
||||
}
|
||||
|
||||
const executeLogic = (step: LogicStep) => {
|
||||
if (!step.options?.variableId || !step.options.expressionToEvaluate) return
|
||||
const expression = step.options.expressionToEvaluate
|
||||
const evaluatedExpression = isMathFormula(expression)
|
||||
? evaluateExpression(parseVariables(expression, typebot.variables))
|
||||
: expression
|
||||
updateVariableValue(step.options.variableId, evaluatedExpression)
|
||||
}
|
||||
|
||||
const getSingleChoiceTargetId = (
|
||||
currentStep: ChoiceInputStep,
|
||||
answerContent?: string
|
||||
@ -68,7 +100,9 @@ export const ChatBlock = ({ stepIds, onBlockEnd }: ChatBlockProps) => {
|
||||
<AvatarSideContainer />
|
||||
<div className="flex flex-col w-full">
|
||||
<TransitionGroup>
|
||||
{displayedSteps.map((step) => (
|
||||
{displayedSteps
|
||||
.filter((step) => isInputStep(step) || isTextBubbleStep(step))
|
||||
.map((step) => (
|
||||
<CSSTransition
|
||||
key={step.id}
|
||||
classNames="bubble"
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useHostAvatars } from '../../../../contexts/HostAvatarsContext'
|
||||
import { useTypebot } from '../../../../contexts/TypebotContext'
|
||||
import { BubbleStepType, StepType, TextStep } from 'models'
|
||||
import { computeTypingTimeout } from '../../../../services/chat'
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useHostAvatars } from 'contexts/HostAvatarsContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { BubbleStepType, TextStep } from 'models'
|
||||
import { computeTypingTimeout } from 'services/chat'
|
||||
import { TypingContent } from './TypingContent'
|
||||
import { parseVariables } from 'services/variable'
|
||||
|
||||
type HostMessageBubbleProps = {
|
||||
step: TextStep
|
||||
@ -24,6 +25,11 @@ export const HostMessageBubble = ({
|
||||
const messageContainer = useRef<HTMLDivElement | null>(null)
|
||||
const [isTyping, setIsTyping] = useState(true)
|
||||
|
||||
const content = useMemo(
|
||||
() => parseVariables(step.content.html, typebot.variables),
|
||||
[typebot.variables]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
sendAvatarOffset()
|
||||
const typingTimeout = computeTypingTimeout(
|
||||
@ -72,7 +78,7 @@ export const HostMessageBubble = ({
|
||||
(isTyping ? 'opacity-0 h-6' : 'opacity-100 h-full')
|
||||
}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: step.content.html,
|
||||
__html: content,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { PublicTypebot } from 'models'
|
||||
import React, { createContext, ReactNode, useContext } from 'react'
|
||||
import React, { createContext, ReactNode, useContext, useState } from 'react'
|
||||
|
||||
const typebotContext = createContext<{
|
||||
typebot: PublicTypebot
|
||||
updateVariableValue: (variableId: string, value: string) => void
|
||||
//@ts-ignore
|
||||
}>({})
|
||||
|
||||
@ -13,10 +14,25 @@ export const TypebotContext = ({
|
||||
children: ReactNode
|
||||
typebot: PublicTypebot
|
||||
}) => {
|
||||
const [localTypebot, setLocalTypebot] = useState<PublicTypebot>(typebot)
|
||||
|
||||
const updateVariableValue = (variableId: string, value: string) => {
|
||||
setLocalTypebot((typebot) => ({
|
||||
...typebot,
|
||||
variables: {
|
||||
...typebot.variables,
|
||||
byId: {
|
||||
...typebot.variables.byId,
|
||||
[variableId]: { ...typebot.variables.byId[variableId], value },
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
return (
|
||||
<typebotContext.Provider
|
||||
value={{
|
||||
typebot,
|
||||
typebot: localTypebot,
|
||||
updateVariableValue,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
46
packages/bot-engine/src/services/variable.ts
Normal file
46
packages/bot-engine/src/services/variable.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Table, Variable } from 'models'
|
||||
import { isDefined } from 'utils'
|
||||
|
||||
const safeEval = eval
|
||||
|
||||
export const stringContainsVariable = (str: string): boolean =>
|
||||
/\{\{(.*?)\}\}/g.test(str)
|
||||
|
||||
export const parseVariables = (
|
||||
text: string,
|
||||
variables: Table<Variable>
|
||||
): string => {
|
||||
if (text === '') return text
|
||||
return text.replace(/\{\{(.*?)\}\}/g, (_, fullVariableString) => {
|
||||
const matchedVarName = fullVariableString.replace(/{{|}}/g, '')
|
||||
const matchedVariableId = variables.allIds.find((variableId) => {
|
||||
const variable = variables.byId[variableId]
|
||||
return matchedVarName === variable.name && isDefined(variable.value)
|
||||
})
|
||||
return variables.byId[matchedVariableId ?? '']?.value ?? ''
|
||||
})
|
||||
}
|
||||
|
||||
export const isMathFormula = (str?: string) =>
|
||||
['*', '/', '+', '-'].some((val) => str && str.includes(val))
|
||||
|
||||
export const evaluateExpression = (str: string) => {
|
||||
let result = replaceCommasWithDots(str)
|
||||
try {
|
||||
const evaluatedNumber = safeEval(result) as number
|
||||
if (countDecimals(evaluatedNumber) > 2) {
|
||||
return evaluatedNumber.toFixed(2)
|
||||
}
|
||||
return evaluatedNumber.toString()
|
||||
} catch (err) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
const replaceCommasWithDots = (str: string) =>
|
||||
str.replace(new RegExp(/(\d+)(,)(\d+)/, 'g'), '$1.$3')
|
||||
|
||||
const countDecimals = (value: number) => {
|
||||
if (value % 1 != 0) return value.toString().split('.')[1].length
|
||||
return 0
|
||||
}
|
@ -13,6 +13,7 @@
|
||||
"outDir": "dist",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDeclarationOnly": true
|
||||
"emitDeclarationOnly": true,
|
||||
"baseUrl": "./src"
|
||||
}
|
||||
}
|
||||
|
@ -1,87 +0,0 @@
|
||||
datasource db {
|
||||
url = env("DATABASE_URL")
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id
|
||||
createdAt DateTime @default(now())
|
||||
email String @unique
|
||||
name String?
|
||||
avatarUrl String?
|
||||
redeemedCoupon Boolean?
|
||||
oAuthCredentials Json?
|
||||
referralId String?
|
||||
domains String[]
|
||||
onboarding_data Json?
|
||||
settings Json
|
||||
typebots Typebot[] @relation("Owner")
|
||||
sharedTypebots Typebot[] @relation("Collaborators")
|
||||
dashboardFolders DashboardFolder[]
|
||||
}
|
||||
|
||||
model DashboardFolder {
|
||||
id BigInt @id @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
name String
|
||||
owner User @relation(fields: [ownerId], references: [id])
|
||||
ownerId String
|
||||
parentFolderId BigInt
|
||||
parentFolder DashboardFolder @relation("ParentChild", fields: [parentFolderId], references: [id])
|
||||
childrenFolder DashboardFolder[] @relation("ParentChild")
|
||||
}
|
||||
|
||||
model Typebot {
|
||||
id BigInt @id @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now())
|
||||
steps Json[]
|
||||
publishedTypebotId BigInt @unique
|
||||
publishedTypebot PublicTypebot @relation(fields: [publishedTypebotId], references: [id])
|
||||
connectors Json[]
|
||||
name String
|
||||
ownerId String
|
||||
owner User @relation("Owner", fields: [ownerId], references: [id])
|
||||
conditions Json
|
||||
startConditions Json
|
||||
theme Json
|
||||
settings Json
|
||||
collaborators User[] @relation("Collaborators")
|
||||
customDomains String[]
|
||||
shareSettings Json
|
||||
variables Json
|
||||
checkedConversionRules String[]
|
||||
results Result[]
|
||||
httpRequests Json[]
|
||||
credentials Json[]
|
||||
}
|
||||
|
||||
model PublicTypebot {
|
||||
id BigInt @id @default(autoincrement())
|
||||
typebot Typebot?
|
||||
steps Json[]
|
||||
name String
|
||||
conditions Json
|
||||
startConditions Json
|
||||
theme Json
|
||||
settings Json
|
||||
connectors Json
|
||||
customDomains String[]
|
||||
shareSettings Json
|
||||
variables Json
|
||||
}
|
||||
|
||||
model Result {
|
||||
id BigInt @id @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now())
|
||||
typebotId BigInt
|
||||
typebot Typebot @relation(fields: [typebotId], references: [id])
|
||||
variables Json[]
|
||||
isCompleted Boolean
|
||||
answers Json[]
|
||||
}
|
@ -93,6 +93,7 @@ model Typebot {
|
||||
blocks Json
|
||||
steps Json
|
||||
choiceItems Json
|
||||
variables Json
|
||||
theme Json
|
||||
settings Json
|
||||
publicId String? @unique
|
||||
@ -106,6 +107,7 @@ model PublicTypebot {
|
||||
blocks Json
|
||||
steps Json
|
||||
choiceItems Json
|
||||
variables Json
|
||||
theme Json
|
||||
settings Json
|
||||
publicId String? @unique
|
||||
|
@ -1,14 +1,22 @@
|
||||
import { PublicTypebot as PublicTypebotFromPrisma } from 'db'
|
||||
import { Block, ChoiceItem, Settings, Step, Theme } from './typebot'
|
||||
import { Variable } from './typebot/variable'
|
||||
import { Table } from './utils'
|
||||
|
||||
export type PublicTypebot = Omit<
|
||||
PublicTypebotFromPrisma,
|
||||
'blocks' | 'startBlock' | 'theme' | 'settings' | 'steps'
|
||||
| 'blocks'
|
||||
| 'startBlock'
|
||||
| 'theme'
|
||||
| 'settings'
|
||||
| 'steps'
|
||||
| 'choiceItems'
|
||||
| 'variables'
|
||||
> & {
|
||||
blocks: Table<Block>
|
||||
steps: Table<Step>
|
||||
choiceItems: Table<ChoiceItem>
|
||||
variables: Table<Variable>
|
||||
theme: Theme
|
||||
settings: Settings
|
||||
}
|
||||
|
@ -2,3 +2,4 @@ export * from './typebot'
|
||||
export * from './steps'
|
||||
export * from './theme'
|
||||
export * from './settings'
|
||||
export * from './variable'
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './steps'
|
||||
export * from './inputs'
|
||||
export * from './logic'
|
||||
|
@ -47,7 +47,7 @@ export type DateInputStep = StepBase & {
|
||||
|
||||
export type PhoneNumberInputStep = StepBase & {
|
||||
type: InputStepType.PHONE
|
||||
options?: InputTextOptionsBase
|
||||
options?: OptionBase & InputTextOptionsBase
|
||||
}
|
||||
|
||||
export type ChoiceInputStep = StepBase & {
|
||||
@ -55,12 +55,6 @@ export type ChoiceInputStep = StepBase & {
|
||||
options: ChoiceInputOptions
|
||||
}
|
||||
|
||||
export type ChoiceInputOptions = {
|
||||
itemIds: string[]
|
||||
isMultipleChoice?: boolean
|
||||
buttonLabel?: string
|
||||
}
|
||||
|
||||
export type ChoiceItem = {
|
||||
id: string
|
||||
stepId: string
|
||||
@ -68,25 +62,34 @@ export type ChoiceItem = {
|
||||
target?: Target
|
||||
}
|
||||
|
||||
export type DateInputOptions = {
|
||||
type OptionBase = { variableId?: string }
|
||||
type InputTextOptionsBase = {
|
||||
labels?: { placeholder?: string; button?: string }
|
||||
}
|
||||
|
||||
export type ChoiceInputOptions = OptionBase & {
|
||||
itemIds: string[]
|
||||
isMultipleChoice?: boolean
|
||||
buttonLabel?: string
|
||||
}
|
||||
|
||||
export type DateInputOptions = OptionBase & {
|
||||
labels?: { button?: string; from?: string; to?: string }
|
||||
hasTime?: boolean
|
||||
isRange?: boolean
|
||||
}
|
||||
|
||||
export type EmailInputOptions = InputTextOptionsBase
|
||||
export type EmailInputOptions = OptionBase & InputTextOptionsBase
|
||||
|
||||
export type UrlInputOptions = InputTextOptionsBase
|
||||
export type UrlInputOptions = OptionBase & InputTextOptionsBase
|
||||
|
||||
type InputTextOptionsBase = {
|
||||
labels?: { placeholder?: string; button?: string }
|
||||
}
|
||||
|
||||
export type TextInputOptions = InputTextOptionsBase & {
|
||||
export type TextInputOptions = OptionBase &
|
||||
InputTextOptionsBase & {
|
||||
isLong?: boolean
|
||||
}
|
||||
|
||||
export type NumberInputOptions = InputTextOptionsBase & {
|
||||
export type NumberInputOptions = OptionBase &
|
||||
InputTextOptionsBase & {
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
|
17
packages/models/src/typebot/steps/logic.ts
Normal file
17
packages/models/src/typebot/steps/logic.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { StepBase } from '.'
|
||||
|
||||
export type LogicStep = SetVariableStep
|
||||
|
||||
export enum LogicStepType {
|
||||
SET_VARIABLE = 'Set variable',
|
||||
}
|
||||
|
||||
export type SetVariableStep = StepBase & {
|
||||
type: LogicStepType.SET_VARIABLE
|
||||
options?: SetVariableOptions
|
||||
}
|
||||
|
||||
export type SetVariableOptions = {
|
||||
variableId?: string
|
||||
expressionToEvaluate?: string
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
import { InputStep, InputStepType } from './inputs'
|
||||
import { LogicStep, LogicStepType } from './logic'
|
||||
|
||||
export type Step = StartStep | BubbleStep | InputStep
|
||||
export type Step = StartStep | BubbleStep | InputStep | LogicStep
|
||||
|
||||
export type BubbleStep = TextStep
|
||||
|
||||
export type StepType = 'start' | BubbleStepType | InputStepType
|
||||
export type StepType = 'start' | BubbleStepType | InputStepType | LogicStepType
|
||||
|
||||
export enum BubbleStepType {
|
||||
TEXT = 'text',
|
||||
|
@ -4,14 +4,16 @@ import { Table } from '../utils'
|
||||
import { Settings } from './settings'
|
||||
import { Step } from './steps/steps'
|
||||
import { Theme } from './theme'
|
||||
import { Variable } from './variable'
|
||||
|
||||
export type Typebot = Omit<
|
||||
TypebotFromPrisma,
|
||||
'blocks' | 'theme' | 'settings' | 'steps'
|
||||
'blocks' | 'theme' | 'settings' | 'steps' | 'choiceItems' | 'variables'
|
||||
> & {
|
||||
blocks: Table<Block>
|
||||
steps: Table<Step>
|
||||
choiceItems: Table<ChoiceItem>
|
||||
variables: Table<Variable>
|
||||
theme: Theme
|
||||
settings: Settings
|
||||
}
|
||||
|
5
packages/models/src/typebot/variable.ts
Normal file
5
packages/models/src/typebot/variable.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export type Variable = {
|
||||
id: string
|
||||
name: string
|
||||
value?: string
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
import {
|
||||
BubbleStep,
|
||||
BubbleStepType,
|
||||
ChoiceInputStep,
|
||||
InputStep,
|
||||
InputStepType,
|
||||
LogicStep,
|
||||
LogicStepType,
|
||||
Step,
|
||||
Table,
|
||||
TextInputStep,
|
||||
@ -45,6 +48,12 @@ export const filterTable = <T>(ids: string[], table: Table<T>): Table<T> => ({
|
||||
export const isInputStep = (step: Step): step is InputStep =>
|
||||
(Object.values(InputStepType) as string[]).includes(step.type)
|
||||
|
||||
export const isBubbleStep = (step: Step): step is BubbleStep =>
|
||||
(Object.values(BubbleStepType) as string[]).includes(step.type)
|
||||
|
||||
export const isLogicStep = (step: Step): step is LogicStep =>
|
||||
(Object.values(LogicStepType) as string[]).includes(step.type)
|
||||
|
||||
export const isTextBubbleStep = (step: Step): step is TextStep =>
|
||||
step.type === BubbleStepType.TEXT
|
||||
|
||||
|
Reference in New Issue
Block a user