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

@@ -208,3 +208,12 @@ export const DownloadIcon = (props: IconProps) => (
<line x1="12" y1="15" x2="12" y2="3"></line>
</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>
)

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 (

View File

@@ -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<SetStateAction<StepType | undefined>>
draggedStep?: Step
setDraggedStep: Dispatch<SetStateAction<Step | undefined>>
draggedStepType?: DraggableStepType
setDraggedStepType: Dispatch<SetStateAction<DraggableStepType | undefined>>
draggedStep?: DraggableStep
setDraggedStep: Dispatch<SetStateAction<DraggableStep | undefined>>
}>({
setDraggedStep: () => console.log("I'm not implemented"),
setDraggedStepType: () => console.log("I'm not implemented"),
})
export const DndContext = ({ children }: { children: ReactNode }) => {
const [draggedStep, setDraggedStep] = useState<Step | undefined>()
const [draggedStepType, setDraggedStepType] = useState<StepType | undefined>()
const [draggedStep, setDraggedStep] = useState<DraggableStep | undefined>()
const [draggedStepType, setDraggedStepType] = useState<
DraggableStepType | undefined
>()
return (
<dndContext.Provider

View File

@@ -1,20 +1,25 @@
import { Coordinates } from 'contexts/GraphContext'
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 { Updater } from 'use-immer'
import { createStepDraft, deleteStepDraft } from './steps'
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
deleteBlock: (blockId: string) => void
}
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) => {
removeEmptyBlocks(typebot)
const newBlock = parseNewBlock({
totalBlocks: typebot.blocks.allIds.length,
initialCoordinates: { x, y },
@@ -22,6 +27,7 @@ export const blocksActions = (setTypebot: Updater<Typebot>): 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<Omit<Block, 'id'>>) =>

View File

@@ -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<Omit<Step, 'id' | 'type'>>
@@ -14,10 +18,14 @@ export type 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) => {
removeEmptyBlocks(typebot)
createStepDraft(typebot, step, blockId, index)
removeEmptyBlocks(typebot)
})
},
updateStep: (stepId: string, updates: Partial<Omit<Step, 'id' | 'type'>>) =>
@@ -28,6 +36,7 @@ export const stepsAction = (setTypebot: Updater<Typebot>): StepsActions => ({
setTypebot((typebot) => {
removeStepIdFromBlock(typebot, stepId)
deleteStepDraft(typebot, stepId)
removeEmptyBlocks(typebot)
})
},
})
@@ -56,7 +65,7 @@ export const deleteStepDraft = (
export const createStepDraft = (
typebot: WritableDraft<Typebot>,
step: StepType | Step,
step: BubbleStepType | InputStepType | Step,
blockId: string,
index?: number
) => {

View File

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

View File

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

View File

@@ -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: [] },
},
},

View File

@@ -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<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 = () => {
return cy
.get('#typebot-iframe')

View File

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

View File

@@ -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<TextStep, 'type' | 'content'> = {
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<TextInputStep, 'type'> = {
type,
}
return {
id,
blockId,
...textStep,
}
}
default: {
const textStep: Pick<TextStep, 'type' | 'content'> = {
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

View File

@@ -106,3 +106,12 @@ export const uploadFile = async (file: File, key: string) => {
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
)