2
0

feat(inputs): Add number input

This commit is contained in:
Baptiste Arnaud
2022-01-08 07:40:55 +01:00
parent 2a040308db
commit d54ebc0cbe
33 changed files with 467 additions and 207 deletions

View File

@ -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 && (
<>
<StepIcon type={type} />
<StepLabel type={type} />
<StepTypeLabel type={type} />
</>
)}
</Button>
@ -62,7 +65,7 @@ export const StepCardOverlay = ({
{...props}
>
<StepIcon type={type} />
<StepLabel type={type} />
<StepTypeLabel type={type} />
</Button>
)
}

View File

@ -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 <ChatIcon />
}
case StepType.TEXT: {
case InputStepType.TEXT: {
return <TextIcon />
}
case StepType.START: {
case InputStepType.NUMBER: {
return <NumberIcon />
}
case 'start': {
return <FlagIcon />
}
default: {
return <TextIcon />
return <></>
}
}
}

View File

@ -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 <></>
}
}
}

View File

@ -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 <></>
}
}
}

View File

@ -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
</Text>
<SimpleGrid columns={2} spacing="2">
{stepListItems.bubbles.map((props) => (
<StepCard key={props.type} onMouseDown={handleMouseDown} {...props} />
{Object.values(BubbleStepType).map((type) => (
<StepCard key={type} type={type} onMouseDown={handleMouseDown} />
))}
</SimpleGrid>
@ -86,8 +81,8 @@ export const StepTypesList = () => {
Inputs
</Text>
<SimpleGrid columns={2} spacing="2">
{stepListItems.inputs.map((props) => (
<StepCard key={props.type} onMouseDown={handleMouseDown} {...props} />
{Object.values(InputStepType).map((type) => (
<StepCard key={type} type={type} onMouseDown={handleMouseDown} />
))}
</SimpleGrid>
{draggedStepType && (

View File

@ -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>
)
}

View File

@ -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<Step>)
switch (step.type) {
case StepType.TEXT_INPUT: {
case InputStepType.TEXT: {
return (
<TextInputSettingsBody
options={step.options}
@ -33,6 +34,14 @@ const SettingsPopoverBodyContent = ({ step }: Props) => {
/>
)
}
case InputStepType.NUMBER: {
return (
<NumberInputSettingsBody
options={step.options}
onOptionsChange={handleOptionsChange}
/>
)
}
default: {
return <></>
}

View File

@ -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>
}
}
}

View File

@ -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<boolean>(
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) ? (
<TextEditor
stepId={step.id}
initialValue={step.content.richText}
@ -186,7 +186,7 @@ export const StepNode = ({
bgColor="white"
>
<StepIcon type={step.type} />
<StepContent {...step} />
<StepNodeLabel {...step} />
{isConnectable && (
<SourceEndpoint
onConnectionDragStart={handleConnectionDragStart}

View File

@ -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>
}
}
}

View File

@ -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}
>
<StepIcon type={step.type} />
<StepContent {...step} />
<StepNodeLabel {...step} />
</HStack>
)
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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 (