feat(inputs): ✨ Add number input
This commit is contained in:
@ -208,3 +208,12 @@ export const DownloadIcon = (props: IconProps) => (
|
|||||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const NumberIcon = (props: IconProps) => (
|
||||||
|
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||||
|
<line x1="4" y1="9" x2="20" y2="9"></line>
|
||||||
|
<line x1="4" y1="15" x2="20" y2="15"></line>
|
||||||
|
<line x1="10" y1="3" x2="8" y2="21"></line>
|
||||||
|
<line x1="16" y1="3" x2="14" y2="21"></line>
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
import { Button, ButtonProps, Flex, HStack } from '@chakra-ui/react'
|
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 { useDnd } from 'contexts/DndContext'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { StepIcon } from './StepIcon'
|
import { StepIcon } from './StepIcon'
|
||||||
import { StepLabel } from './StepLabel'
|
import { StepTypeLabel } from './StepTypeLabel'
|
||||||
|
|
||||||
export const StepCard = ({
|
export const StepCard = ({
|
||||||
type,
|
type,
|
||||||
onMouseDown,
|
onMouseDown,
|
||||||
}: {
|
}: {
|
||||||
type: StepType
|
type: BubbleStepType | InputStepType
|
||||||
onMouseDown: (e: React.MouseEvent, type: StepType) => void
|
onMouseDown: (
|
||||||
|
e: React.MouseEvent,
|
||||||
|
type: BubbleStepType | InputStepType
|
||||||
|
) => void
|
||||||
}) => {
|
}) => {
|
||||||
const { draggedStepType } = useDnd()
|
const { draggedStepType } = useDnd()
|
||||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||||
@ -35,7 +38,7 @@ export const StepCard = ({
|
|||||||
{!isMouseDown && (
|
{!isMouseDown && (
|
||||||
<>
|
<>
|
||||||
<StepIcon type={type} />
|
<StepIcon type={type} />
|
||||||
<StepLabel type={type} />
|
<StepTypeLabel type={type} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
@ -62,7 +65,7 @@ export const StepCardOverlay = ({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<StepIcon type={type} />
|
<StepIcon type={type} />
|
||||||
<StepLabel type={type} />
|
<StepTypeLabel type={type} />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,25 @@
|
|||||||
import { ChatIcon, FlagIcon, TextIcon } from 'assets/icons'
|
import { ChatIcon, FlagIcon, NumberIcon, TextIcon } from 'assets/icons'
|
||||||
import { StepType } from 'models'
|
import { BubbleStepType, InputStepType, StepType } from 'models'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
type StepIconProps = { type: StepType }
|
type StepIconProps = { type: StepType }
|
||||||
|
|
||||||
export const StepIcon = ({ type }: StepIconProps) => {
|
export const StepIcon = ({ type }: StepIconProps) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case StepType.TEXT: {
|
case BubbleStepType.TEXT: {
|
||||||
return <ChatIcon />
|
return <ChatIcon />
|
||||||
}
|
}
|
||||||
case StepType.TEXT: {
|
case InputStepType.TEXT: {
|
||||||
return <TextIcon />
|
return <TextIcon />
|
||||||
}
|
}
|
||||||
case StepType.START: {
|
case InputStepType.NUMBER: {
|
||||||
|
return <NumberIcon />
|
||||||
|
}
|
||||||
|
case 'start': {
|
||||||
return <FlagIcon />
|
return <FlagIcon />
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return <TextIcon />
|
return <></>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>Text</Text>
|
|
||||||
}
|
|
||||||
case StepType.TEXT_INPUT: {
|
|
||||||
return <Text>Text</Text>
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return <></>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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>Text</Text>
|
||||||
|
}
|
||||||
|
case InputStepType.NUMBER: {
|
||||||
|
return <Text>Number</Text>
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,19 +5,11 @@ import {
|
|||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
useEventListener,
|
useEventListener,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { StepType } from 'models'
|
import { BubbleStepType, InputStepType } from 'models'
|
||||||
import { useDnd } from 'contexts/DndContext'
|
import { useDnd } from 'contexts/DndContext'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { StepCard, StepCardOverlay } from './StepCard'
|
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 = () => {
|
export const StepTypesList = () => {
|
||||||
const { setDraggedStepType, draggedStepType } = useDnd()
|
const { setDraggedStepType, draggedStepType } = useDnd()
|
||||||
const [position, setPosition] = useState({
|
const [position, setPosition] = useState({
|
||||||
@ -37,7 +29,10 @@ export const StepTypesList = () => {
|
|||||||
}
|
}
|
||||||
useEventListener('mousemove', handleMouseMove)
|
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 element = e.currentTarget as HTMLDivElement
|
||||||
const rect = element.getBoundingClientRect()
|
const rect = element.getBoundingClientRect()
|
||||||
const relativeX = e.clientX - rect.left
|
const relativeX = e.clientX - rect.left
|
||||||
@ -77,8 +72,8 @@ export const StepTypesList = () => {
|
|||||||
Bubbles
|
Bubbles
|
||||||
</Text>
|
</Text>
|
||||||
<SimpleGrid columns={2} spacing="2">
|
<SimpleGrid columns={2} spacing="2">
|
||||||
{stepListItems.bubbles.map((props) => (
|
{Object.values(BubbleStepType).map((type) => (
|
||||||
<StepCard key={props.type} onMouseDown={handleMouseDown} {...props} />
|
<StepCard key={type} type={type} onMouseDown={handleMouseDown} />
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
@ -86,8 +81,8 @@ export const StepTypesList = () => {
|
|||||||
Inputs
|
Inputs
|
||||||
</Text>
|
</Text>
|
||||||
<SimpleGrid columns={2} spacing="2">
|
<SimpleGrid columns={2} spacing="2">
|
||||||
{stepListItems.inputs.map((props) => (
|
{Object.values(InputStepType).map((type) => (
|
||||||
<StepCard key={props.type} onMouseDown={handleMouseDown} {...props} />
|
<StepCard key={type} type={type} onMouseDown={handleMouseDown} />
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
{draggedStepType && (
|
{draggedStepType && (
|
||||||
|
@ -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 (
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<Stack>
|
||||||
|
<FormLabel mb="0" htmlFor="placeholder">
|
||||||
|
Placeholder:
|
||||||
|
</FormLabel>
|
||||||
|
<DebouncedInput
|
||||||
|
id="placeholder"
|
||||||
|
initialValue={options?.labels?.placeholder ?? 'Type your answer...'}
|
||||||
|
delay={100}
|
||||||
|
onChange={handlePlaceholderChange}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<FormLabel mb="0" htmlFor="button">
|
||||||
|
Button label:
|
||||||
|
</FormLabel>
|
||||||
|
<DebouncedInput
|
||||||
|
id="button"
|
||||||
|
initialValue={options?.labels?.button ?? 'Send'}
|
||||||
|
delay={100}
|
||||||
|
onChange={handleButtonLabelChange}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<HStack justifyContent="space-between">
|
||||||
|
<FormLabel mb="0" htmlFor="min">
|
||||||
|
Min:
|
||||||
|
</FormLabel>
|
||||||
|
<SmartNumberInput
|
||||||
|
id="min"
|
||||||
|
initialValue={options?.min}
|
||||||
|
onValueChange={handleMinChange}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
<HStack justifyContent="space-between">
|
||||||
|
<FormLabel mb="0" htmlFor="max">
|
||||||
|
Max:
|
||||||
|
</FormLabel>
|
||||||
|
<SmartNumberInput
|
||||||
|
id="max"
|
||||||
|
initialValue={options?.max}
|
||||||
|
onValueChange={handleMaxChange}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
<HStack justifyContent="space-between">
|
||||||
|
<FormLabel mb="0" htmlFor="step">
|
||||||
|
Step:
|
||||||
|
</FormLabel>
|
||||||
|
<SmartNumberInput
|
||||||
|
id="step"
|
||||||
|
initialValue={options?.step}
|
||||||
|
onValueChange={handleStepChange}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { PopoverContent, PopoverArrow, PopoverBody } from '@chakra-ui/react'
|
import { PopoverContent, PopoverArrow, PopoverBody } from '@chakra-ui/react'
|
||||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
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'
|
import { TextInputSettingsBody } from './TextInputSettingsBody'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -25,7 +26,7 @@ const SettingsPopoverBodyContent = ({ step }: Props) => {
|
|||||||
updateStep(step.id, { options } as Partial<Step>)
|
updateStep(step.id, { options } as Partial<Step>)
|
||||||
|
|
||||||
switch (step.type) {
|
switch (step.type) {
|
||||||
case StepType.TEXT_INPUT: {
|
case InputStepType.TEXT: {
|
||||||
return (
|
return (
|
||||||
<TextInputSettingsBody
|
<TextInputSettingsBody
|
||||||
options={step.options}
|
options={step.options}
|
||||||
@ -33,6 +34,14 @@ const SettingsPopoverBodyContent = ({ step }: Props) => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
case InputStepType.NUMBER: {
|
||||||
|
return (
|
||||||
|
<NumberInputSettingsBody
|
||||||
|
options={step.options}
|
||||||
|
onOptionsChange={handleOptionsChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
@ -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 (
|
|
||||||
<Flex
|
|
||||||
flexDir={'column'}
|
|
||||||
opacity={props.content.html === '' ? '0.5' : '1'}
|
|
||||||
className="slate-html-container"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html:
|
|
||||||
props.content.html === ''
|
|
||||||
? `<p>Click to edit...</p>`
|
|
||||||
: props.content.html,
|
|
||||||
}}
|
|
||||||
></Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
case StepType.TEXT_INPUT: {
|
|
||||||
return <Text color={'gray.500'}>Type your answer...</Text>
|
|
||||||
}
|
|
||||||
case StepType.START: {
|
|
||||||
return <Text>{props.label}</Text>
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return <Text>No input</Text>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,15 +11,15 @@ import { Block, Step } from 'models'
|
|||||||
import { SourceEndpoint } from './SourceEndpoint'
|
import { SourceEndpoint } from './SourceEndpoint'
|
||||||
import { useGraph } from 'contexts/GraphContext'
|
import { useGraph } from 'contexts/GraphContext'
|
||||||
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
|
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 { Coordinates } from '@dnd-kit/core/dist/types'
|
||||||
import { TextEditor } from './TextEditor/TextEditor'
|
import { TextEditor } from './TextEditor/TextEditor'
|
||||||
import { StepContent } from './StepContent'
|
import { StepNodeLabel } from './StepNodeLabel'
|
||||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||||
import { ContextMenu } from 'components/shared/ContextMenu'
|
import { ContextMenu } from 'components/shared/ContextMenu'
|
||||||
import { StepNodeContextMenu } from './RightClickMenu'
|
import { StepNodeContextMenu } from './RightClickMenu'
|
||||||
import { SettingsPopoverContent } from './SettingsPopoverContent'
|
import { SettingsPopoverContent } from './SettingsPopoverContent'
|
||||||
import { isStepText } from 'services/typebots'
|
import { DraggableStep } from 'contexts/DndContext'
|
||||||
|
|
||||||
export const StepNode = ({
|
export const StepNode = ({
|
||||||
step,
|
step,
|
||||||
@ -34,7 +34,7 @@ export const StepNode = ({
|
|||||||
onMouseMoveTopOfElement?: () => void
|
onMouseMoveTopOfElement?: () => void
|
||||||
onMouseDown?: (
|
onMouseDown?: (
|
||||||
stepNodePosition: { absolute: Coordinates; relative: Coordinates },
|
stepNodePosition: { absolute: Coordinates; relative: Coordinates },
|
||||||
step: Step
|
step: DraggableStep
|
||||||
) => void
|
) => void
|
||||||
}) => {
|
}) => {
|
||||||
const { setConnectingIds, connectingIds } = useGraph()
|
const { setConnectingIds, connectingIds } = useGraph()
|
||||||
@ -43,7 +43,7 @@ export const StepNode = ({
|
|||||||
const [mouseDownEvent, setMouseDownEvent] =
|
const [mouseDownEvent, setMouseDownEvent] =
|
||||||
useState<{ absolute: Coordinates; relative: Coordinates }>()
|
useState<{ absolute: Coordinates; relative: Coordinates }>()
|
||||||
const [isEditing, setIsEditing] = useState<boolean>(
|
const [isEditing, setIsEditing] = useState<boolean>(
|
||||||
isStepText(step) && step.content.plainText === ''
|
isTextBubbleStep(step) && step.content.plainText === ''
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -102,8 +102,8 @@ export const StepNode = ({
|
|||||||
mouseDownEvent &&
|
mouseDownEvent &&
|
||||||
onMouseDown &&
|
onMouseDown &&
|
||||||
(event.movementX > 0 || event.movementY > 0)
|
(event.movementX > 0 || event.movementY > 0)
|
||||||
if (isMovingAndIsMouseDown) {
|
if (isMovingAndIsMouseDown && step.type !== 'start') {
|
||||||
onMouseDown(mouseDownEvent, step as Step)
|
onMouseDown(mouseDownEvent, step)
|
||||||
deleteStep(step.id)
|
deleteStep(step.id)
|
||||||
setMouseDownEvent(undefined)
|
setMouseDownEvent(undefined)
|
||||||
}
|
}
|
||||||
@ -142,7 +142,7 @@ export const StepNode = ({
|
|||||||
connectingIds?.target?.blockId,
|
connectingIds?.target?.blockId,
|
||||||
])
|
])
|
||||||
|
|
||||||
return isEditing && isStepText(step) ? (
|
return isEditing && isTextBubbleStep(step) ? (
|
||||||
<TextEditor
|
<TextEditor
|
||||||
stepId={step.id}
|
stepId={step.id}
|
||||||
initialValue={step.content.richText}
|
initialValue={step.content.richText}
|
||||||
@ -186,7 +186,7 @@ export const StepNode = ({
|
|||||||
bgColor="white"
|
bgColor="white"
|
||||||
>
|
>
|
||||||
<StepIcon type={step.type} />
|
<StepIcon type={step.type} />
|
||||||
<StepContent {...step} />
|
<StepNodeLabel {...step} />
|
||||||
{isConnectable && (
|
{isConnectable && (
|
||||||
<SourceEndpoint
|
<SourceEndpoint
|
||||||
onConnectionDragStart={handleConnectionDragStart}
|
onConnectionDragStart={handleConnectionDragStart}
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
import { Flex, Text } from '@chakra-ui/react'
|
||||||
|
import { Step, StartStep, BubbleStepType, InputStepType } from 'models'
|
||||||
|
|
||||||
|
export const StepNodeLabel = (props: Step | StartStep) => {
|
||||||
|
switch (props.type) {
|
||||||
|
case BubbleStepType.TEXT: {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
flexDir={'column'}
|
||||||
|
opacity={props.content.html === '' ? '0.5' : '1'}
|
||||||
|
className="slate-html-container"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html:
|
||||||
|
props.content.html === ''
|
||||||
|
? `<p>Click to edit...</p>`
|
||||||
|
: props.content.html,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case InputStepType.TEXT: {
|
||||||
|
return (
|
||||||
|
<Text color={'gray.500'}>
|
||||||
|
{props.options?.labels?.placeholder ?? 'Type your answer...'}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case InputStepType.NUMBER: {
|
||||||
|
return (
|
||||||
|
<Text color={'gray.500'}>
|
||||||
|
{props.options?.labels?.placeholder ?? 'Type your answer...'}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case 'start': {
|
||||||
|
return <Text>{props.label}</Text>
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return <Text>No input</Text>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { StackProps, HStack } from '@chakra-ui/react'
|
import { StackProps, HStack } from '@chakra-ui/react'
|
||||||
import { StartStep, Step } from 'models'
|
import { StartStep, Step } from 'models'
|
||||||
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
|
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
|
||||||
import { StepContent } from './StepContent'
|
import { StepNodeLabel } from './StepNodeLabel'
|
||||||
|
|
||||||
export const StepNodeOverlay = ({
|
export const StepNodeOverlay = ({
|
||||||
step,
|
step,
|
||||||
@ -19,7 +19,7 @@ export const StepNodeOverlay = ({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<StepIcon type={step.type} />
|
<StepIcon type={step.type} />
|
||||||
<StepContent {...step} />
|
<StepNodeLabel {...step} />
|
||||||
</HStack>
|
</HStack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react'
|
import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react'
|
||||||
import { Step, Table } from 'models'
|
import { Step, Table } from 'models'
|
||||||
import { useDnd } from 'contexts/DndContext'
|
import { DraggableStep, useDnd } from 'contexts/DndContext'
|
||||||
import { Coordinates } from 'contexts/GraphContext'
|
import { Coordinates } from 'contexts/GraphContext'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { StepNode, StepNodeOverlay } from './StepNode'
|
import { StepNode, StepNodeOverlay } from './StepNode'
|
||||||
@ -54,7 +54,7 @@ export const StepsList = ({
|
|||||||
|
|
||||||
const handleStepMouseDown = (
|
const handleStepMouseDown = (
|
||||||
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
|
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
|
||||||
step: Step
|
step: DraggableStep
|
||||||
) => {
|
) => {
|
||||||
setPosition(absolute)
|
setPosition(absolute)
|
||||||
setRelativeCoordinates(relative)
|
setRelativeCoordinates(relative)
|
||||||
|
@ -2,11 +2,10 @@ import { Flex, FlexProps, useEventListener } from '@chakra-ui/react'
|
|||||||
import React, { useRef, useMemo } from 'react'
|
import React, { useRef, useMemo } from 'react'
|
||||||
import { blockWidth, useGraph } from 'contexts/GraphContext'
|
import { blockWidth, useGraph } from 'contexts/GraphContext'
|
||||||
import { BlockNode } from './BlockNode/BlockNode'
|
import { BlockNode } from './BlockNode/BlockNode'
|
||||||
import { useDnd } from 'contexts/DndContext'
|
import { DraggableStepType, useDnd } from 'contexts/DndContext'
|
||||||
import { Edges } from './Edges'
|
import { Edges } from './Edges'
|
||||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||||
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
|
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
|
||||||
import { StepType } from 'models'
|
|
||||||
|
|
||||||
const Graph = ({ ...props }: FlexProps) => {
|
const Graph = ({ ...props }: FlexProps) => {
|
||||||
const { draggedStepType, setDraggedStepType, draggedStep, setDraggedStep } =
|
const { draggedStepType, setDraggedStepType, draggedStep, setDraggedStep } =
|
||||||
@ -44,7 +43,7 @@ const Graph = ({ ...props }: FlexProps) => {
|
|||||||
createBlock({
|
createBlock({
|
||||||
x: e.clientX - graphPosition.x - blockWidth / 3,
|
x: e.clientX - graphPosition.x - blockWidth / 3,
|
||||||
y: e.clientY - graphPosition.y - 20 - headerHeight,
|
y: e.clientY - graphPosition.y - 20 - headerHeight,
|
||||||
step: draggedStep ?? (draggedStepType as StepType),
|
step: draggedStep ?? (draggedStepType as DraggableStepType),
|
||||||
})
|
})
|
||||||
setDraggedStep(undefined)
|
setDraggedStep(undefined)
|
||||||
setDraggedStepType(undefined)
|
setDraggedStepType(undefined)
|
||||||
|
@ -13,14 +13,14 @@ export const SmartNumberInput = ({
|
|||||||
onValueChange,
|
onValueChange,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
initialValue: number
|
initialValue?: number
|
||||||
onValueChange: (value: number) => void
|
onValueChange: (value?: number) => void
|
||||||
} & NumberInputProps) => {
|
} & NumberInputProps) => {
|
||||||
const [value, setValue] = useState(initialValue.toString())
|
const [value, setValue] = useState(initialValue?.toString() ?? '')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value.endsWith('.') || value.endsWith(',')) return
|
if (value.endsWith('.') || value.endsWith(',')) return
|
||||||
if (value === '') onValueChange(0)
|
if (value === '') onValueChange(undefined)
|
||||||
const newValue = parseFloat(value)
|
const newValue = parseFloat(value)
|
||||||
if (isNaN(newValue)) return
|
if (isNaN(newValue)) return
|
||||||
onValueChange(newValue)
|
onValueChange(newValue)
|
||||||
|
@ -17,14 +17,14 @@ export const TypingEmulation = ({
|
|||||||
onUpdate({ ...typingEmulation, enabled: !typingEmulation.enabled })
|
onUpdate({ ...typingEmulation, enabled: !typingEmulation.enabled })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSpeedChange = (speed: number) => {
|
const handleSpeedChange = (speed?: number) => {
|
||||||
if (!typingEmulation) return
|
if (!typingEmulation) return
|
||||||
onUpdate({ ...typingEmulation, speed })
|
onUpdate({ ...typingEmulation, speed: speed ?? 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMaxDelayChange = (maxDelay: number) => {
|
const handleMaxDelayChange = (maxDelay?: number) => {
|
||||||
if (!typingEmulation) return
|
if (!typingEmulation) return
|
||||||
onUpdate({ ...typingEmulation, maxDelay: maxDelay })
|
onUpdate({ ...typingEmulation, maxDelay: maxDelay ?? 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Step, StepType } from 'models'
|
import { BubbleStep, BubbleStepType, InputStep, InputStepType } from 'models'
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
Dispatch,
|
Dispatch,
|
||||||
@ -8,19 +8,24 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
|
||||||
|
export type DraggableStep = BubbleStep | InputStep
|
||||||
|
export type DraggableStepType = BubbleStepType | InputStepType
|
||||||
|
|
||||||
const dndContext = createContext<{
|
const dndContext = createContext<{
|
||||||
draggedStepType?: StepType
|
draggedStepType?: DraggableStepType
|
||||||
setDraggedStepType: Dispatch<SetStateAction<StepType | undefined>>
|
setDraggedStepType: Dispatch<SetStateAction<DraggableStepType | undefined>>
|
||||||
draggedStep?: Step
|
draggedStep?: DraggableStep
|
||||||
setDraggedStep: Dispatch<SetStateAction<Step | undefined>>
|
setDraggedStep: Dispatch<SetStateAction<DraggableStep | undefined>>
|
||||||
}>({
|
}>({
|
||||||
setDraggedStep: () => console.log("I'm not implemented"),
|
setDraggedStep: () => console.log("I'm not implemented"),
|
||||||
setDraggedStepType: () => console.log("I'm not implemented"),
|
setDraggedStepType: () => console.log("I'm not implemented"),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const DndContext = ({ children }: { children: ReactNode }) => {
|
export const DndContext = ({ children }: { children: ReactNode }) => {
|
||||||
const [draggedStep, setDraggedStep] = useState<Step | undefined>()
|
const [draggedStep, setDraggedStep] = useState<DraggableStep | undefined>()
|
||||||
const [draggedStepType, setDraggedStepType] = useState<StepType | undefined>()
|
const [draggedStepType, setDraggedStepType] = useState<
|
||||||
|
DraggableStepType | undefined
|
||||||
|
>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dndContext.Provider
|
<dndContext.Provider
|
||||||
|
@ -1,20 +1,25 @@
|
|||||||
import { Coordinates } from 'contexts/GraphContext'
|
import { Coordinates } from 'contexts/GraphContext'
|
||||||
import { WritableDraft } from 'immer/dist/internal'
|
import { WritableDraft } from 'immer/dist/internal'
|
||||||
import { Block, Step, StepType, Typebot } from 'models'
|
import { Block, BubbleStepType, InputStepType, Step, Typebot } from 'models'
|
||||||
import { parseNewBlock } from 'services/typebots'
|
import { parseNewBlock } from 'services/typebots'
|
||||||
import { Updater } from 'use-immer'
|
import { Updater } from 'use-immer'
|
||||||
import { createStepDraft, deleteStepDraft } from './steps'
|
import { createStepDraft, deleteStepDraft } from './steps'
|
||||||
|
|
||||||
export type BlocksActions = {
|
export type BlocksActions = {
|
||||||
createBlock: (props: Coordinates & { step: StepType | Step }) => void
|
createBlock: (
|
||||||
|
props: Coordinates & { step: BubbleStepType | InputStepType | Step }
|
||||||
|
) => void
|
||||||
updateBlock: (blockId: string, updates: Partial<Omit<Block, 'id'>>) => void
|
updateBlock: (blockId: string, updates: Partial<Omit<Block, 'id'>>) => void
|
||||||
deleteBlock: (blockId: string) => void
|
deleteBlock: (blockId: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const blocksActions = (setTypebot: Updater<Typebot>): BlocksActions => ({
|
export const blocksActions = (setTypebot: Updater<Typebot>): BlocksActions => ({
|
||||||
createBlock: ({ x, y, step }: Coordinates & { step: StepType | Step }) => {
|
createBlock: ({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
step,
|
||||||
|
}: Coordinates & { step: BubbleStepType | InputStepType | Step }) => {
|
||||||
setTypebot((typebot) => {
|
setTypebot((typebot) => {
|
||||||
removeEmptyBlocks(typebot)
|
|
||||||
const newBlock = parseNewBlock({
|
const newBlock = parseNewBlock({
|
||||||
totalBlocks: typebot.blocks.allIds.length,
|
totalBlocks: typebot.blocks.allIds.length,
|
||||||
initialCoordinates: { x, y },
|
initialCoordinates: { x, y },
|
||||||
@ -22,6 +27,7 @@ export const blocksActions = (setTypebot: Updater<Typebot>): BlocksActions => ({
|
|||||||
typebot.blocks.byId[newBlock.id] = newBlock
|
typebot.blocks.byId[newBlock.id] = newBlock
|
||||||
typebot.blocks.allIds.push(newBlock.id)
|
typebot.blocks.allIds.push(newBlock.id)
|
||||||
createStepDraft(typebot, step, newBlock.id)
|
createStepDraft(typebot, step, newBlock.id)
|
||||||
|
removeEmptyBlocks(typebot)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateBlock: (blockId: string, updates: Partial<Omit<Block, 'id'>>) =>
|
updateBlock: (blockId: string, updates: Partial<Omit<Block, 'id'>>) =>
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import { Step, StepType, Typebot } from 'models'
|
import { BubbleStepType, InputStepType, Step, Typebot } from 'models'
|
||||||
import { parseNewStep } from 'services/typebots'
|
import { parseNewStep } from 'services/typebots'
|
||||||
import { Updater } from 'use-immer'
|
import { Updater } from 'use-immer'
|
||||||
import { removeEmptyBlocks } from './blocks'
|
import { removeEmptyBlocks } from './blocks'
|
||||||
import { WritableDraft } from 'immer/dist/types/types-external'
|
import { WritableDraft } from 'immer/dist/types/types-external'
|
||||||
|
|
||||||
export type StepsActions = {
|
export type StepsActions = {
|
||||||
createStep: (blockId: string, step: StepType | Step, index?: number) => void
|
createStep: (
|
||||||
|
blockId: string,
|
||||||
|
step: BubbleStepType | InputStepType | Step,
|
||||||
|
index?: number
|
||||||
|
) => void
|
||||||
updateStep: (
|
updateStep: (
|
||||||
stepId: string,
|
stepId: string,
|
||||||
updates: Partial<Omit<Step, 'id' | 'type'>>
|
updates: Partial<Omit<Step, 'id' | 'type'>>
|
||||||
@ -14,10 +18,14 @@ export type StepsActions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const stepsAction = (setTypebot: Updater<Typebot>): StepsActions => ({
|
export const stepsAction = (setTypebot: Updater<Typebot>): StepsActions => ({
|
||||||
createStep: (blockId: string, step: StepType | Step, index?: number) => {
|
createStep: (
|
||||||
|
blockId: string,
|
||||||
|
step: BubbleStepType | InputStepType | Step,
|
||||||
|
index?: number
|
||||||
|
) => {
|
||||||
setTypebot((typebot) => {
|
setTypebot((typebot) => {
|
||||||
removeEmptyBlocks(typebot)
|
|
||||||
createStepDraft(typebot, step, blockId, index)
|
createStepDraft(typebot, step, blockId, index)
|
||||||
|
removeEmptyBlocks(typebot)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateStep: (stepId: string, updates: Partial<Omit<Step, 'id' | 'type'>>) =>
|
updateStep: (stepId: string, updates: Partial<Omit<Step, 'id' | 'type'>>) =>
|
||||||
@ -28,6 +36,7 @@ export const stepsAction = (setTypebot: Updater<Typebot>): StepsActions => ({
|
|||||||
setTypebot((typebot) => {
|
setTypebot((typebot) => {
|
||||||
removeStepIdFromBlock(typebot, stepId)
|
removeStepIdFromBlock(typebot, stepId)
|
||||||
deleteStepDraft(typebot, stepId)
|
deleteStepDraft(typebot, stepId)
|
||||||
|
removeEmptyBlocks(typebot)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -56,7 +65,7 @@ export const deleteStepDraft = (
|
|||||||
|
|
||||||
export const createStepDraft = (
|
export const createStepDraft = (
|
||||||
typebot: WritableDraft<Typebot>,
|
typebot: WritableDraft<Typebot>,
|
||||||
step: StepType | Step,
|
step: BubbleStepType | InputStepType | Step,
|
||||||
blockId: string,
|
blockId: string,
|
||||||
index?: number
|
index?: number
|
||||||
) => {
|
) => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { PublicTypebot, StepType, Typebot } from 'models'
|
import { InputStepType, PublicTypebot, Typebot } from 'models'
|
||||||
import { Plan, PrismaClient } from 'db'
|
import { Plan, PrismaClient } from 'db'
|
||||||
import { parseTestTypebot } from './utils'
|
import { parseTestTypebot } from './utils'
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ const createTypebots = async () => {
|
|||||||
byId: {
|
byId: {
|
||||||
step1: {
|
step1: {
|
||||||
id: 'step1',
|
id: 'step1',
|
||||||
type: StepType.TEXT_INPUT,
|
type: InputStepType.TEXT,
|
||||||
blockId: 'block1',
|
blockId: 'block1',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Block,
|
Block,
|
||||||
StepType,
|
|
||||||
Theme,
|
Theme,
|
||||||
BackgroundType,
|
BackgroundType,
|
||||||
Settings,
|
Settings,
|
||||||
@ -59,7 +58,7 @@ export const parseTestTypebot = ({
|
|||||||
byId: {
|
byId: {
|
||||||
step0: {
|
step0: {
|
||||||
id: 'step0',
|
id: 'step0',
|
||||||
type: StepType.START,
|
type: 'start',
|
||||||
blockId: 'block0',
|
blockId: 'block0',
|
||||||
label: 'Start',
|
label: 'Start',
|
||||||
target: { blockId: 'block1' },
|
target: { blockId: 'block1' },
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { parseTestTypebot } from 'cypress/plugins/utils'
|
import { parseTestTypebot } from 'cypress/plugins/utils'
|
||||||
import { StepType } from 'models'
|
import { BubbleStepType } from 'models'
|
||||||
|
|
||||||
describe('Text bubbles', () => {
|
describe('Text bubbles', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -15,7 +15,7 @@ describe('Text bubbles', () => {
|
|||||||
step1: {
|
step1: {
|
||||||
id: 'step1',
|
id: 'step1',
|
||||||
blockId: 'block1',
|
blockId: 'block1',
|
||||||
type: StepType.TEXT,
|
type: BubbleStepType.TEXT,
|
||||||
content: { html: '', plainText: '', richText: [] },
|
content: { html: '', plainText: '', richText: [] },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,42 +1,14 @@
|
|||||||
import { parseTestTypebot } from 'cypress/plugins/utils'
|
import { parseTestTypebot } from 'cypress/plugins/utils'
|
||||||
import { StepType } from 'models'
|
import { InputStep, InputStepType } from 'models'
|
||||||
|
|
||||||
describe('Text input', () => {
|
describe('Text input', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.task('seed')
|
cy.task('seed')
|
||||||
cy.task(
|
createTypebotWithStep({ type: InputStepType.TEXT })
|
||||||
'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'],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
cy.signOut()
|
cy.signOut()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('text input options should work', () => {
|
it('options should work', () => {
|
||||||
cy.signIn('test2@gmail.com')
|
cy.signIn('test2@gmail.com')
|
||||||
cy.visit('/typebots/typebot3/edit')
|
cy.visit('/typebots/typebot3/edit')
|
||||||
cy.findByRole('button', { name: 'Preview' }).click()
|
cy.findByRole('button', { name: 'Preview' }).click()
|
||||||
@ -48,6 +20,7 @@ describe('Text input', () => {
|
|||||||
.type('Your name...')
|
.type('Your name...')
|
||||||
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
|
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
|
||||||
cy.findByRole('button', { name: 'Restart' }).click()
|
cy.findByRole('button', { name: 'Restart' }).click()
|
||||||
|
cy.findByTestId('step-step1').should('contain.text', 'Your name...')
|
||||||
getIframeBody().findByPlaceholderText('Your name...').should('exist')
|
getIframeBody().findByPlaceholderText('Your name...').should('exist')
|
||||||
getIframeBody().findByRole('button', { name: 'Go' })
|
getIframeBody().findByRole('button', { name: 'Go' })
|
||||||
cy.findByTestId('step-step1').click({ force: true })
|
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<InputStep, 'id' | 'blockId'>) => {
|
||||||
|
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 = () => {
|
const getIframeBody = () => {
|
||||||
return cy
|
return cy
|
||||||
.get('#typebot-iframe')
|
.get('#typebot-iframe')
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { InputStep, PublicTypebot, Step, StepType, Typebot } from 'models'
|
import { PublicTypebot, Typebot } from 'models'
|
||||||
import { sendRequest } from './utils'
|
import { sendRequest } from './utils'
|
||||||
import shortId from 'short-uuid'
|
import shortId from 'short-uuid'
|
||||||
import { HStack, Text } from '@chakra-ui/react'
|
import { HStack, Text } from '@chakra-ui/react'
|
||||||
import { CalendarIcon } from 'assets/icons'
|
import { CalendarIcon } from 'assets/icons'
|
||||||
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
|
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
|
||||||
|
import { isInputStep } from 'utils'
|
||||||
|
|
||||||
export const parseTypebotToPublicTypebot = (
|
export const parseTypebotToPublicTypebot = (
|
||||||
typebot: Typebot
|
typebot: Typebot
|
||||||
@ -59,7 +60,7 @@ export const parseSubmissionsColumns = (
|
|||||||
.map((blockId) => {
|
.map((blockId) => {
|
||||||
const block = typebot.blocks.byId[blockId]
|
const block = typebot.blocks.byId[blockId]
|
||||||
const inputStepId = block.stepIds.find((stepId) =>
|
const inputStepId = block.stepIds.find((stepId) =>
|
||||||
stepIsInput(typebot.steps.byId[stepId])
|
isInputStep(typebot.steps.byId[stepId])
|
||||||
)
|
)
|
||||||
const inputStep = typebot.steps.byId[inputStepId as string]
|
const inputStep = typebot.steps.byId[inputStepId as string]
|
||||||
return {
|
return {
|
||||||
@ -80,8 +81,5 @@ const blockContainsInput = (
|
|||||||
blockId: string
|
blockId: string
|
||||||
) =>
|
) =>
|
||||||
typebot.blocks.byId[blockId].stepIds.some((stepId) =>
|
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
|
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
Step,
|
|
||||||
StepType,
|
|
||||||
Block,
|
Block,
|
||||||
TextStep,
|
TextStep,
|
||||||
TextInputStep,
|
|
||||||
PublicTypebot,
|
PublicTypebot,
|
||||||
BackgroundType,
|
BackgroundType,
|
||||||
Settings,
|
Settings,
|
||||||
StartStep,
|
StartStep,
|
||||||
Theme,
|
Theme,
|
||||||
|
BubbleStep,
|
||||||
|
InputStep,
|
||||||
|
BubbleStepType,
|
||||||
|
InputStepType,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import shortId from 'short-uuid'
|
import shortId from 'short-uuid'
|
||||||
import { Typebot } from 'models'
|
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()}`
|
const id = `s${shortId.generate()}`
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case StepType.TEXT: {
|
case BubbleStepType.TEXT: {
|
||||||
const textStep: Pick<TextStep, 'type' | 'content'> = {
|
const textStep: Pick<TextStep, 'type' | 'content'> = {
|
||||||
type,
|
type,
|
||||||
content: { html: '', richText: [], plainText: '' },
|
content: { html: '', richText: [], plainText: '' },
|
||||||
@ -118,22 +122,12 @@ export const parseNewStep = (type: StepType, blockId: string): Step => {
|
|||||||
...textStep,
|
...textStep,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case StepType.TEXT_INPUT: {
|
|
||||||
const textStep: Pick<TextInputStep, 'type'> = {
|
|
||||||
type,
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
blockId,
|
|
||||||
...textStep,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default: {
|
default: {
|
||||||
const textStep: Pick<TextStep, 'type' | 'content'> = {
|
return {
|
||||||
type: StepType.TEXT,
|
id,
|
||||||
content: { html: '', richText: [], plainText: '' },
|
blockId,
|
||||||
|
type,
|
||||||
}
|
}
|
||||||
return { blockId, id, ...textStep }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -180,7 +174,7 @@ export const parseNewTypebot = ({
|
|||||||
blockId: startBlockId,
|
blockId: startBlockId,
|
||||||
id: startStepId,
|
id: startStepId,
|
||||||
label: 'Start',
|
label: 'Start',
|
||||||
type: StepType.START,
|
type: 'start',
|
||||||
}
|
}
|
||||||
const startBlock: Block = {
|
const startBlock: Block = {
|
||||||
id: startBlockId,
|
id: startBlockId,
|
||||||
@ -211,6 +205,3 @@ export const parseNewTypebot = ({
|
|||||||
settings,
|
settings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isStepText = (step: Step): step is TextStep =>
|
|
||||||
step.type === StepType.TEXT
|
|
||||||
|
@ -106,3 +106,12 @@ export const uploadFile = async (file: File, key: string) => {
|
|||||||
url: upload.ok ? `${url}/${key}` : null,
|
url: upload.ok ? `${url}/${key}` : null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const removeUndefinedFields = <T>(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
|
||||||
|
)
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useAnswers } from '../../../contexts/AnswersContext'
|
import { useAnswers } from '../../../contexts/AnswersContext'
|
||||||
import { useHostAvatars } from '../../../contexts/HostAvatarsContext'
|
import { useHostAvatars } from '../../../contexts/HostAvatarsContext'
|
||||||
import { InputStep, Step } from 'models'
|
import { InputStep, InputStepType, Step } from 'models'
|
||||||
import { isTextInputStep, isTextStep } from '../../../services/utils'
|
|
||||||
import { GuestBubble } from './bubbles/GuestBubble'
|
import { GuestBubble } from './bubbles/GuestBubble'
|
||||||
import { HostMessageBubble } from './bubbles/HostMessageBubble'
|
import { HostMessageBubble } from './bubbles/HostMessageBubble'
|
||||||
import { TextInput } from './inputs/TextInput'
|
import { TextInput } from './inputs/TextInput'
|
||||||
|
import { isInputStep, isTextBubbleStep, isTextInputStep } from 'utils'
|
||||||
|
import { NumberInput } from './inputs/NumberInput'
|
||||||
|
|
||||||
export const ChatStep = ({
|
export const ChatStep = ({
|
||||||
step,
|
step,
|
||||||
@ -21,9 +22,9 @@ export const ChatStep = ({
|
|||||||
onTransitionEnd()
|
onTransitionEnd()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextStep(step))
|
if (isTextBubbleStep(step))
|
||||||
return <HostMessageBubble step={step} onTransitionEnd={onTransitionEnd} />
|
return <HostMessageBubble step={step} onTransitionEnd={onTransitionEnd} />
|
||||||
if (isTextInputStep(step))
|
if (isInputStep(step))
|
||||||
return <InputChatStep step={step} onSubmit={handleInputSubmit} />
|
return <InputChatStep step={step} onSubmit={handleInputSubmit} />
|
||||||
return <span>No step</span>
|
return <span>No step</span>
|
||||||
}
|
}
|
||||||
@ -50,5 +51,10 @@ const InputChatStep = ({
|
|||||||
if (answer) {
|
if (answer) {
|
||||||
return <GuestBubble message={answer} />
|
return <GuestBubble message={answer} />
|
||||||
}
|
}
|
||||||
return <TextInput step={step} onSubmit={handleSubmit} />
|
switch (step.type) {
|
||||||
|
case InputStepType.TEXT:
|
||||||
|
return <TextInput step={step} onSubmit={handleSubmit} />
|
||||||
|
case InputStepType.NUMBER:
|
||||||
|
return <NumberInput step={step} onSubmit={handleSubmit} />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { useHostAvatars } from '../../../../contexts/HostAvatarsContext'
|
import { useHostAvatars } from '../../../../contexts/HostAvatarsContext'
|
||||||
import { useTypebot } from '../../../../contexts/TypebotContext'
|
import { useTypebot } from '../../../../contexts/TypebotContext'
|
||||||
import { StepType, TextStep } from 'models'
|
import { BubbleStepType, StepType, TextStep } from 'models'
|
||||||
import { computeTypingTimeout } from '../../../../services/chat'
|
import { computeTypingTimeout } from '../../../../services/chat'
|
||||||
import { TypingContent } from './TypingContent'
|
import { TypingContent } from './TypingContent'
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ export const HostMessageBubble = ({
|
|||||||
>
|
>
|
||||||
{isTyping ? <TypingContent /> : <></>}
|
{isTyping ? <TypingContent /> : <></>}
|
||||||
</div>
|
</div>
|
||||||
{step.type === StepType.TEXT && (
|
{step.type === BubbleStepType.TEXT && (
|
||||||
<p
|
<p
|
||||||
style={{
|
style={{
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
import { NumberInputStep, TextInputStep } from 'models'
|
||||||
|
import React, { FormEvent, useRef, useState } from 'react'
|
||||||
|
import { SendIcon } from '../../../../assets/icons'
|
||||||
|
|
||||||
|
type NumberInputProps = {
|
||||||
|
step: NumberInputStep
|
||||||
|
onSubmit: (value: string) => 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 (
|
||||||
|
<div className="flex flex-col w-full lg:w-4/6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<form
|
||||||
|
className="flex items-end justify-between rounded-lg pr-2 typebot-input"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
className="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full comp-input"
|
||||||
|
type="number"
|
||||||
|
placeholder={
|
||||||
|
step.options?.labels?.placeholder ?? 'Type your answer...'
|
||||||
|
}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
style={{ appearance: 'auto' }}
|
||||||
|
min={step.options?.min}
|
||||||
|
max={step.options?.max}
|
||||||
|
step={step.options?.step}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={
|
||||||
|
'my-2 ml-2 py-2 px-4 font-semibold rounded-md text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 typebot-button active'
|
||||||
|
}
|
||||||
|
disabled={inputValue === ''}
|
||||||
|
>
|
||||||
|
<span className="hidden xs:flex">
|
||||||
|
{step.options?.labels?.button ?? 'Send'}
|
||||||
|
</span>
|
||||||
|
<SendIcon className="send-icon flex xs:hidden" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1 +1,3 @@
|
|||||||
export * from './components/TypebotViewer'
|
export * from './components/TypebotViewer'
|
||||||
|
|
||||||
|
export * from 'util'
|
||||||
|
@ -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
|
|
@ -2,34 +2,53 @@ export type Step = StartStep | BubbleStep | InputStep
|
|||||||
|
|
||||||
export type BubbleStep = TextStep
|
export type BubbleStep = TextStep
|
||||||
|
|
||||||
export type InputStep = TextInputStep
|
export type InputStep = TextInputStep | NumberInputStep
|
||||||
|
|
||||||
export enum StepType {
|
export type StepType = 'start' | BubbleStepType | InputStepType
|
||||||
START = 'start',
|
|
||||||
|
export enum BubbleStepType {
|
||||||
TEXT = 'text',
|
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 StepBase = { id: string; blockId: string; target?: Target }
|
||||||
|
|
||||||
export type StartStep = StepBase & {
|
export type StartStep = StepBase & {
|
||||||
type: StepType.START
|
type: 'start'
|
||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TextStep = StepBase & {
|
export type TextStep = StepBase & {
|
||||||
type: StepType.TEXT
|
type: BubbleStepType.TEXT
|
||||||
content: { html: string; richText: unknown[]; plainText: string }
|
content: { html: string; richText: unknown[]; plainText: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TextInputStep = StepBase & {
|
export type TextInputStep = StepBase & {
|
||||||
type: StepType.TEXT_INPUT
|
type: InputStepType.TEXT
|
||||||
options?: TextInputOptions
|
options?: TextInputOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TextInputOptions = {
|
export type NumberInputStep = StepBase & {
|
||||||
|
type: InputStepType.NUMBER
|
||||||
|
options?: NumberInputOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
type InputOptionsBase = {
|
||||||
labels?: { placeholder?: string; button?: string }
|
labels?: { placeholder?: string; button?: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TextInputOptions = InputOptionsBase & {
|
||||||
isLong?: boolean
|
isLong?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NumberInputOptions = InputOptionsBase & {
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
step?: number
|
||||||
|
}
|
||||||
|
|
||||||
export type Target = { blockId: string; stepId?: string }
|
export type Target = { blockId: string; stepId?: string }
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import { Table } from 'models'
|
import {
|
||||||
|
BubbleStepType,
|
||||||
|
InputStep,
|
||||||
|
InputStepType,
|
||||||
|
Step,
|
||||||
|
Table,
|
||||||
|
TextInputStep,
|
||||||
|
TextStep,
|
||||||
|
} from 'models'
|
||||||
|
|
||||||
export const sendRequest = async <ResponseData>({
|
export const sendRequest = async <ResponseData>({
|
||||||
url,
|
url,
|
||||||
@ -32,3 +40,12 @@ export const filterTable = <T>(ids: string[], table: Table<T>): Table<T> => ({
|
|||||||
byId: ids.reduce((acc, id) => ({ ...acc, [id]: table.byId[id] }), {}),
|
byId: ids.reduce((acc, id) => ({ ...acc, [id]: table.byId[id] }), {}),
|
||||||
allIds: ids,
|
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
|
||||||
|
Reference in New Issue
Block a user