feat(inputs): ✨ Add text options
This commit is contained in:
@ -15,8 +15,8 @@ export const Board = () => {
|
|||||||
<StepTypesList />
|
<StepTypesList />
|
||||||
<GraphProvider>
|
<GraphProvider>
|
||||||
<Graph flex="1" />
|
<Graph flex="1" />
|
||||||
|
{rightPanel === RightPanel.PREVIEW && <PreviewDrawer />}
|
||||||
</GraphProvider>
|
</GraphProvider>
|
||||||
{rightPanel === RightPanel.PREVIEW && <PreviewDrawer />}
|
|
||||||
</DndContext>
|
</DndContext>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
import { PopoverContent, PopoverArrow, PopoverBody } from '@chakra-ui/react'
|
||||||
|
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||||
|
import { Step, StepType, TextInputOptions } from 'models'
|
||||||
|
import { TextInputSettingsBody } from './TextInputSettingsBody'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
step: Step
|
||||||
|
}
|
||||||
|
export const SettingsPopoverContent = ({ step }: Props) => {
|
||||||
|
const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopoverContent onMouseDown={handleMouseDown}>
|
||||||
|
<PopoverArrow />
|
||||||
|
<PopoverBody p="6">
|
||||||
|
<SettingsPopoverBodyContent step={step} />
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingsPopoverBodyContent = ({ step }: Props) => {
|
||||||
|
const { updateStep } = useTypebot()
|
||||||
|
const handleOptionsChange = (options: TextInputOptions) =>
|
||||||
|
updateStep(step.id, { options } as Partial<Step>)
|
||||||
|
|
||||||
|
switch (step.type) {
|
||||||
|
case StepType.TEXT_INPUT: {
|
||||||
|
return (
|
||||||
|
<TextInputSettingsBody
|
||||||
|
options={step.options}
|
||||||
|
onOptionsChange={handleOptionsChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||||
|
import { DebouncedInput } from 'components/shared/DebouncedInput'
|
||||||
|
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||||
|
import { TextInputOptions } from 'models'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
type TextInputSettingsBodyProps = {
|
||||||
|
options?: TextInputOptions
|
||||||
|
onOptionsChange: (options: TextInputOptions) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextInputSettingsBody = ({
|
||||||
|
options,
|
||||||
|
onOptionsChange,
|
||||||
|
}: TextInputSettingsBodyProps) => {
|
||||||
|
const handlePlaceholderChange = (placeholder: string) =>
|
||||||
|
onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } })
|
||||||
|
const handleButtonLabelChange = (button: string) =>
|
||||||
|
onOptionsChange({ ...options, labels: { ...options?.labels, button } })
|
||||||
|
const handleLongChange = (isLong: boolean) =>
|
||||||
|
onOptionsChange({ ...options, isLong })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<SwitchWithLabel
|
||||||
|
id="switch"
|
||||||
|
label="Long text?"
|
||||||
|
initialValue={options?.isLong ?? false}
|
||||||
|
onCheckChange={handleLongChange}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { SettingsPopoverContent } from './SettingsPopoverContent'
|
@ -1,4 +1,11 @@
|
|||||||
import { Box, Flex, HStack, useEventListener } from '@chakra-ui/react'
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
HStack,
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
useEventListener,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
import React, { useEffect, useMemo, useState } from 'react'
|
import React, { useEffect, useMemo, useState } from 'react'
|
||||||
import { Block, Step, StepType } from 'models'
|
import { Block, Step, StepType } from 'models'
|
||||||
import { SourceEndpoint } from './SourceEndpoint'
|
import { SourceEndpoint } from './SourceEndpoint'
|
||||||
@ -11,6 +18,7 @@ import { StepContent } from './StepContent'
|
|||||||
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'
|
||||||
|
|
||||||
export const StepNode = ({
|
export const StepNode = ({
|
||||||
step,
|
step,
|
||||||
@ -144,58 +152,64 @@ export const StepNode = ({
|
|||||||
renderMenu={() => <StepNodeContextMenu stepId={step.id} />}
|
renderMenu={() => <StepNodeContextMenu stepId={step.id} />}
|
||||||
>
|
>
|
||||||
{(ref, isOpened) => (
|
{(ref, isOpened) => (
|
||||||
<Flex
|
<Popover placement="left" isLazy>
|
||||||
pos="relative"
|
<PopoverTrigger>
|
||||||
ref={ref}
|
<Flex
|
||||||
onMouseMove={handleMouseMove}
|
pos="relative"
|
||||||
onMouseDown={handleMouseDown}
|
ref={ref}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseMove={handleMouseMove}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseUp={handleMouseUp}
|
onMouseEnter={handleMouseEnter}
|
||||||
>
|
onMouseLeave={handleMouseLeave}
|
||||||
{connectedStubPosition === 'left' && (
|
onMouseUp={handleMouseUp}
|
||||||
<Box
|
data-testid={`step-${step.id}`}
|
||||||
h="2px"
|
>
|
||||||
pos="absolute"
|
{connectedStubPosition === 'left' && (
|
||||||
left="-18px"
|
<Box
|
||||||
top="25px"
|
h="2px"
|
||||||
w="18px"
|
pos="absolute"
|
||||||
bgColor="blue.500"
|
left="-18px"
|
||||||
/>
|
top="25px"
|
||||||
)}
|
w="18px"
|
||||||
<HStack
|
bgColor="blue.500"
|
||||||
flex="1"
|
/>
|
||||||
userSelect="none"
|
)}
|
||||||
p="3"
|
<HStack
|
||||||
borderWidth="2px"
|
flex="1"
|
||||||
borderColor={isConnecting || isOpened ? 'blue.400' : 'gray.400'}
|
userSelect="none"
|
||||||
rounded="lg"
|
p="3"
|
||||||
cursor={'pointer'}
|
borderWidth="2px"
|
||||||
bgColor="white"
|
borderColor={isConnecting || isOpened ? 'blue.400' : 'gray.400'}
|
||||||
>
|
rounded="lg"
|
||||||
<StepIcon type={step.type} />
|
cursor={'pointer'}
|
||||||
<StepContent {...step} />
|
bgColor="white"
|
||||||
{isConnectable && (
|
>
|
||||||
<SourceEndpoint
|
<StepIcon type={step.type} />
|
||||||
onConnectionDragStart={handleConnectionDragStart}
|
<StepContent {...step} />
|
||||||
pos="absolute"
|
{isConnectable && (
|
||||||
right="20px"
|
<SourceEndpoint
|
||||||
/>
|
onConnectionDragStart={handleConnectionDragStart}
|
||||||
)}
|
pos="absolute"
|
||||||
</HStack>
|
right="20px"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
|
||||||
{isDefined(connectedStubPosition) && (
|
{isDefined(connectedStubPosition) && (
|
||||||
<Box
|
<Box
|
||||||
h="2px"
|
h="2px"
|
||||||
pos="absolute"
|
pos="absolute"
|
||||||
right={connectedStubPosition === 'left' ? undefined : '-18px'}
|
right={connectedStubPosition === 'left' ? undefined : '-18px'}
|
||||||
left={connectedStubPosition === 'left' ? '-18px' : undefined}
|
left={connectedStubPosition === 'left' ? '-18px' : undefined}
|
||||||
top="25px"
|
top="25px"
|
||||||
w="18px"
|
w="18px"
|
||||||
bgColor="gray.500"
|
bgColor="gray.500"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<SettingsPopoverContent step={step} />
|
||||||
|
</Popover>
|
||||||
)}
|
)}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
)
|
)
|
||||||
|
@ -21,8 +21,9 @@ export const PreviewDrawer = () => {
|
|||||||
const { setRightPanel } = useEditor()
|
const { setRightPanel } = useEditor()
|
||||||
const { previewingIds, setPreviewingIds } = useGraph()
|
const { previewingIds, setPreviewingIds } = useGraph()
|
||||||
const [isResizing, setIsResizing] = useState(false)
|
const [isResizing, setIsResizing] = useState(false)
|
||||||
const [width, setWidth] = useState(400)
|
const [width, setWidth] = useState(500)
|
||||||
const [isResizeHandleVisible, setIsResizeHandleVisible] = useState(false)
|
const [isResizeHandleVisible, setIsResizeHandleVisible] = useState(false)
|
||||||
|
const [restartKey, setRestartKey] = useState(0)
|
||||||
|
|
||||||
const publicTypebot = useMemo(
|
const publicTypebot = useMemo(
|
||||||
() => (typebot ? parseTypebotToPublicTypebot(typebot) : undefined),
|
() => (typebot ? parseTypebotToPublicTypebot(typebot) : undefined),
|
||||||
@ -47,11 +48,13 @@ export const PreviewDrawer = () => {
|
|||||||
const handleNewBlockVisible = (targetId: string) =>
|
const handleNewBlockVisible = (targetId: string) =>
|
||||||
setPreviewingIds({
|
setPreviewingIds({
|
||||||
sourceId: !previewingIds.sourceId
|
sourceId: !previewingIds.sourceId
|
||||||
? 'start-block'
|
? typebot?.blocks.allIds[0]
|
||||||
: previewingIds.targetId,
|
: previewingIds.targetId,
|
||||||
targetId: targetId,
|
targetId: targetId,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleRestartClick = () => setRestartKey((key) => key + 1)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
pos="absolute"
|
pos="absolute"
|
||||||
@ -77,7 +80,7 @@ export const PreviewDrawer = () => {
|
|||||||
|
|
||||||
<VStack w="full" spacing={4}>
|
<VStack w="full" spacing={4}>
|
||||||
<Flex justifyContent={'space-between'} w="full">
|
<Flex justifyContent={'space-between'} w="full">
|
||||||
<Button>Restart</Button>
|
<Button onClick={handleRestartClick}>Restart</Button>
|
||||||
<CloseButton onClick={() => setRightPanel(undefined)} />
|
<CloseButton onClick={() => setRightPanel(undefined)} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
@ -87,6 +90,7 @@ export const PreviewDrawer = () => {
|
|||||||
borderRadius={'lg'}
|
borderRadius={'lg'}
|
||||||
h="full"
|
h="full"
|
||||||
w="full"
|
w="full"
|
||||||
|
key={restartKey}
|
||||||
pointerEvents={isResizing ? 'none' : 'auto'}
|
pointerEvents={isResizing ? 'none' : 'auto'}
|
||||||
>
|
>
|
||||||
<TypebotViewer
|
<TypebotViewer
|
||||||
|
31
apps/builder/components/shared/DebouncedInput.tsx
Normal file
31
apps/builder/components/shared/DebouncedInput.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Input, InputProps } from '@chakra-ui/react'
|
||||||
|
import { ChangeEvent, useEffect, useState } from 'react'
|
||||||
|
import { useDebounce } from 'use-debounce'
|
||||||
|
|
||||||
|
type Props = Omit<InputProps, 'onChange' | 'value'> & {
|
||||||
|
delay: number
|
||||||
|
initialValue: string
|
||||||
|
onChange: (debouncedValue: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DebouncedInput = ({
|
||||||
|
delay,
|
||||||
|
onChange,
|
||||||
|
initialValue,
|
||||||
|
...props
|
||||||
|
}: Props) => {
|
||||||
|
const [currentValue, setCurrentValue] = useState(initialValue)
|
||||||
|
const [currentValueDebounced] = useDebounce(currentValue, delay)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentValueDebounced === initialValue) return
|
||||||
|
onChange(currentValueDebounced)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [currentValueDebounced])
|
||||||
|
|
||||||
|
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setCurrentValue(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Input {...props} value={currentValue} onChange={handleChange} />
|
||||||
|
}
|
30
apps/builder/components/shared/SwitchWithLabel.tsx
Normal file
30
apps/builder/components/shared/SwitchWithLabel.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { FormLabel, HStack, Switch, SwitchProps } from '@chakra-ui/react'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
type SwitchWithLabelProps = {
|
||||||
|
label: string
|
||||||
|
initialValue: boolean
|
||||||
|
onCheckChange: (isChecked: boolean) => void
|
||||||
|
} & SwitchProps
|
||||||
|
|
||||||
|
export const SwitchWithLabel = ({
|
||||||
|
label,
|
||||||
|
initialValue,
|
||||||
|
onCheckChange,
|
||||||
|
...props
|
||||||
|
}: SwitchWithLabelProps) => {
|
||||||
|
const [isChecked, setIsChecked] = useState(initialValue)
|
||||||
|
|
||||||
|
const handleChange = () => {
|
||||||
|
setIsChecked(!isChecked)
|
||||||
|
onCheckChange(!isChecked)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<HStack justifyContent="space-between">
|
||||||
|
<FormLabel htmlFor={props.id} mb="0">
|
||||||
|
{label}
|
||||||
|
</FormLabel>
|
||||||
|
<Switch isChecked={isChecked} onChange={handleChange} {...props} />
|
||||||
|
</HStack>
|
||||||
|
)
|
||||||
|
}
|
@ -15,6 +15,9 @@ export const seedDb = async () => {
|
|||||||
return createAnswers()
|
return createAnswers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createTypebot = (typebot: Typebot) =>
|
||||||
|
prisma.typebot.create({ data: typebot as any })
|
||||||
|
|
||||||
const createUsers = () =>
|
const createUsers = () =>
|
||||||
prisma.user.createMany({
|
prisma.user.createMany({
|
||||||
data: [
|
data: [
|
||||||
|
@ -3,7 +3,7 @@ import {
|
|||||||
FacebookSocialLogin,
|
FacebookSocialLogin,
|
||||||
GoogleSocialLogin,
|
GoogleSocialLogin,
|
||||||
} from 'cypress-social-logins/src/Plugins'
|
} from 'cypress-social-logins/src/Plugins'
|
||||||
import { seedDb } from './database'
|
import { createTypebot, seedDb } from './database'
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -16,6 +16,7 @@ const handler = (on: any) => {
|
|||||||
FacebookSocialLogin: FacebookSocialLogin,
|
FacebookSocialLogin: FacebookSocialLogin,
|
||||||
GitHubSocialLogin: GitHubSocialLogin,
|
GitHubSocialLogin: GitHubSocialLogin,
|
||||||
seed: seedDb,
|
seed: seedDb,
|
||||||
|
createTypebot,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
66
apps/builder/cypress/tests/inputs.ts
Normal file
66
apps/builder/cypress/tests/inputs.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { parseTestTypebot } from 'cypress/plugins/utils'
|
||||||
|
import { StepType } 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'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
cy.signOut()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('text input 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('button', { name: 'Restart' }).click()
|
||||||
|
getIframeBody().findByPlaceholderText('Your name...').should('exist')
|
||||||
|
getIframeBody().findByRole('button', { name: 'Go' })
|
||||||
|
cy.findByTestId('step-step1').click({ force: true })
|
||||||
|
cy.findByRole('checkbox', { name: 'Long text?' }).check({ force: true })
|
||||||
|
cy.findByRole('button', { name: 'Restart' }).click()
|
||||||
|
getIframeBody().findByTestId('textarea').should('exist')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const getIframeBody = () => {
|
||||||
|
return cy
|
||||||
|
.get('#typebot-iframe')
|
||||||
|
.its('0.contentDocument.body')
|
||||||
|
.should('not.be.empty')
|
||||||
|
.then(cy.wrap)
|
||||||
|
}
|
@ -35,6 +35,7 @@
|
|||||||
"kbar": "^0.1.0-beta.24",
|
"kbar": "^0.1.0-beta.24",
|
||||||
"micro": "^9.3.4",
|
"micro": "^9.3.4",
|
||||||
"micro-cors": "^0.1.1",
|
"micro-cors": "^0.1.1",
|
||||||
|
"models": "*",
|
||||||
"next": "^12.0.7",
|
"next": "^12.0.7",
|
||||||
"next-auth": "beta",
|
"next-auth": "beta",
|
||||||
"nodemailer": "^6.7.2",
|
"nodemailer": "^6.7.2",
|
||||||
@ -56,8 +57,7 @@
|
|||||||
"swr": "^1.1.2",
|
"swr": "^1.1.2",
|
||||||
"use-debounce": "^7.0.1",
|
"use-debounce": "^7.0.1",
|
||||||
"use-immer": "^0.6.0",
|
"use-immer": "^0.6.0",
|
||||||
"utils": "*",
|
"utils": "*"
|
||||||
"models": "*"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/cypress": "^8.0.2",
|
"@testing-library/cypress": "^8.0.2",
|
||||||
|
@ -19,6 +19,9 @@ export const AvatarSideContainer = () => {
|
|||||||
setMarginBottom(isMobile ? 38 : 48)
|
setMarginBottom(isMobile ? 38 : 48)
|
||||||
})
|
})
|
||||||
resizeObserver.observe(document.body)
|
resizeObserver.observe(document.body)
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect()
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
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 { Step } from 'models'
|
import { InputStep, Step } from 'models'
|
||||||
import { isTextInputStep, isTextStep } from '../../../services/utils'
|
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'
|
||||||
@ -24,11 +24,17 @@ export const ChatStep = ({
|
|||||||
if (isTextStep(step))
|
if (isTextStep(step))
|
||||||
return <HostMessageBubble step={step} onTransitionEnd={onTransitionEnd} />
|
return <HostMessageBubble step={step} onTransitionEnd={onTransitionEnd} />
|
||||||
if (isTextInputStep(step))
|
if (isTextInputStep(step))
|
||||||
return <InputChatStep onSubmit={handleInputSubmit} />
|
return <InputChatStep step={step} onSubmit={handleInputSubmit} />
|
||||||
return <span>No step</span>
|
return <span>No step</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
const InputChatStep = ({ onSubmit }: { onSubmit: (value: string) => void }) => {
|
const InputChatStep = ({
|
||||||
|
step,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
step: InputStep
|
||||||
|
onSubmit: (value: string) => void
|
||||||
|
}) => {
|
||||||
const { addNewAvatarOffset } = useHostAvatars()
|
const { addNewAvatarOffset } = useHostAvatars()
|
||||||
const [answer, setAnswer] = useState<string>()
|
const [answer, setAnswer] = useState<string>()
|
||||||
|
|
||||||
@ -44,5 +50,5 @@ const InputChatStep = ({ onSubmit }: { onSubmit: (value: string) => void }) => {
|
|||||||
if (answer) {
|
if (answer) {
|
||||||
return <GuestBubble message={answer} />
|
return <GuestBubble message={answer} />
|
||||||
}
|
}
|
||||||
return <TextInput onSubmit={handleSubmit} />
|
return <TextInput step={step} onSubmit={handleSubmit} />
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
|
import { TextInputStep } from 'models'
|
||||||
import React, { FormEvent, useRef, useState } from 'react'
|
import React, { FormEvent, useRef, useState } from 'react'
|
||||||
import { SendIcon } from '../../../../assets/icons'
|
import { SendIcon } from '../../../../assets/icons'
|
||||||
|
|
||||||
type TextInputProps = {
|
type TextInputProps = {
|
||||||
|
step: TextInputStep
|
||||||
onSubmit: (value: string) => void
|
onSubmit: (value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextInput = ({ onSubmit }: TextInputProps) => {
|
export const TextInput = ({ step, onSubmit }: TextInputProps) => {
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef(null)
|
||||||
const [inputValue, setInputValue] = useState('')
|
const [inputValue, setInputValue] = useState('')
|
||||||
|
|
||||||
@ -19,24 +21,43 @@ export const TextInput = ({ onSubmit }: TextInputProps) => {
|
|||||||
<div className="flex flex-col w-full lg:w-4/6">
|
<div className="flex flex-col w-full lg:w-4/6">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<form
|
<form
|
||||||
className="flex items-center justify-between rounded-lg pr-2 typebot-input"
|
className="flex items-end justify-between rounded-lg pr-2 typebot-input"
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
<input
|
{step.options?.isLong ? (
|
||||||
ref={inputRef}
|
<textarea
|
||||||
className="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full comp-input"
|
ref={inputRef}
|
||||||
type="text"
|
className="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full comp-input"
|
||||||
placeholder={'Type your answer...'}
|
placeholder={
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
step.options?.labels?.placeholder ?? 'Type your answer...'
|
||||||
required
|
}
|
||||||
/>
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
data-testid="textarea"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
className="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full comp-input"
|
||||||
|
type="text"
|
||||||
|
placeholder={
|
||||||
|
step.options?.labels?.placeholder ?? 'Type your answer...'
|
||||||
|
}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={
|
className={
|
||||||
'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'
|
'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">Submit</span>
|
<span className="hidden xs:flex">
|
||||||
|
{step.options?.labels?.button ?? 'Send'}
|
||||||
|
</span>
|
||||||
<SendIcon className="send-icon flex xs:hidden" />
|
<SendIcon className="send-icon flex xs:hidden" />
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -24,6 +24,12 @@ export type TextStep = StepBase & {
|
|||||||
|
|
||||||
export type TextInputStep = StepBase & {
|
export type TextInputStep = StepBase & {
|
||||||
type: StepType.TEXT_INPUT
|
type: StepType.TEXT_INPUT
|
||||||
|
options?: TextInputOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TextInputOptions = {
|
||||||
|
labels?: { placeholder?: string; button?: string }
|
||||||
|
isLong?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Target = { blockId: string; stepId?: string }
|
export type Target = { blockId: string; stepId?: string }
|
||||||
|
Reference in New Issue
Block a user