diff --git a/apps/builder/components/board/Board.tsx b/apps/builder/components/board/Board.tsx index 24cc10ecc..a26f8c6ec 100644 --- a/apps/builder/components/board/Board.tsx +++ b/apps/builder/components/board/Board.tsx @@ -15,8 +15,8 @@ export const Board = () => { + {rightPanel === RightPanel.PREVIEW && } - {rightPanel === RightPanel.PREVIEW && } ) diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/SettingsPopoverContent.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/SettingsPopoverContent.tsx new file mode 100644 index 000000000..4aeb27027 --- /dev/null +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/SettingsPopoverContent.tsx @@ -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 ( + + + + + + + ) +} + +const SettingsPopoverBodyContent = ({ step }: Props) => { + const { updateStep } = useTypebot() + const handleOptionsChange = (options: TextInputOptions) => + updateStep(step.id, { options } as Partial) + + switch (step.type) { + case StepType.TEXT_INPUT: { + return ( + + ) + } + default: { + return <> + } + } +} diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/TextInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/TextInputSettingsBody.tsx new file mode 100644 index 000000000..ec72abf13 --- /dev/null +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/TextInputSettingsBody.tsx @@ -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 ( + + + + + Placeholder: + + + + + + Button label: + + + + + ) +} diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/index.ts b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/index.ts new file mode 100644 index 000000000..1820f3b5b --- /dev/null +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/index.ts @@ -0,0 +1 @@ +export { SettingsPopoverContent } from './SettingsPopoverContent' diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx index 88909f820..2a7592f88 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx @@ -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 { Block, Step, StepType } from 'models' import { SourceEndpoint } from './SourceEndpoint' @@ -11,6 +18,7 @@ import { StepContent } from './StepContent' import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { ContextMenu } from 'components/shared/ContextMenu' import { StepNodeContextMenu } from './RightClickMenu' +import { SettingsPopoverContent } from './SettingsPopoverContent' export const StepNode = ({ step, @@ -144,58 +152,64 @@ export const StepNode = ({ renderMenu={() => } > {(ref, isOpened) => ( - - {connectedStubPosition === 'left' && ( - - )} - - - - {isConnectable && ( - - )} - + + + + {connectedStubPosition === 'left' && ( + + )} + + + + {isConnectable && ( + + )} + - {isDefined(connectedStubPosition) && ( - - )} - + {isDefined(connectedStubPosition) && ( + + )} + + + + )} ) diff --git a/apps/builder/components/board/preview/PreviewDrawer.tsx b/apps/builder/components/board/preview/PreviewDrawer.tsx index c64ea2883..3e71b754e 100644 --- a/apps/builder/components/board/preview/PreviewDrawer.tsx +++ b/apps/builder/components/board/preview/PreviewDrawer.tsx @@ -21,8 +21,9 @@ export const PreviewDrawer = () => { const { setRightPanel } = useEditor() const { previewingIds, setPreviewingIds } = useGraph() const [isResizing, setIsResizing] = useState(false) - const [width, setWidth] = useState(400) + const [width, setWidth] = useState(500) const [isResizeHandleVisible, setIsResizeHandleVisible] = useState(false) + const [restartKey, setRestartKey] = useState(0) const publicTypebot = useMemo( () => (typebot ? parseTypebotToPublicTypebot(typebot) : undefined), @@ -47,11 +48,13 @@ export const PreviewDrawer = () => { const handleNewBlockVisible = (targetId: string) => setPreviewingIds({ sourceId: !previewingIds.sourceId - ? 'start-block' + ? typebot?.blocks.allIds[0] : previewingIds.targetId, targetId: targetId, }) + const handleRestartClick = () => setRestartKey((key) => key + 1) + return ( { - + setRightPanel(undefined)} /> @@ -87,6 +90,7 @@ export const PreviewDrawer = () => { borderRadius={'lg'} h="full" w="full" + key={restartKey} pointerEvents={isResizing ? 'none' : 'auto'} > & { + 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) => { + setCurrentValue(e.target.value) + } + + return +} diff --git a/apps/builder/components/shared/SwitchWithLabel.tsx b/apps/builder/components/shared/SwitchWithLabel.tsx new file mode 100644 index 000000000..b31c49c2c --- /dev/null +++ b/apps/builder/components/shared/SwitchWithLabel.tsx @@ -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 ( + + + {label} + + + + ) +} diff --git a/apps/builder/cypress/plugins/database.ts b/apps/builder/cypress/plugins/database.ts index 4fef0458a..3aedaabcd 100644 --- a/apps/builder/cypress/plugins/database.ts +++ b/apps/builder/cypress/plugins/database.ts @@ -15,6 +15,9 @@ export const seedDb = async () => { return createAnswers() } +export const createTypebot = (typebot: Typebot) => + prisma.typebot.create({ data: typebot as any }) + const createUsers = () => prisma.user.createMany({ data: [ diff --git a/apps/builder/cypress/plugins/index.ts b/apps/builder/cypress/plugins/index.ts index 0723ef984..60b90b633 100644 --- a/apps/builder/cypress/plugins/index.ts +++ b/apps/builder/cypress/plugins/index.ts @@ -3,7 +3,7 @@ import { FacebookSocialLogin, GoogleSocialLogin, } from 'cypress-social-logins/src/Plugins' -import { seedDb } from './database' +import { createTypebot, seedDb } from './database' /// /** @@ -16,6 +16,7 @@ const handler = (on: any) => { FacebookSocialLogin: FacebookSocialLogin, GitHubSocialLogin: GitHubSocialLogin, seed: seedDb, + createTypebot, }) } diff --git a/apps/builder/cypress/tests/inputs.ts b/apps/builder/cypress/tests/inputs.ts new file mode 100644 index 000000000..c015fc10a --- /dev/null +++ b/apps/builder/cypress/tests/inputs.ts @@ -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) +} diff --git a/apps/builder/package.json b/apps/builder/package.json index 37737f92c..977ec338b 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -35,6 +35,7 @@ "kbar": "^0.1.0-beta.24", "micro": "^9.3.4", "micro-cors": "^0.1.1", + "models": "*", "next": "^12.0.7", "next-auth": "beta", "nodemailer": "^6.7.2", @@ -56,8 +57,7 @@ "swr": "^1.1.2", "use-debounce": "^7.0.1", "use-immer": "^0.6.0", - "utils": "*", - "models": "*" + "utils": "*" }, "devDependencies": { "@testing-library/cypress": "^8.0.2", diff --git a/packages/bot-engine/src/components/ChatBlock/AvatarSideContainer.tsx b/packages/bot-engine/src/components/ChatBlock/AvatarSideContainer.tsx index 1829c14a8..a96cbdeef 100644 --- a/packages/bot-engine/src/components/ChatBlock/AvatarSideContainer.tsx +++ b/packages/bot-engine/src/components/ChatBlock/AvatarSideContainer.tsx @@ -19,6 +19,9 @@ export const AvatarSideContainer = () => { setMarginBottom(isMobile ? 38 : 48) }) resizeObserver.observe(document.body) + return () => { + resizeObserver.disconnect() + } }, []) return ( diff --git a/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx b/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx index f6d6d3cea..28c14769b 100644 --- a/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx +++ b/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react' import { useAnswers } from '../../../contexts/AnswersContext' import { useHostAvatars } from '../../../contexts/HostAvatarsContext' -import { Step } from 'models' +import { InputStep, Step } from 'models' import { isTextInputStep, isTextStep } from '../../../services/utils' import { GuestBubble } from './bubbles/GuestBubble' import { HostMessageBubble } from './bubbles/HostMessageBubble' @@ -24,11 +24,17 @@ export const ChatStep = ({ if (isTextStep(step)) return if (isTextInputStep(step)) - return + return return No step } -const InputChatStep = ({ onSubmit }: { onSubmit: (value: string) => void }) => { +const InputChatStep = ({ + step, + onSubmit, +}: { + step: InputStep + onSubmit: (value: string) => void +}) => { const { addNewAvatarOffset } = useHostAvatars() const [answer, setAnswer] = useState() @@ -44,5 +50,5 @@ const InputChatStep = ({ onSubmit }: { onSubmit: (value: string) => void }) => { if (answer) { return } - return + return } diff --git a/packages/bot-engine/src/components/ChatBlock/ChatStep/inputs/TextInput.tsx b/packages/bot-engine/src/components/ChatBlock/ChatStep/inputs/TextInput.tsx index 5dd9bce80..00abb5090 100644 --- a/packages/bot-engine/src/components/ChatBlock/ChatStep/inputs/TextInput.tsx +++ b/packages/bot-engine/src/components/ChatBlock/ChatStep/inputs/TextInput.tsx @@ -1,11 +1,13 @@ +import { TextInputStep } from 'models' import React, { FormEvent, useRef, useState } from 'react' import { SendIcon } from '../../../../assets/icons' type TextInputProps = { + step: TextInputStep onSubmit: (value: string) => void } -export const TextInput = ({ onSubmit }: TextInputProps) => { +export const TextInput = ({ step, onSubmit }: TextInputProps) => { const inputRef = useRef(null) const [inputValue, setInputValue] = useState('') @@ -19,24 +21,43 @@ export const TextInput = ({ onSubmit }: TextInputProps) => {
- setInputValue(e.target.value)} - required - /> + {step.options?.isLong ? ( +