diff --git a/apps/builder/assets/icons.tsx b/apps/builder/assets/icons.tsx
index 7f8be9d63..3039a1a83 100644
--- a/apps/builder/assets/icons.tsx
+++ b/apps/builder/assets/icons.tsx
@@ -208,3 +208,12 @@ export const DownloadIcon = (props: IconProps) => (
)
+
+export const NumberIcon = (props: IconProps) => (
+
+
+
+
+
+
+)
diff --git a/apps/builder/components/board/StepTypesList/StepCard.tsx b/apps/builder/components/board/StepTypesList/StepCard.tsx
index 8d32cc8c2..9d27b5db2 100644
--- a/apps/builder/components/board/StepTypesList/StepCard.tsx
+++ b/apps/builder/components/board/StepTypesList/StepCard.tsx
@@ -1,16 +1,19 @@
import { Button, ButtonProps, Flex, HStack } from '@chakra-ui/react'
-import { StepType } from 'models'
+import { BubbleStepType, InputStepType, StepType } from 'models'
import { useDnd } from 'contexts/DndContext'
import React, { useEffect, useState } from 'react'
import { StepIcon } from './StepIcon'
-import { StepLabel } from './StepLabel'
+import { StepTypeLabel } from './StepTypeLabel'
export const StepCard = ({
type,
onMouseDown,
}: {
- type: StepType
- onMouseDown: (e: React.MouseEvent, type: StepType) => void
+ type: BubbleStepType | InputStepType
+ onMouseDown: (
+ e: React.MouseEvent,
+ type: BubbleStepType | InputStepType
+ ) => void
}) => {
const { draggedStepType } = useDnd()
const [isMouseDown, setIsMouseDown] = useState(false)
@@ -35,7 +38,7 @@ export const StepCard = ({
{!isMouseDown && (
<>
-
+
>
)}
@@ -62,7 +65,7 @@ export const StepCardOverlay = ({
{...props}
>
-
+
)
}
diff --git a/apps/builder/components/board/StepTypesList/StepIcon.tsx b/apps/builder/components/board/StepTypesList/StepIcon.tsx
index 8ae8f3db0..80792505d 100644
--- a/apps/builder/components/board/StepTypesList/StepIcon.tsx
+++ b/apps/builder/components/board/StepTypesList/StepIcon.tsx
@@ -1,22 +1,25 @@
-import { ChatIcon, FlagIcon, TextIcon } from 'assets/icons'
-import { StepType } from 'models'
+import { ChatIcon, FlagIcon, NumberIcon, TextIcon } from 'assets/icons'
+import { BubbleStepType, InputStepType, StepType } from 'models'
import React from 'react'
type StepIconProps = { type: StepType }
export const StepIcon = ({ type }: StepIconProps) => {
switch (type) {
- case StepType.TEXT: {
+ case BubbleStepType.TEXT: {
return
}
- case StepType.TEXT: {
+ case InputStepType.TEXT: {
return
}
- case StepType.START: {
+ case InputStepType.NUMBER: {
+ return
+ }
+ case 'start': {
return
}
default: {
- return
+ return <>>
}
}
}
diff --git a/apps/builder/components/board/StepTypesList/StepLabel.tsx b/apps/builder/components/board/StepTypesList/StepLabel.tsx
deleted file mode 100644
index 4be13c678..000000000
--- a/apps/builder/components/board/StepTypesList/StepLabel.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { Text } from '@chakra-ui/react'
-import { StepType } from 'models'
-import React from 'react'
-
-type Props = { type: StepType }
-
-export const StepLabel = ({ type }: Props) => {
- switch (type) {
- case StepType.TEXT: {
- return Text
- }
- case StepType.TEXT_INPUT: {
- return Text
- }
- default: {
- return <>>
- }
- }
-}
diff --git a/apps/builder/components/board/StepTypesList/StepTypeLabel.tsx b/apps/builder/components/board/StepTypesList/StepTypeLabel.tsx
new file mode 100644
index 000000000..231fa7d74
--- /dev/null
+++ b/apps/builder/components/board/StepTypesList/StepTypeLabel.tsx
@@ -0,0 +1,20 @@
+import { Text } from '@chakra-ui/react'
+import { BubbleStepType, InputStepType, StepType } from 'models'
+import React from 'react'
+
+type Props = { type: StepType }
+
+export const StepTypeLabel = ({ type }: Props) => {
+ switch (type) {
+ case BubbleStepType.TEXT:
+ case InputStepType.TEXT: {
+ return Text
+ }
+ case InputStepType.NUMBER: {
+ return Number
+ }
+ default: {
+ return <>>
+ }
+ }
+}
diff --git a/apps/builder/components/board/StepTypesList/StepTypesList.tsx b/apps/builder/components/board/StepTypesList/StepTypesList.tsx
index cdf654483..5b79e734d 100644
--- a/apps/builder/components/board/StepTypesList/StepTypesList.tsx
+++ b/apps/builder/components/board/StepTypesList/StepTypesList.tsx
@@ -5,19 +5,11 @@ import {
SimpleGrid,
useEventListener,
} from '@chakra-ui/react'
-import { StepType } from 'models'
+import { BubbleStepType, InputStepType } from 'models'
import { useDnd } from 'contexts/DndContext'
import React, { useState } from 'react'
import { StepCard, StepCardOverlay } from './StepCard'
-export const stepListItems: {
- bubbles: { type: StepType }[]
- inputs: { type: StepType }[]
-} = {
- bubbles: [{ type: StepType.TEXT }],
- inputs: [{ type: StepType.TEXT_INPUT }],
-}
-
export const StepTypesList = () => {
const { setDraggedStepType, draggedStepType } = useDnd()
const [position, setPosition] = useState({
@@ -37,7 +29,10 @@ export const StepTypesList = () => {
}
useEventListener('mousemove', handleMouseMove)
- const handleMouseDown = (e: React.MouseEvent, type: StepType) => {
+ const handleMouseDown = (
+ e: React.MouseEvent,
+ type: BubbleStepType | InputStepType
+ ) => {
const element = e.currentTarget as HTMLDivElement
const rect = element.getBoundingClientRect()
const relativeX = e.clientX - rect.left
@@ -77,8 +72,8 @@ export const StepTypesList = () => {
Bubbles
- {stepListItems.bubbles.map((props) => (
-
+ {Object.values(BubbleStepType).map((type) => (
+
))}
@@ -86,8 +81,8 @@ export const StepTypesList = () => {
Inputs
- {stepListItems.inputs.map((props) => (
-
+ {Object.values(InputStepType).map((type) => (
+
))}
{draggedStepType && (
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/NumberInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/NumberInputSettingsBody.tsx
new file mode 100644
index 000000000..7e022f3a1
--- /dev/null
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/NumberInputSettingsBody.tsx
@@ -0,0 +1,84 @@
+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 React from 'react'
+import { removeUndefinedFields } from 'services/utils'
+
+type NumberInputSettingsBodyProps = {
+ options?: NumberInputOptions
+ onOptionsChange: (options: NumberInputOptions) => void
+}
+
+export const NumberInputSettingsBody = ({
+ options,
+ onOptionsChange,
+}: NumberInputSettingsBodyProps) => {
+ const handlePlaceholderChange = (placeholder: string) =>
+ onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } })
+ const handleButtonLabelChange = (button: string) =>
+ onOptionsChange({ ...options, labels: { ...options?.labels, button } })
+ const handleMinChange = (min?: number) =>
+ onOptionsChange(removeUndefinedFields({ ...options, min }))
+ const handleMaxChange = (max?: number) =>
+ onOptionsChange(removeUndefinedFields({ ...options, max }))
+ const handleStepChange = (step?: number) =>
+ onOptionsChange(removeUndefinedFields({ ...options, step }))
+
+ return (
+
+
+
+ Placeholder:
+
+
+
+
+
+ Button label:
+
+
+
+
+
+ Min:
+
+
+
+
+
+ Max:
+
+
+
+
+
+ Step:
+
+
+
+
+ )
+}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/SettingsPopoverContent.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/SettingsPopoverContent.tsx
index 4aeb27027..2c2915257 100644
--- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/SettingsPopoverContent.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/SettingsPopoverContent.tsx
@@ -1,6 +1,7 @@
import { PopoverContent, PopoverArrow, PopoverBody } from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
-import { Step, StepType, TextInputOptions } from 'models'
+import { InputStepType, Step, TextInputOptions } from 'models'
+import { NumberInputSettingsBody } from './NumberInputSettingsBody'
import { TextInputSettingsBody } from './TextInputSettingsBody'
type Props = {
@@ -25,7 +26,7 @@ const SettingsPopoverBodyContent = ({ step }: Props) => {
updateStep(step.id, { options } as Partial)
switch (step.type) {
- case StepType.TEXT_INPUT: {
+ case InputStepType.TEXT: {
return (
{
/>
)
}
+ case InputStepType.NUMBER: {
+ return (
+
+ )
+ }
default: {
return <>>
}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepContent.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepContent.tsx
deleted file mode 100644
index e09e90f9f..000000000
--- a/apps/builder/components/board/graph/BlockNode/StepNode/StepContent.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Flex, Text } from '@chakra-ui/react'
-import { Step, StartStep, StepType } from 'models'
-
-export const StepContent = (props: Step | StartStep) => {
- switch (props.type) {
- case StepType.TEXT: {
- return (
- Click to edit...
`
- : props.content.html,
- }}
- >
- )
- }
- case StepType.TEXT_INPUT: {
- return Type your answer...
- }
- case StepType.START: {
- return {props.label}
- }
- default: {
- return No input
- }
- }
-}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx
index c6b2fff7e..5ed6f64b4 100644
--- a/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx
@@ -11,15 +11,15 @@ import { Block, Step } from 'models'
import { SourceEndpoint } from './SourceEndpoint'
import { useGraph } from 'contexts/GraphContext'
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
-import { isDefined } from 'utils'
+import { isDefined, isTextBubbleStep } from 'utils'
import { Coordinates } from '@dnd-kit/core/dist/types'
import { TextEditor } from './TextEditor/TextEditor'
-import { StepContent } from './StepContent'
+import { StepNodeLabel } from './StepNodeLabel'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { ContextMenu } from 'components/shared/ContextMenu'
import { StepNodeContextMenu } from './RightClickMenu'
import { SettingsPopoverContent } from './SettingsPopoverContent'
-import { isStepText } from 'services/typebots'
+import { DraggableStep } from 'contexts/DndContext'
export const StepNode = ({
step,
@@ -34,7 +34,7 @@ export const StepNode = ({
onMouseMoveTopOfElement?: () => void
onMouseDown?: (
stepNodePosition: { absolute: Coordinates; relative: Coordinates },
- step: Step
+ step: DraggableStep
) => void
}) => {
const { setConnectingIds, connectingIds } = useGraph()
@@ -43,7 +43,7 @@ export const StepNode = ({
const [mouseDownEvent, setMouseDownEvent] =
useState<{ absolute: Coordinates; relative: Coordinates }>()
const [isEditing, setIsEditing] = useState(
- isStepText(step) && step.content.plainText === ''
+ isTextBubbleStep(step) && step.content.plainText === ''
)
useEffect(() => {
@@ -102,8 +102,8 @@ export const StepNode = ({
mouseDownEvent &&
onMouseDown &&
(event.movementX > 0 || event.movementY > 0)
- if (isMovingAndIsMouseDown) {
- onMouseDown(mouseDownEvent, step as Step)
+ if (isMovingAndIsMouseDown && step.type !== 'start') {
+ onMouseDown(mouseDownEvent, step)
deleteStep(step.id)
setMouseDownEvent(undefined)
}
@@ -142,7 +142,7 @@ export const StepNode = ({
connectingIds?.target?.blockId,
])
- return isEditing && isStepText(step) ? (
+ return isEditing && isTextBubbleStep(step) ? (
-
+
{isConnectable && (
{
+ switch (props.type) {
+ case BubbleStepType.TEXT: {
+ return (
+ Click to edit...`
+ : props.content.html,
+ }}
+ />
+ )
+ }
+ case InputStepType.TEXT: {
+ return (
+
+ {props.options?.labels?.placeholder ?? 'Type your answer...'}
+
+ )
+ }
+ case InputStepType.NUMBER: {
+ return (
+
+ {props.options?.labels?.placeholder ?? 'Type your answer...'}
+
+ )
+ }
+ case 'start': {
+ return {props.label}
+ }
+ default: {
+ return No input
+ }
+ }
+}
diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeOverlay.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeOverlay.tsx
index 31a31cd9f..b17e2d697 100644
--- a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeOverlay.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeOverlay.tsx
@@ -1,7 +1,7 @@
import { StackProps, HStack } from '@chakra-ui/react'
import { StartStep, Step } from 'models'
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
-import { StepContent } from './StepContent'
+import { StepNodeLabel } from './StepNodeLabel'
export const StepNodeOverlay = ({
step,
@@ -19,7 +19,7 @@ export const StepNodeOverlay = ({
{...props}
>
-
+
)
}
diff --git a/apps/builder/components/board/graph/BlockNode/StepsList.tsx b/apps/builder/components/board/graph/BlockNode/StepsList.tsx
index f791a78ce..1530a523a 100644
--- a/apps/builder/components/board/graph/BlockNode/StepsList.tsx
+++ b/apps/builder/components/board/graph/BlockNode/StepsList.tsx
@@ -1,6 +1,6 @@
import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react'
import { Step, Table } from 'models'
-import { useDnd } from 'contexts/DndContext'
+import { DraggableStep, useDnd } from 'contexts/DndContext'
import { Coordinates } from 'contexts/GraphContext'
import { useState } from 'react'
import { StepNode, StepNodeOverlay } from './StepNode'
@@ -54,7 +54,7 @@ export const StepsList = ({
const handleStepMouseDown = (
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
- step: Step
+ step: DraggableStep
) => {
setPosition(absolute)
setRelativeCoordinates(relative)
diff --git a/apps/builder/components/board/graph/Graph.tsx b/apps/builder/components/board/graph/Graph.tsx
index bf79097e4..60f56a138 100644
--- a/apps/builder/components/board/graph/Graph.tsx
+++ b/apps/builder/components/board/graph/Graph.tsx
@@ -2,11 +2,10 @@ import { Flex, FlexProps, useEventListener } from '@chakra-ui/react'
import React, { useRef, useMemo } from 'react'
import { blockWidth, useGraph } from 'contexts/GraphContext'
import { BlockNode } from './BlockNode/BlockNode'
-import { useDnd } from 'contexts/DndContext'
+import { DraggableStepType, useDnd } from 'contexts/DndContext'
import { Edges } from './Edges'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
-import { StepType } from 'models'
const Graph = ({ ...props }: FlexProps) => {
const { draggedStepType, setDraggedStepType, draggedStep, setDraggedStep } =
@@ -44,7 +43,7 @@ const Graph = ({ ...props }: FlexProps) => {
createBlock({
x: e.clientX - graphPosition.x - blockWidth / 3,
y: e.clientY - graphPosition.y - 20 - headerHeight,
- step: draggedStep ?? (draggedStepType as StepType),
+ step: draggedStep ?? (draggedStepType as DraggableStepType),
})
setDraggedStep(undefined)
setDraggedStepType(undefined)
diff --git a/apps/builder/components/settings/SmartNumberInput.tsx b/apps/builder/components/settings/SmartNumberInput.tsx
index 073ac4739..367c6f4ba 100644
--- a/apps/builder/components/settings/SmartNumberInput.tsx
+++ b/apps/builder/components/settings/SmartNumberInput.tsx
@@ -13,14 +13,14 @@ export const SmartNumberInput = ({
onValueChange,
...props
}: {
- initialValue: number
- onValueChange: (value: number) => void
+ initialValue?: number
+ onValueChange: (value?: number) => void
} & NumberInputProps) => {
- const [value, setValue] = useState(initialValue.toString())
+ const [value, setValue] = useState(initialValue?.toString() ?? '')
useEffect(() => {
if (value.endsWith('.') || value.endsWith(',')) return
- if (value === '') onValueChange(0)
+ if (value === '') onValueChange(undefined)
const newValue = parseFloat(value)
if (isNaN(newValue)) return
onValueChange(newValue)
diff --git a/apps/builder/components/settings/TypingEmulation.tsx b/apps/builder/components/settings/TypingEmulation.tsx
index 41a6c35f0..a949ab84d 100644
--- a/apps/builder/components/settings/TypingEmulation.tsx
+++ b/apps/builder/components/settings/TypingEmulation.tsx
@@ -17,14 +17,14 @@ export const TypingEmulation = ({
onUpdate({ ...typingEmulation, enabled: !typingEmulation.enabled })
}
- const handleSpeedChange = (speed: number) => {
+ const handleSpeedChange = (speed?: number) => {
if (!typingEmulation) return
- onUpdate({ ...typingEmulation, speed })
+ onUpdate({ ...typingEmulation, speed: speed ?? 0 })
}
- const handleMaxDelayChange = (maxDelay: number) => {
+ const handleMaxDelayChange = (maxDelay?: number) => {
if (!typingEmulation) return
- onUpdate({ ...typingEmulation, maxDelay: maxDelay })
+ onUpdate({ ...typingEmulation, maxDelay: maxDelay ?? 0 })
}
return (
diff --git a/apps/builder/contexts/DndContext.tsx b/apps/builder/contexts/DndContext.tsx
index 7a324b689..32c6a2275 100644
--- a/apps/builder/contexts/DndContext.tsx
+++ b/apps/builder/contexts/DndContext.tsx
@@ -1,4 +1,4 @@
-import { Step, StepType } from 'models'
+import { BubbleStep, BubbleStepType, InputStep, InputStepType } from 'models'
import {
createContext,
Dispatch,
@@ -8,19 +8,24 @@ import {
useState,
} from 'react'
+export type DraggableStep = BubbleStep | InputStep
+export type DraggableStepType = BubbleStepType | InputStepType
+
const dndContext = createContext<{
- draggedStepType?: StepType
- setDraggedStepType: Dispatch>
- draggedStep?: Step
- setDraggedStep: Dispatch>
+ draggedStepType?: DraggableStepType
+ setDraggedStepType: Dispatch>
+ draggedStep?: DraggableStep
+ setDraggedStep: Dispatch>
}>({
setDraggedStep: () => console.log("I'm not implemented"),
setDraggedStepType: () => console.log("I'm not implemented"),
})
export const DndContext = ({ children }: { children: ReactNode }) => {
- const [draggedStep, setDraggedStep] = useState()
- const [draggedStepType, setDraggedStepType] = useState()
+ const [draggedStep, setDraggedStep] = useState()
+ const [draggedStepType, setDraggedStepType] = useState<
+ DraggableStepType | undefined
+ >()
return (
void
+ createBlock: (
+ props: Coordinates & { step: BubbleStepType | InputStepType | Step }
+ ) => void
updateBlock: (blockId: string, updates: Partial>) => void
deleteBlock: (blockId: string) => void
}
export const blocksActions = (setTypebot: Updater): BlocksActions => ({
- createBlock: ({ x, y, step }: Coordinates & { step: StepType | Step }) => {
+ createBlock: ({
+ x,
+ y,
+ step,
+ }: Coordinates & { step: BubbleStepType | InputStepType | Step }) => {
setTypebot((typebot) => {
- removeEmptyBlocks(typebot)
const newBlock = parseNewBlock({
totalBlocks: typebot.blocks.allIds.length,
initialCoordinates: { x, y },
@@ -22,6 +27,7 @@ export const blocksActions = (setTypebot: Updater): BlocksActions => ({
typebot.blocks.byId[newBlock.id] = newBlock
typebot.blocks.allIds.push(newBlock.id)
createStepDraft(typebot, step, newBlock.id)
+ removeEmptyBlocks(typebot)
})
},
updateBlock: (blockId: string, updates: Partial>) =>
diff --git a/apps/builder/contexts/TypebotContext/actions/steps.ts b/apps/builder/contexts/TypebotContext/actions/steps.ts
index e721424df..d8f89b317 100644
--- a/apps/builder/contexts/TypebotContext/actions/steps.ts
+++ b/apps/builder/contexts/TypebotContext/actions/steps.ts
@@ -1,11 +1,15 @@
-import { Step, StepType, Typebot } from 'models'
+import { BubbleStepType, InputStepType, Step, Typebot } from 'models'
import { parseNewStep } from 'services/typebots'
import { Updater } from 'use-immer'
import { removeEmptyBlocks } from './blocks'
import { WritableDraft } from 'immer/dist/types/types-external'
export type StepsActions = {
- createStep: (blockId: string, step: StepType | Step, index?: number) => void
+ createStep: (
+ blockId: string,
+ step: BubbleStepType | InputStepType | Step,
+ index?: number
+ ) => void
updateStep: (
stepId: string,
updates: Partial>
@@ -14,10 +18,14 @@ export type StepsActions = {
}
export const stepsAction = (setTypebot: Updater): StepsActions => ({
- createStep: (blockId: string, step: StepType | Step, index?: number) => {
+ createStep: (
+ blockId: string,
+ step: BubbleStepType | InputStepType | Step,
+ index?: number
+ ) => {
setTypebot((typebot) => {
- removeEmptyBlocks(typebot)
createStepDraft(typebot, step, blockId, index)
+ removeEmptyBlocks(typebot)
})
},
updateStep: (stepId: string, updates: Partial>) =>
@@ -28,6 +36,7 @@ export const stepsAction = (setTypebot: Updater): StepsActions => ({
setTypebot((typebot) => {
removeStepIdFromBlock(typebot, stepId)
deleteStepDraft(typebot, stepId)
+ removeEmptyBlocks(typebot)
})
},
})
@@ -56,7 +65,7 @@ export const deleteStepDraft = (
export const createStepDraft = (
typebot: WritableDraft,
- step: StepType | Step,
+ step: BubbleStepType | InputStepType | Step,
blockId: string,
index?: number
) => {
diff --git a/apps/builder/cypress/plugins/database.ts b/apps/builder/cypress/plugins/database.ts
index 3aedaabcd..6629f1003 100644
--- a/apps/builder/cypress/plugins/database.ts
+++ b/apps/builder/cypress/plugins/database.ts
@@ -1,4 +1,4 @@
-import { PublicTypebot, StepType, Typebot } from 'models'
+import { InputStepType, PublicTypebot, Typebot } from 'models'
import { Plan, PrismaClient } from 'db'
import { parseTestTypebot } from './utils'
@@ -58,7 +58,7 @@ const createTypebots = async () => {
byId: {
step1: {
id: 'step1',
- type: StepType.TEXT_INPUT,
+ type: InputStepType.TEXT,
blockId: 'block1',
},
},
diff --git a/apps/builder/cypress/plugins/utils.ts b/apps/builder/cypress/plugins/utils.ts
index 303b9412f..162919a7d 100644
--- a/apps/builder/cypress/plugins/utils.ts
+++ b/apps/builder/cypress/plugins/utils.ts
@@ -1,6 +1,5 @@
import {
Block,
- StepType,
Theme,
BackgroundType,
Settings,
@@ -59,7 +58,7 @@ export const parseTestTypebot = ({
byId: {
step0: {
id: 'step0',
- type: StepType.START,
+ type: 'start',
blockId: 'block0',
label: 'Start',
target: { blockId: 'block1' },
diff --git a/apps/builder/cypress/tests/bubbles.ts b/apps/builder/cypress/tests/bubbles.ts
index f25ff5a26..490e9e34b 100644
--- a/apps/builder/cypress/tests/bubbles.ts
+++ b/apps/builder/cypress/tests/bubbles.ts
@@ -1,5 +1,5 @@
import { parseTestTypebot } from 'cypress/plugins/utils'
-import { StepType } from 'models'
+import { BubbleStepType } from 'models'
describe('Text bubbles', () => {
beforeEach(() => {
@@ -15,7 +15,7 @@ describe('Text bubbles', () => {
step1: {
id: 'step1',
blockId: 'block1',
- type: StepType.TEXT,
+ type: BubbleStepType.TEXT,
content: { html: '', plainText: '', richText: [] },
},
},
diff --git a/apps/builder/cypress/tests/inputs.ts b/apps/builder/cypress/tests/inputs.ts
index c015fc10a..7fb78250a 100644
--- a/apps/builder/cypress/tests/inputs.ts
+++ b/apps/builder/cypress/tests/inputs.ts
@@ -1,42 +1,14 @@
import { parseTestTypebot } from 'cypress/plugins/utils'
-import { StepType } from 'models'
+import { InputStep, InputStepType } from 'models'
describe('Text input', () => {
beforeEach(() => {
cy.task('seed')
- cy.task(
- 'createTypebot',
- parseTestTypebot({
- id: 'typebot3',
- name: 'Typebot #3',
- ownerId: 'test2',
- steps: {
- byId: {
- step1: {
- id: 'step1',
- blockId: 'block1',
- type: StepType.TEXT_INPUT,
- },
- },
- allIds: ['step1'],
- },
- blocks: {
- byId: {
- block1: {
- id: 'block1',
- graphCoordinates: { x: 400, y: 200 },
- title: 'Block #1',
- stepIds: ['step1'],
- },
- },
- allIds: ['block1'],
- },
- })
- )
+ createTypebotWithStep({ type: InputStepType.TEXT })
cy.signOut()
})
- it('text input options should work', () => {
+ it('options should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click()
@@ -48,6 +20,7 @@ describe('Text input', () => {
.type('Your name...')
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
cy.findByRole('button', { name: 'Restart' }).click()
+ cy.findByTestId('step-step1').should('contain.text', 'Your name...')
getIframeBody().findByPlaceholderText('Your name...').should('exist')
getIframeBody().findByRole('button', { name: 'Go' })
cy.findByTestId('step-step1').click({ force: true })
@@ -57,6 +30,68 @@ describe('Text input', () => {
})
})
+describe('Number input', () => {
+ beforeEach(() => {
+ cy.task('seed')
+ createTypebotWithStep({ type: InputStepType.NUMBER })
+ cy.signOut()
+ })
+
+ it.only('options should work', () => {
+ cy.signIn('test2@gmail.com')
+ cy.visit('/typebots/typebot3/edit')
+ cy.findByRole('button', { name: 'Preview' }).click()
+ getIframeBody().findByPlaceholderText('Type your answer...').should('exist')
+ getIframeBody().findByRole('button', { name: 'Send' })
+ cy.findByTestId('step-step1').click({ force: true })
+ cy.findByRole('textbox', { name: 'Placeholder:' })
+ .clear()
+ .type('Your name...')
+ cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
+ cy.findByRole('spinbutton', { name: 'Min:' }).type('0')
+ cy.findByRole('spinbutton', { name: 'Max:' }).type('100')
+ cy.findByRole('spinbutton', { name: 'Step:' }).type('10')
+ cy.findByRole('button', { name: 'Restart' }).click()
+ cy.findByTestId('step-step1').should('contain.text', 'Your name...')
+ getIframeBody()
+ .findByPlaceholderText('Your name...')
+ .should('exist')
+ .type('-1{enter}')
+ .clear()
+ .type('150{enter}')
+ getIframeBody().findByRole('button', { name: 'Go' })
+ cy.findByTestId('step-step1').click({ force: true })
+ })
+})
+
+const createTypebotWithStep = (step: Omit) => {
+ cy.task(
+ 'createTypebot',
+ parseTestTypebot({
+ id: 'typebot3',
+ name: 'Typebot #3',
+ ownerId: 'test2',
+ steps: {
+ byId: {
+ step1: { ...step, id: 'step1', blockId: 'block1' },
+ },
+ allIds: ['step1'],
+ },
+ blocks: {
+ byId: {
+ block1: {
+ id: 'block1',
+ graphCoordinates: { x: 400, y: 200 },
+ title: 'Block #1',
+ stepIds: ['step1'],
+ },
+ },
+ allIds: ['block1'],
+ },
+ })
+ )
+}
+
const getIframeBody = () => {
return cy
.get('#typebot-iframe')
diff --git a/apps/builder/services/publicTypebot.tsx b/apps/builder/services/publicTypebot.tsx
index 55a8aeb96..2afb8597e 100644
--- a/apps/builder/services/publicTypebot.tsx
+++ b/apps/builder/services/publicTypebot.tsx
@@ -1,9 +1,10 @@
-import { InputStep, PublicTypebot, Step, StepType, Typebot } from 'models'
+import { PublicTypebot, Typebot } from 'models'
import { sendRequest } from './utils'
import shortId from 'short-uuid'
import { HStack, Text } from '@chakra-ui/react'
import { CalendarIcon } from 'assets/icons'
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
+import { isInputStep } from 'utils'
export const parseTypebotToPublicTypebot = (
typebot: Typebot
@@ -59,7 +60,7 @@ export const parseSubmissionsColumns = (
.map((blockId) => {
const block = typebot.blocks.byId[blockId]
const inputStepId = block.stepIds.find((stepId) =>
- stepIsInput(typebot.steps.byId[stepId])
+ isInputStep(typebot.steps.byId[stepId])
)
const inputStep = typebot.steps.byId[inputStepId as string]
return {
@@ -80,8 +81,5 @@ const blockContainsInput = (
blockId: string
) =>
typebot.blocks.byId[blockId].stepIds.some((stepId) =>
- stepIsInput(typebot.steps.byId[stepId])
+ isInputStep(typebot.steps.byId[stepId])
)
-
-export const stepIsInput = (step: Step): step is InputStep =>
- step.type === StepType.TEXT_INPUT
diff --git a/apps/builder/services/typebots.ts b/apps/builder/services/typebots.ts
index 12033a450..dd27fa363 100644
--- a/apps/builder/services/typebots.ts
+++ b/apps/builder/services/typebots.ts
@@ -1,14 +1,15 @@
import {
- Step,
- StepType,
Block,
TextStep,
- TextInputStep,
PublicTypebot,
BackgroundType,
Settings,
StartStep,
Theme,
+ BubbleStep,
+ InputStep,
+ BubbleStepType,
+ InputStepType,
} from 'models'
import shortId from 'short-uuid'
import { Typebot } from 'models'
@@ -104,10 +105,13 @@ export const parseNewBlock = ({
}
}
-export const parseNewStep = (type: StepType, blockId: string): Step => {
+export const parseNewStep = (
+ type: BubbleStepType | InputStepType,
+ blockId: string
+): BubbleStep | InputStep => {
const id = `s${shortId.generate()}`
switch (type) {
- case StepType.TEXT: {
+ case BubbleStepType.TEXT: {
const textStep: Pick = {
type,
content: { html: '', richText: [], plainText: '' },
@@ -118,22 +122,12 @@ export const parseNewStep = (type: StepType, blockId: string): Step => {
...textStep,
}
}
- case StepType.TEXT_INPUT: {
- const textStep: Pick = {
- type,
- }
- return {
- id,
- blockId,
- ...textStep,
- }
- }
default: {
- const textStep: Pick = {
- type: StepType.TEXT,
- content: { html: '', richText: [], plainText: '' },
+ return {
+ id,
+ blockId,
+ type,
}
- return { blockId, id, ...textStep }
}
}
}
@@ -180,7 +174,7 @@ export const parseNewTypebot = ({
blockId: startBlockId,
id: startStepId,
label: 'Start',
- type: StepType.START,
+ type: 'start',
}
const startBlock: Block = {
id: startBlockId,
@@ -211,6 +205,3 @@ export const parseNewTypebot = ({
settings,
}
}
-
-export const isStepText = (step: Step): step is TextStep =>
- step.type === StepType.TEXT
diff --git a/apps/builder/services/utils.ts b/apps/builder/services/utils.ts
index 7485a80c7..8062733c7 100644
--- a/apps/builder/services/utils.ts
+++ b/apps/builder/services/utils.ts
@@ -106,3 +106,12 @@ export const uploadFile = async (file: File, key: string) => {
url: upload.ok ? `${url}/${key}` : null,
}
}
+
+export const removeUndefinedFields = (obj: T): T =>
+ Object.keys(obj).reduce(
+ (acc, key) =>
+ obj[key as keyof T] === undefined
+ ? { ...acc }
+ : { ...acc, [key]: obj[key as keyof T] },
+ {} as T
+ )
diff --git a/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx b/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx
index 28c14769b..82fb3fcb5 100644
--- a/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx
+++ b/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx
@@ -1,11 +1,12 @@
import React, { useEffect, useState } from 'react'
import { useAnswers } from '../../../contexts/AnswersContext'
import { useHostAvatars } from '../../../contexts/HostAvatarsContext'
-import { InputStep, Step } from 'models'
-import { isTextInputStep, isTextStep } from '../../../services/utils'
+import { InputStep, InputStepType, Step } from 'models'
import { GuestBubble } from './bubbles/GuestBubble'
import { HostMessageBubble } from './bubbles/HostMessageBubble'
import { TextInput } from './inputs/TextInput'
+import { isInputStep, isTextBubbleStep, isTextInputStep } from 'utils'
+import { NumberInput } from './inputs/NumberInput'
export const ChatStep = ({
step,
@@ -21,9 +22,9 @@ export const ChatStep = ({
onTransitionEnd()
}
- if (isTextStep(step))
+ if (isTextBubbleStep(step))
return
- if (isTextInputStep(step))
+ if (isInputStep(step))
return
return No step
}
@@ -50,5 +51,10 @@ const InputChatStep = ({
if (answer) {
return
}
- return
+ switch (step.type) {
+ case InputStepType.TEXT:
+ return
+ case InputStepType.NUMBER:
+ return
+ }
}
diff --git a/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/HostMessageBubble.tsx b/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/HostMessageBubble.tsx
index cc2720010..fad80bbd7 100644
--- a/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/HostMessageBubble.tsx
+++ b/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/HostMessageBubble.tsx
@@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState } from 'react'
import { useHostAvatars } from '../../../../contexts/HostAvatarsContext'
import { useTypebot } from '../../../../contexts/TypebotContext'
-import { StepType, TextStep } from 'models'
+import { BubbleStepType, StepType, TextStep } from 'models'
import { computeTypingTimeout } from '../../../../services/chat'
import { TypingContent } from './TypingContent'
@@ -62,7 +62,7 @@ export const HostMessageBubble = ({
>
{isTyping ? : <>>}
- {step.type === StepType.TEXT && (
+ {step.type === BubbleStepType.TEXT && (
void
+}
+
+export const NumberInput = ({ step, onSubmit }: NumberInputProps) => {
+ const inputRef = useRef(null)
+ const [inputValue, setInputValue] = useState('')
+
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault()
+ if (inputValue === '') return
+ onSubmit(inputValue)
+ }
+
+ return (
+
+ )
+}
diff --git a/packages/bot-engine/src/index.ts b/packages/bot-engine/src/index.ts
index 482a6ab8c..c089e42a3 100644
--- a/packages/bot-engine/src/index.ts
+++ b/packages/bot-engine/src/index.ts
@@ -1 +1,3 @@
export * from './components/TypebotViewer'
+
+export * from 'util'
diff --git a/packages/bot-engine/src/services/utils.ts b/packages/bot-engine/src/services/utils.ts
deleted file mode 100644
index 7e6c41bf8..000000000
--- a/packages/bot-engine/src/services/utils.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Step, TextStep, StepType, TextInputStep } from 'models'
-
-export const isTextStep = (step: Step): step is TextStep =>
- step.type === StepType.TEXT
-
-export const isTextInputStep = (step: Step): step is TextInputStep =>
- step.type === StepType.TEXT_INPUT
diff --git a/packages/models/src/typebot/steps.ts b/packages/models/src/typebot/steps.ts
index cfe644956..98f1ddf32 100644
--- a/packages/models/src/typebot/steps.ts
+++ b/packages/models/src/typebot/steps.ts
@@ -2,34 +2,53 @@ export type Step = StartStep | BubbleStep | InputStep
export type BubbleStep = TextStep
-export type InputStep = TextInputStep
+export type InputStep = TextInputStep | NumberInputStep
-export enum StepType {
- START = 'start',
+export type StepType = 'start' | BubbleStepType | InputStepType
+
+export enum BubbleStepType {
TEXT = 'text',
- TEXT_INPUT = 'text input',
+}
+
+export enum InputStepType {
+ TEXT = 'text input',
+ NUMBER = 'number input',
}
export type StepBase = { id: string; blockId: string; target?: Target }
export type StartStep = StepBase & {
- type: StepType.START
+ type: 'start'
label: string
}
export type TextStep = StepBase & {
- type: StepType.TEXT
+ type: BubbleStepType.TEXT
content: { html: string; richText: unknown[]; plainText: string }
}
export type TextInputStep = StepBase & {
- type: StepType.TEXT_INPUT
+ type: InputStepType.TEXT
options?: TextInputOptions
}
-export type TextInputOptions = {
+export type NumberInputStep = StepBase & {
+ type: InputStepType.NUMBER
+ options?: NumberInputOptions
+}
+
+type InputOptionsBase = {
labels?: { placeholder?: string; button?: string }
+}
+
+export type TextInputOptions = InputOptionsBase & {
isLong?: boolean
}
+export type NumberInputOptions = InputOptionsBase & {
+ min?: number
+ max?: number
+ step?: number
+}
+
export type Target = { blockId: string; stepId?: string }
diff --git a/packages/utils/src/utils.ts b/packages/utils/src/utils.ts
index 1f1571895..efeb4f612 100644
--- a/packages/utils/src/utils.ts
+++ b/packages/utils/src/utils.ts
@@ -1,4 +1,12 @@
-import { Table } from 'models'
+import {
+ BubbleStepType,
+ InputStep,
+ InputStepType,
+ Step,
+ Table,
+ TextInputStep,
+ TextStep,
+} from 'models'
export const sendRequest = async ({
url,
@@ -32,3 +40,12 @@ export const filterTable = (ids: string[], table: Table): Table => ({
byId: ids.reduce((acc, id) => ({ ...acc, [id]: table.byId[id] }), {}),
allIds: ids,
})
+
+export const isInputStep = (step: Step): step is InputStep =>
+ (Object.values(InputStepType) as string[]).includes(step.type)
+
+export const isTextBubbleStep = (step: Step): step is TextStep =>
+ step.type === BubbleStepType.TEXT
+
+export const isTextInputStep = (step: Step): step is TextInputStep =>
+ step.type === InputStepType.TEXT