diff --git a/apps/builder/assets/icons.tsx b/apps/builder/assets/icons.tsx index 7f8be9d63..3039a1a83 100644 --- a/apps/builder/assets/icons.tsx +++ b/apps/builder/assets/icons.tsx @@ -208,3 +208,12 @@ export const DownloadIcon = (props: IconProps) => ( ) + +export const NumberIcon = (props: IconProps) => ( + + + + + + +) diff --git a/apps/builder/components/board/StepTypesList/StepCard.tsx b/apps/builder/components/board/StepTypesList/StepCard.tsx index 8d32cc8c2..9d27b5db2 100644 --- a/apps/builder/components/board/StepTypesList/StepCard.tsx +++ b/apps/builder/components/board/StepTypesList/StepCard.tsx @@ -1,16 +1,19 @@ import { Button, ButtonProps, Flex, HStack } from '@chakra-ui/react' -import { StepType } from 'models' +import { BubbleStepType, InputStepType, StepType } from 'models' import { useDnd } from 'contexts/DndContext' import React, { useEffect, useState } from 'react' import { StepIcon } from './StepIcon' -import { StepLabel } from './StepLabel' +import { StepTypeLabel } from './StepTypeLabel' export const StepCard = ({ type, onMouseDown, }: { - type: StepType - onMouseDown: (e: React.MouseEvent, type: StepType) => void + type: BubbleStepType | InputStepType + onMouseDown: ( + e: React.MouseEvent, + type: BubbleStepType | InputStepType + ) => void }) => { const { draggedStepType } = useDnd() const [isMouseDown, setIsMouseDown] = useState(false) @@ -35,7 +38,7 @@ export const StepCard = ({ {!isMouseDown && ( <> - + )} @@ -62,7 +65,7 @@ export const StepCardOverlay = ({ {...props} > - + ) } diff --git a/apps/builder/components/board/StepTypesList/StepIcon.tsx b/apps/builder/components/board/StepTypesList/StepIcon.tsx index 8ae8f3db0..80792505d 100644 --- a/apps/builder/components/board/StepTypesList/StepIcon.tsx +++ b/apps/builder/components/board/StepTypesList/StepIcon.tsx @@ -1,22 +1,25 @@ -import { ChatIcon, FlagIcon, TextIcon } from 'assets/icons' -import { StepType } from 'models' +import { ChatIcon, FlagIcon, NumberIcon, TextIcon } from 'assets/icons' +import { BubbleStepType, InputStepType, StepType } from 'models' import React from 'react' type StepIconProps = { type: StepType } export const StepIcon = ({ type }: StepIconProps) => { switch (type) { - case StepType.TEXT: { + case BubbleStepType.TEXT: { return } - case StepType.TEXT: { + case InputStepType.TEXT: { return } - case StepType.START: { + case InputStepType.NUMBER: { + return + } + case 'start': { return } default: { - return + return <> } } } diff --git a/apps/builder/components/board/StepTypesList/StepLabel.tsx b/apps/builder/components/board/StepTypesList/StepLabel.tsx deleted file mode 100644 index 4be13c678..000000000 --- a/apps/builder/components/board/StepTypesList/StepLabel.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Text } from '@chakra-ui/react' -import { StepType } from 'models' -import React from 'react' - -type Props = { type: StepType } - -export const StepLabel = ({ type }: Props) => { - switch (type) { - case StepType.TEXT: { - return Text - } - case StepType.TEXT_INPUT: { - return Text - } - default: { - return <> - } - } -} diff --git a/apps/builder/components/board/StepTypesList/StepTypeLabel.tsx b/apps/builder/components/board/StepTypesList/StepTypeLabel.tsx new file mode 100644 index 000000000..231fa7d74 --- /dev/null +++ b/apps/builder/components/board/StepTypesList/StepTypeLabel.tsx @@ -0,0 +1,20 @@ +import { Text } from '@chakra-ui/react' +import { BubbleStepType, InputStepType, StepType } from 'models' +import React from 'react' + +type Props = { type: StepType } + +export const StepTypeLabel = ({ type }: Props) => { + switch (type) { + case BubbleStepType.TEXT: + case InputStepType.TEXT: { + return Text + } + case InputStepType.NUMBER: { + return Number + } + default: { + return <> + } + } +} diff --git a/apps/builder/components/board/StepTypesList/StepTypesList.tsx b/apps/builder/components/board/StepTypesList/StepTypesList.tsx index cdf654483..5b79e734d 100644 --- a/apps/builder/components/board/StepTypesList/StepTypesList.tsx +++ b/apps/builder/components/board/StepTypesList/StepTypesList.tsx @@ -5,19 +5,11 @@ import { SimpleGrid, useEventListener, } from '@chakra-ui/react' -import { StepType } from 'models' +import { BubbleStepType, InputStepType } from 'models' import { useDnd } from 'contexts/DndContext' import React, { useState } from 'react' import { StepCard, StepCardOverlay } from './StepCard' -export const stepListItems: { - bubbles: { type: StepType }[] - inputs: { type: StepType }[] -} = { - bubbles: [{ type: StepType.TEXT }], - inputs: [{ type: StepType.TEXT_INPUT }], -} - export const StepTypesList = () => { const { setDraggedStepType, draggedStepType } = useDnd() const [position, setPosition] = useState({ @@ -37,7 +29,10 @@ export const StepTypesList = () => { } useEventListener('mousemove', handleMouseMove) - const handleMouseDown = (e: React.MouseEvent, type: StepType) => { + const handleMouseDown = ( + e: React.MouseEvent, + type: BubbleStepType | InputStepType + ) => { const element = e.currentTarget as HTMLDivElement const rect = element.getBoundingClientRect() const relativeX = e.clientX - rect.left @@ -77,8 +72,8 @@ export const StepTypesList = () => { Bubbles - {stepListItems.bubbles.map((props) => ( - + {Object.values(BubbleStepType).map((type) => ( + ))} @@ -86,8 +81,8 @@ export const StepTypesList = () => { Inputs - {stepListItems.inputs.map((props) => ( - + {Object.values(InputStepType).map((type) => ( + ))} {draggedStepType && ( diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/NumberInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/NumberInputSettingsBody.tsx new file mode 100644 index 000000000..7e022f3a1 --- /dev/null +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/NumberInputSettingsBody.tsx @@ -0,0 +1,84 @@ +import { FormLabel, HStack, Stack } from '@chakra-ui/react' +import { SmartNumberInput } from 'components/settings/SmartNumberInput' +import { DebouncedInput } from 'components/shared/DebouncedInput' +import { NumberInputOptions } from 'models' +import React from 'react' +import { removeUndefinedFields } from 'services/utils' + +type NumberInputSettingsBodyProps = { + options?: NumberInputOptions + onOptionsChange: (options: NumberInputOptions) => void +} + +export const NumberInputSettingsBody = ({ + options, + onOptionsChange, +}: NumberInputSettingsBodyProps) => { + const handlePlaceholderChange = (placeholder: string) => + onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } }) + const handleButtonLabelChange = (button: string) => + onOptionsChange({ ...options, labels: { ...options?.labels, button } }) + const handleMinChange = (min?: number) => + onOptionsChange(removeUndefinedFields({ ...options, min })) + const handleMaxChange = (max?: number) => + onOptionsChange(removeUndefinedFields({ ...options, max })) + const handleStepChange = (step?: number) => + onOptionsChange(removeUndefinedFields({ ...options, step })) + + return ( + + + + Placeholder: + + + + + + Button label: + + + + + + Min: + + + + + + Max: + + + + + + Step: + + + + + ) +} diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/SettingsPopoverContent.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/SettingsPopoverContent.tsx index 4aeb27027..2c2915257 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/SettingsPopoverContent.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/SettingsPopoverContent.tsx @@ -1,6 +1,7 @@ import { PopoverContent, PopoverArrow, PopoverBody } from '@chakra-ui/react' import { useTypebot } from 'contexts/TypebotContext/TypebotContext' -import { Step, StepType, TextInputOptions } from 'models' +import { InputStepType, Step, TextInputOptions } from 'models' +import { NumberInputSettingsBody } from './NumberInputSettingsBody' import { TextInputSettingsBody } from './TextInputSettingsBody' type Props = { @@ -25,7 +26,7 @@ const SettingsPopoverBodyContent = ({ step }: Props) => { updateStep(step.id, { options } as Partial) switch (step.type) { - case StepType.TEXT_INPUT: { + case InputStepType.TEXT: { return ( { /> ) } + case InputStepType.NUMBER: { + return ( + + ) + } default: { return <> } diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepContent.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepContent.tsx deleted file mode 100644 index e09e90f9f..000000000 --- a/apps/builder/components/board/graph/BlockNode/StepNode/StepContent.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Flex, Text } from '@chakra-ui/react' -import { Step, StartStep, StepType } from 'models' - -export const StepContent = (props: Step | StartStep) => { - switch (props.type) { - case StepType.TEXT: { - return ( - Click to edit...

` - : props.content.html, - }} - >
- ) - } - case StepType.TEXT_INPUT: { - return Type your answer... - } - case StepType.START: { - return {props.label} - } - default: { - return No input - } - } -} diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx index c6b2fff7e..5ed6f64b4 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx @@ -11,15 +11,15 @@ import { Block, Step } from 'models' import { SourceEndpoint } from './SourceEndpoint' import { useGraph } from 'contexts/GraphContext' import { StepIcon } from 'components/board/StepTypesList/StepIcon' -import { isDefined } from 'utils' +import { isDefined, isTextBubbleStep } from 'utils' import { Coordinates } from '@dnd-kit/core/dist/types' import { TextEditor } from './TextEditor/TextEditor' -import { StepContent } from './StepContent' +import { StepNodeLabel } from './StepNodeLabel' import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { ContextMenu } from 'components/shared/ContextMenu' import { StepNodeContextMenu } from './RightClickMenu' import { SettingsPopoverContent } from './SettingsPopoverContent' -import { isStepText } from 'services/typebots' +import { DraggableStep } from 'contexts/DndContext' export const StepNode = ({ step, @@ -34,7 +34,7 @@ export const StepNode = ({ onMouseMoveTopOfElement?: () => void onMouseDown?: ( stepNodePosition: { absolute: Coordinates; relative: Coordinates }, - step: Step + step: DraggableStep ) => void }) => { const { setConnectingIds, connectingIds } = useGraph() @@ -43,7 +43,7 @@ export const StepNode = ({ const [mouseDownEvent, setMouseDownEvent] = useState<{ absolute: Coordinates; relative: Coordinates }>() const [isEditing, setIsEditing] = useState( - isStepText(step) && step.content.plainText === '' + isTextBubbleStep(step) && step.content.plainText === '' ) useEffect(() => { @@ -102,8 +102,8 @@ export const StepNode = ({ mouseDownEvent && onMouseDown && (event.movementX > 0 || event.movementY > 0) - if (isMovingAndIsMouseDown) { - onMouseDown(mouseDownEvent, step as Step) + if (isMovingAndIsMouseDown && step.type !== 'start') { + onMouseDown(mouseDownEvent, step) deleteStep(step.id) setMouseDownEvent(undefined) } @@ -142,7 +142,7 @@ export const StepNode = ({ connectingIds?.target?.blockId, ]) - return isEditing && isStepText(step) ? ( + return isEditing && isTextBubbleStep(step) ? ( - + {isConnectable && ( { + switch (props.type) { + case BubbleStepType.TEXT: { + return ( + Click to edit...

` + : props.content.html, + }} + /> + ) + } + case InputStepType.TEXT: { + return ( + + {props.options?.labels?.placeholder ?? 'Type your answer...'} + + ) + } + case InputStepType.NUMBER: { + return ( + + {props.options?.labels?.placeholder ?? 'Type your answer...'} + + ) + } + case 'start': { + return {props.label} + } + default: { + return No input + } + } +} diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeOverlay.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeOverlay.tsx index 31a31cd9f..b17e2d697 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeOverlay.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeOverlay.tsx @@ -1,7 +1,7 @@ import { StackProps, HStack } from '@chakra-ui/react' import { StartStep, Step } from 'models' import { StepIcon } from 'components/board/StepTypesList/StepIcon' -import { StepContent } from './StepContent' +import { StepNodeLabel } from './StepNodeLabel' export const StepNodeOverlay = ({ step, @@ -19,7 +19,7 @@ export const StepNodeOverlay = ({ {...props} > - + ) } diff --git a/apps/builder/components/board/graph/BlockNode/StepsList.tsx b/apps/builder/components/board/graph/BlockNode/StepsList.tsx index f791a78ce..1530a523a 100644 --- a/apps/builder/components/board/graph/BlockNode/StepsList.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepsList.tsx @@ -1,6 +1,6 @@ import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react' import { Step, Table } from 'models' -import { useDnd } from 'contexts/DndContext' +import { DraggableStep, useDnd } from 'contexts/DndContext' import { Coordinates } from 'contexts/GraphContext' import { useState } from 'react' import { StepNode, StepNodeOverlay } from './StepNode' @@ -54,7 +54,7 @@ export const StepsList = ({ const handleStepMouseDown = ( { absolute, relative }: { absolute: Coordinates; relative: Coordinates }, - step: Step + step: DraggableStep ) => { setPosition(absolute) setRelativeCoordinates(relative) diff --git a/apps/builder/components/board/graph/Graph.tsx b/apps/builder/components/board/graph/Graph.tsx index bf79097e4..60f56a138 100644 --- a/apps/builder/components/board/graph/Graph.tsx +++ b/apps/builder/components/board/graph/Graph.tsx @@ -2,11 +2,10 @@ import { Flex, FlexProps, useEventListener } from '@chakra-ui/react' import React, { useRef, useMemo } from 'react' import { blockWidth, useGraph } from 'contexts/GraphContext' import { BlockNode } from './BlockNode/BlockNode' -import { useDnd } from 'contexts/DndContext' +import { DraggableStepType, useDnd } from 'contexts/DndContext' import { Edges } from './Edges' import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader' -import { StepType } from 'models' const Graph = ({ ...props }: FlexProps) => { const { draggedStepType, setDraggedStepType, draggedStep, setDraggedStep } = @@ -44,7 +43,7 @@ const Graph = ({ ...props }: FlexProps) => { createBlock({ x: e.clientX - graphPosition.x - blockWidth / 3, y: e.clientY - graphPosition.y - 20 - headerHeight, - step: draggedStep ?? (draggedStepType as StepType), + step: draggedStep ?? (draggedStepType as DraggableStepType), }) setDraggedStep(undefined) setDraggedStepType(undefined) diff --git a/apps/builder/components/settings/SmartNumberInput.tsx b/apps/builder/components/settings/SmartNumberInput.tsx index 073ac4739..367c6f4ba 100644 --- a/apps/builder/components/settings/SmartNumberInput.tsx +++ b/apps/builder/components/settings/SmartNumberInput.tsx @@ -13,14 +13,14 @@ export const SmartNumberInput = ({ onValueChange, ...props }: { - initialValue: number - onValueChange: (value: number) => void + initialValue?: number + onValueChange: (value?: number) => void } & NumberInputProps) => { - const [value, setValue] = useState(initialValue.toString()) + const [value, setValue] = useState(initialValue?.toString() ?? '') useEffect(() => { if (value.endsWith('.') || value.endsWith(',')) return - if (value === '') onValueChange(0) + if (value === '') onValueChange(undefined) const newValue = parseFloat(value) if (isNaN(newValue)) return onValueChange(newValue) diff --git a/apps/builder/components/settings/TypingEmulation.tsx b/apps/builder/components/settings/TypingEmulation.tsx index 41a6c35f0..a949ab84d 100644 --- a/apps/builder/components/settings/TypingEmulation.tsx +++ b/apps/builder/components/settings/TypingEmulation.tsx @@ -17,14 +17,14 @@ export const TypingEmulation = ({ onUpdate({ ...typingEmulation, enabled: !typingEmulation.enabled }) } - const handleSpeedChange = (speed: number) => { + const handleSpeedChange = (speed?: number) => { if (!typingEmulation) return - onUpdate({ ...typingEmulation, speed }) + onUpdate({ ...typingEmulation, speed: speed ?? 0 }) } - const handleMaxDelayChange = (maxDelay: number) => { + const handleMaxDelayChange = (maxDelay?: number) => { if (!typingEmulation) return - onUpdate({ ...typingEmulation, maxDelay: maxDelay }) + onUpdate({ ...typingEmulation, maxDelay: maxDelay ?? 0 }) } return ( diff --git a/apps/builder/contexts/DndContext.tsx b/apps/builder/contexts/DndContext.tsx index 7a324b689..32c6a2275 100644 --- a/apps/builder/contexts/DndContext.tsx +++ b/apps/builder/contexts/DndContext.tsx @@ -1,4 +1,4 @@ -import { Step, StepType } from 'models' +import { BubbleStep, BubbleStepType, InputStep, InputStepType } from 'models' import { createContext, Dispatch, @@ -8,19 +8,24 @@ import { useState, } from 'react' +export type DraggableStep = BubbleStep | InputStep +export type DraggableStepType = BubbleStepType | InputStepType + const dndContext = createContext<{ - draggedStepType?: StepType - setDraggedStepType: Dispatch> - draggedStep?: Step - setDraggedStep: Dispatch> + draggedStepType?: DraggableStepType + setDraggedStepType: Dispatch> + draggedStep?: DraggableStep + setDraggedStep: Dispatch> }>({ setDraggedStep: () => console.log("I'm not implemented"), setDraggedStepType: () => console.log("I'm not implemented"), }) export const DndContext = ({ children }: { children: ReactNode }) => { - const [draggedStep, setDraggedStep] = useState() - const [draggedStepType, setDraggedStepType] = useState() + const [draggedStep, setDraggedStep] = useState() + const [draggedStepType, setDraggedStepType] = useState< + DraggableStepType | undefined + >() return ( void + createBlock: ( + props: Coordinates & { step: BubbleStepType | InputStepType | Step } + ) => void updateBlock: (blockId: string, updates: Partial>) => void deleteBlock: (blockId: string) => void } export const blocksActions = (setTypebot: Updater): BlocksActions => ({ - createBlock: ({ x, y, step }: Coordinates & { step: StepType | Step }) => { + createBlock: ({ + x, + y, + step, + }: Coordinates & { step: BubbleStepType | InputStepType | Step }) => { setTypebot((typebot) => { - removeEmptyBlocks(typebot) const newBlock = parseNewBlock({ totalBlocks: typebot.blocks.allIds.length, initialCoordinates: { x, y }, @@ -22,6 +27,7 @@ export const blocksActions = (setTypebot: Updater): BlocksActions => ({ typebot.blocks.byId[newBlock.id] = newBlock typebot.blocks.allIds.push(newBlock.id) createStepDraft(typebot, step, newBlock.id) + removeEmptyBlocks(typebot) }) }, updateBlock: (blockId: string, updates: Partial>) => diff --git a/apps/builder/contexts/TypebotContext/actions/steps.ts b/apps/builder/contexts/TypebotContext/actions/steps.ts index e721424df..d8f89b317 100644 --- a/apps/builder/contexts/TypebotContext/actions/steps.ts +++ b/apps/builder/contexts/TypebotContext/actions/steps.ts @@ -1,11 +1,15 @@ -import { Step, StepType, Typebot } from 'models' +import { BubbleStepType, InputStepType, Step, Typebot } from 'models' import { parseNewStep } from 'services/typebots' import { Updater } from 'use-immer' import { removeEmptyBlocks } from './blocks' import { WritableDraft } from 'immer/dist/types/types-external' export type StepsActions = { - createStep: (blockId: string, step: StepType | Step, index?: number) => void + createStep: ( + blockId: string, + step: BubbleStepType | InputStepType | Step, + index?: number + ) => void updateStep: ( stepId: string, updates: Partial> @@ -14,10 +18,14 @@ export type StepsActions = { } export const stepsAction = (setTypebot: Updater): StepsActions => ({ - createStep: (blockId: string, step: StepType | Step, index?: number) => { + createStep: ( + blockId: string, + step: BubbleStepType | InputStepType | Step, + index?: number + ) => { setTypebot((typebot) => { - removeEmptyBlocks(typebot) createStepDraft(typebot, step, blockId, index) + removeEmptyBlocks(typebot) }) }, updateStep: (stepId: string, updates: Partial>) => @@ -28,6 +36,7 @@ export const stepsAction = (setTypebot: Updater): StepsActions => ({ setTypebot((typebot) => { removeStepIdFromBlock(typebot, stepId) deleteStepDraft(typebot, stepId) + removeEmptyBlocks(typebot) }) }, }) @@ -56,7 +65,7 @@ export const deleteStepDraft = ( export const createStepDraft = ( typebot: WritableDraft, - step: StepType | Step, + step: BubbleStepType | InputStepType | Step, blockId: string, index?: number ) => { diff --git a/apps/builder/cypress/plugins/database.ts b/apps/builder/cypress/plugins/database.ts index 3aedaabcd..6629f1003 100644 --- a/apps/builder/cypress/plugins/database.ts +++ b/apps/builder/cypress/plugins/database.ts @@ -1,4 +1,4 @@ -import { PublicTypebot, StepType, Typebot } from 'models' +import { InputStepType, PublicTypebot, Typebot } from 'models' import { Plan, PrismaClient } from 'db' import { parseTestTypebot } from './utils' @@ -58,7 +58,7 @@ const createTypebots = async () => { byId: { step1: { id: 'step1', - type: StepType.TEXT_INPUT, + type: InputStepType.TEXT, blockId: 'block1', }, }, diff --git a/apps/builder/cypress/plugins/utils.ts b/apps/builder/cypress/plugins/utils.ts index 303b9412f..162919a7d 100644 --- a/apps/builder/cypress/plugins/utils.ts +++ b/apps/builder/cypress/plugins/utils.ts @@ -1,6 +1,5 @@ import { Block, - StepType, Theme, BackgroundType, Settings, @@ -59,7 +58,7 @@ export const parseTestTypebot = ({ byId: { step0: { id: 'step0', - type: StepType.START, + type: 'start', blockId: 'block0', label: 'Start', target: { blockId: 'block1' }, diff --git a/apps/builder/cypress/tests/bubbles.ts b/apps/builder/cypress/tests/bubbles.ts index f25ff5a26..490e9e34b 100644 --- a/apps/builder/cypress/tests/bubbles.ts +++ b/apps/builder/cypress/tests/bubbles.ts @@ -1,5 +1,5 @@ import { parseTestTypebot } from 'cypress/plugins/utils' -import { StepType } from 'models' +import { BubbleStepType } from 'models' describe('Text bubbles', () => { beforeEach(() => { @@ -15,7 +15,7 @@ describe('Text bubbles', () => { step1: { id: 'step1', blockId: 'block1', - type: StepType.TEXT, + type: BubbleStepType.TEXT, content: { html: '', plainText: '', richText: [] }, }, }, diff --git a/apps/builder/cypress/tests/inputs.ts b/apps/builder/cypress/tests/inputs.ts index c015fc10a..7fb78250a 100644 --- a/apps/builder/cypress/tests/inputs.ts +++ b/apps/builder/cypress/tests/inputs.ts @@ -1,42 +1,14 @@ import { parseTestTypebot } from 'cypress/plugins/utils' -import { StepType } from 'models' +import { InputStep, InputStepType } from 'models' describe('Text input', () => { beforeEach(() => { cy.task('seed') - cy.task( - 'createTypebot', - parseTestTypebot({ - id: 'typebot3', - name: 'Typebot #3', - ownerId: 'test2', - steps: { - byId: { - step1: { - id: 'step1', - blockId: 'block1', - type: StepType.TEXT_INPUT, - }, - }, - allIds: ['step1'], - }, - blocks: { - byId: { - block1: { - id: 'block1', - graphCoordinates: { x: 400, y: 200 }, - title: 'Block #1', - stepIds: ['step1'], - }, - }, - allIds: ['block1'], - }, - }) - ) + createTypebotWithStep({ type: InputStepType.TEXT }) cy.signOut() }) - it('text input options should work', () => { + it('options should work', () => { cy.signIn('test2@gmail.com') cy.visit('/typebots/typebot3/edit') cy.findByRole('button', { name: 'Preview' }).click() @@ -48,6 +20,7 @@ describe('Text input', () => { .type('Your name...') cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go') cy.findByRole('button', { name: 'Restart' }).click() + cy.findByTestId('step-step1').should('contain.text', 'Your name...') getIframeBody().findByPlaceholderText('Your name...').should('exist') getIframeBody().findByRole('button', { name: 'Go' }) cy.findByTestId('step-step1').click({ force: true }) @@ -57,6 +30,68 @@ describe('Text input', () => { }) }) +describe('Number input', () => { + beforeEach(() => { + cy.task('seed') + createTypebotWithStep({ type: InputStepType.NUMBER }) + cy.signOut() + }) + + it.only('options should work', () => { + cy.signIn('test2@gmail.com') + cy.visit('/typebots/typebot3/edit') + cy.findByRole('button', { name: 'Preview' }).click() + getIframeBody().findByPlaceholderText('Type your answer...').should('exist') + getIframeBody().findByRole('button', { name: 'Send' }) + cy.findByTestId('step-step1').click({ force: true }) + cy.findByRole('textbox', { name: 'Placeholder:' }) + .clear() + .type('Your name...') + cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go') + cy.findByRole('spinbutton', { name: 'Min:' }).type('0') + cy.findByRole('spinbutton', { name: 'Max:' }).type('100') + cy.findByRole('spinbutton', { name: 'Step:' }).type('10') + cy.findByRole('button', { name: 'Restart' }).click() + cy.findByTestId('step-step1').should('contain.text', 'Your name...') + getIframeBody() + .findByPlaceholderText('Your name...') + .should('exist') + .type('-1{enter}') + .clear() + .type('150{enter}') + getIframeBody().findByRole('button', { name: 'Go' }) + cy.findByTestId('step-step1').click({ force: true }) + }) +}) + +const createTypebotWithStep = (step: Omit) => { + cy.task( + 'createTypebot', + parseTestTypebot({ + id: 'typebot3', + name: 'Typebot #3', + ownerId: 'test2', + steps: { + byId: { + step1: { ...step, id: 'step1', blockId: 'block1' }, + }, + allIds: ['step1'], + }, + blocks: { + byId: { + block1: { + id: 'block1', + graphCoordinates: { x: 400, y: 200 }, + title: 'Block #1', + stepIds: ['step1'], + }, + }, + allIds: ['block1'], + }, + }) + ) +} + const getIframeBody = () => { return cy .get('#typebot-iframe') diff --git a/apps/builder/services/publicTypebot.tsx b/apps/builder/services/publicTypebot.tsx index 55a8aeb96..2afb8597e 100644 --- a/apps/builder/services/publicTypebot.tsx +++ b/apps/builder/services/publicTypebot.tsx @@ -1,9 +1,10 @@ -import { InputStep, PublicTypebot, Step, StepType, Typebot } from 'models' +import { PublicTypebot, Typebot } from 'models' import { sendRequest } from './utils' import shortId from 'short-uuid' import { HStack, Text } from '@chakra-ui/react' import { CalendarIcon } from 'assets/icons' import { StepIcon } from 'components/board/StepTypesList/StepIcon' +import { isInputStep } from 'utils' export const parseTypebotToPublicTypebot = ( typebot: Typebot @@ -59,7 +60,7 @@ export const parseSubmissionsColumns = ( .map((blockId) => { const block = typebot.blocks.byId[blockId] const inputStepId = block.stepIds.find((stepId) => - stepIsInput(typebot.steps.byId[stepId]) + isInputStep(typebot.steps.byId[stepId]) ) const inputStep = typebot.steps.byId[inputStepId as string] return { @@ -80,8 +81,5 @@ const blockContainsInput = ( blockId: string ) => typebot.blocks.byId[blockId].stepIds.some((stepId) => - stepIsInput(typebot.steps.byId[stepId]) + isInputStep(typebot.steps.byId[stepId]) ) - -export const stepIsInput = (step: Step): step is InputStep => - step.type === StepType.TEXT_INPUT diff --git a/apps/builder/services/typebots.ts b/apps/builder/services/typebots.ts index 12033a450..dd27fa363 100644 --- a/apps/builder/services/typebots.ts +++ b/apps/builder/services/typebots.ts @@ -1,14 +1,15 @@ import { - Step, - StepType, Block, TextStep, - TextInputStep, PublicTypebot, BackgroundType, Settings, StartStep, Theme, + BubbleStep, + InputStep, + BubbleStepType, + InputStepType, } from 'models' import shortId from 'short-uuid' import { Typebot } from 'models' @@ -104,10 +105,13 @@ export const parseNewBlock = ({ } } -export const parseNewStep = (type: StepType, blockId: string): Step => { +export const parseNewStep = ( + type: BubbleStepType | InputStepType, + blockId: string +): BubbleStep | InputStep => { const id = `s${shortId.generate()}` switch (type) { - case StepType.TEXT: { + case BubbleStepType.TEXT: { const textStep: Pick = { type, content: { html: '', richText: [], plainText: '' }, @@ -118,22 +122,12 @@ export const parseNewStep = (type: StepType, blockId: string): Step => { ...textStep, } } - case StepType.TEXT_INPUT: { - const textStep: Pick = { - type, - } - return { - id, - blockId, - ...textStep, - } - } default: { - const textStep: Pick = { - type: StepType.TEXT, - content: { html: '', richText: [], plainText: '' }, + return { + id, + blockId, + type, } - return { blockId, id, ...textStep } } } } @@ -180,7 +174,7 @@ export const parseNewTypebot = ({ blockId: startBlockId, id: startStepId, label: 'Start', - type: StepType.START, + type: 'start', } const startBlock: Block = { id: startBlockId, @@ -211,6 +205,3 @@ export const parseNewTypebot = ({ settings, } } - -export const isStepText = (step: Step): step is TextStep => - step.type === StepType.TEXT diff --git a/apps/builder/services/utils.ts b/apps/builder/services/utils.ts index 7485a80c7..8062733c7 100644 --- a/apps/builder/services/utils.ts +++ b/apps/builder/services/utils.ts @@ -106,3 +106,12 @@ export const uploadFile = async (file: File, key: string) => { url: upload.ok ? `${url}/${key}` : null, } } + +export const removeUndefinedFields = (obj: T): T => + Object.keys(obj).reduce( + (acc, key) => + obj[key as keyof T] === undefined + ? { ...acc } + : { ...acc, [key]: obj[key as keyof T] }, + {} as T + ) diff --git a/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx b/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx index 28c14769b..82fb3fcb5 100644 --- a/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx +++ b/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx @@ -1,11 +1,12 @@ import React, { useEffect, useState } from 'react' import { useAnswers } from '../../../contexts/AnswersContext' import { useHostAvatars } from '../../../contexts/HostAvatarsContext' -import { InputStep, Step } from 'models' -import { isTextInputStep, isTextStep } from '../../../services/utils' +import { InputStep, InputStepType, Step } from 'models' import { GuestBubble } from './bubbles/GuestBubble' import { HostMessageBubble } from './bubbles/HostMessageBubble' import { TextInput } from './inputs/TextInput' +import { isInputStep, isTextBubbleStep, isTextInputStep } from 'utils' +import { NumberInput } from './inputs/NumberInput' export const ChatStep = ({ step, @@ -21,9 +22,9 @@ export const ChatStep = ({ onTransitionEnd() } - if (isTextStep(step)) + if (isTextBubbleStep(step)) return - if (isTextInputStep(step)) + if (isInputStep(step)) return return No step } @@ -50,5 +51,10 @@ const InputChatStep = ({ if (answer) { return } - return + switch (step.type) { + case InputStepType.TEXT: + return + case InputStepType.NUMBER: + return + } } diff --git a/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/HostMessageBubble.tsx b/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/HostMessageBubble.tsx index cc2720010..fad80bbd7 100644 --- a/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/HostMessageBubble.tsx +++ b/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/HostMessageBubble.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState } from 'react' import { useHostAvatars } from '../../../../contexts/HostAvatarsContext' import { useTypebot } from '../../../../contexts/TypebotContext' -import { StepType, TextStep } from 'models' +import { BubbleStepType, StepType, TextStep } from 'models' import { computeTypingTimeout } from '../../../../services/chat' import { TypingContent } from './TypingContent' @@ -62,7 +62,7 @@ export const HostMessageBubble = ({ > {isTyping ? : <>} - {step.type === StepType.TEXT && ( + {step.type === BubbleStepType.TEXT && (

void +} + +export const NumberInput = ({ step, onSubmit }: NumberInputProps) => { + const inputRef = useRef(null) + const [inputValue, setInputValue] = useState('') + + const handleSubmit = (e: FormEvent) => { + e.preventDefault() + if (inputValue === '') return + onSubmit(inputValue) + } + + return ( +

+
+
+ setInputValue(e.target.value)} + style={{ appearance: 'auto' }} + min={step.options?.min} + max={step.options?.max} + step={step.options?.step} + required + /> + +
+
+
+ ) +} diff --git a/packages/bot-engine/src/index.ts b/packages/bot-engine/src/index.ts index 482a6ab8c..c089e42a3 100644 --- a/packages/bot-engine/src/index.ts +++ b/packages/bot-engine/src/index.ts @@ -1 +1,3 @@ export * from './components/TypebotViewer' + +export * from 'util' diff --git a/packages/bot-engine/src/services/utils.ts b/packages/bot-engine/src/services/utils.ts deleted file mode 100644 index 7e6c41bf8..000000000 --- a/packages/bot-engine/src/services/utils.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Step, TextStep, StepType, TextInputStep } from 'models' - -export const isTextStep = (step: Step): step is TextStep => - step.type === StepType.TEXT - -export const isTextInputStep = (step: Step): step is TextInputStep => - step.type === StepType.TEXT_INPUT diff --git a/packages/models/src/typebot/steps.ts b/packages/models/src/typebot/steps.ts index cfe644956..98f1ddf32 100644 --- a/packages/models/src/typebot/steps.ts +++ b/packages/models/src/typebot/steps.ts @@ -2,34 +2,53 @@ export type Step = StartStep | BubbleStep | InputStep export type BubbleStep = TextStep -export type InputStep = TextInputStep +export type InputStep = TextInputStep | NumberInputStep -export enum StepType { - START = 'start', +export type StepType = 'start' | BubbleStepType | InputStepType + +export enum BubbleStepType { TEXT = 'text', - TEXT_INPUT = 'text input', +} + +export enum InputStepType { + TEXT = 'text input', + NUMBER = 'number input', } export type StepBase = { id: string; blockId: string; target?: Target } export type StartStep = StepBase & { - type: StepType.START + type: 'start' label: string } export type TextStep = StepBase & { - type: StepType.TEXT + type: BubbleStepType.TEXT content: { html: string; richText: unknown[]; plainText: string } } export type TextInputStep = StepBase & { - type: StepType.TEXT_INPUT + type: InputStepType.TEXT options?: TextInputOptions } -export type TextInputOptions = { +export type NumberInputStep = StepBase & { + type: InputStepType.NUMBER + options?: NumberInputOptions +} + +type InputOptionsBase = { labels?: { placeholder?: string; button?: string } +} + +export type TextInputOptions = InputOptionsBase & { isLong?: boolean } +export type NumberInputOptions = InputOptionsBase & { + min?: number + max?: number + step?: number +} + export type Target = { blockId: string; stepId?: string } diff --git a/packages/utils/src/utils.ts b/packages/utils/src/utils.ts index 1f1571895..efeb4f612 100644 --- a/packages/utils/src/utils.ts +++ b/packages/utils/src/utils.ts @@ -1,4 +1,12 @@ -import { Table } from 'models' +import { + BubbleStepType, + InputStep, + InputStepType, + Step, + Table, + TextInputStep, + TextStep, +} from 'models' export const sendRequest = async ({ url, @@ -32,3 +40,12 @@ export const filterTable = (ids: string[], table: Table): Table => ({ byId: ids.reduce((acc, id) => ({ ...acc, [id]: table.byId[id] }), {}), allIds: ids, }) + +export const isInputStep = (step: Step): step is InputStep => + (Object.values(InputStepType) as string[]).includes(step.type) + +export const isTextBubbleStep = (step: Step): step is TextStep => + step.type === BubbleStepType.TEXT + +export const isTextInputStep = (step: Step): step is TextInputStep => + step.type === InputStepType.TEXT