From 4ccb7bca49aa10b6ebca09eba0274346814dfa04 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Fri, 14 Jan 2022 07:49:24 +0100 Subject: [PATCH] =?UTF-8?q?feat(inputs):=20=E2=9C=A8=20Add=20Set=20variabl?= =?UTF-8?q?e=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/StepTypesList/StepCard.tsx | 6 +- .../board/StepTypesList/StepIcon.tsx | 6 +- .../board/StepTypesList/StepTypeLabel.tsx | 5 +- .../board/StepTypesList/StepTypesList.tsx | 13 +- .../SettingsPopoverContent.tsx | 17 +- .../bodies/ChoiceInputSettingsBody.tsx | 14 +- .../bodies/DateInputSettingsBody.tsx | 14 +- .../bodies/EmailInputSettingsBody.tsx | 14 +- .../bodies/NumberInputSettingsBody.tsx | 14 +- .../bodies/PhoneNumberSettingsBody.tsx | 14 +- .../bodies/SetVariableSettingsBody.tsx | 46 +++++ .../bodies/TextInputSettingsBody.tsx | 14 +- .../bodies/UrlInputSettingsBody.tsx | 14 +- .../graph/BlockNode/StepNode/StepNode.tsx | 12 +- .../BlockNode/StepNode/StepNodeContent.tsx | 27 ++- .../StepNode/TextEditor/TextEditor.tsx | 78 +++++++- .../BlockNode/StepNode/TextEditor/ToolBar.tsx | 12 +- .../board/preview/PreviewDrawer.tsx | 2 +- .../components/shared/DebouncedTextarea.tsx | 38 ++++ .../SearchableDropdown.tsx | 0 .../components/shared/VariableSearchInput.tsx | 170 ++++++++++++++++++ .../FontSelector/FontSelector.tsx | 2 +- apps/builder/contexts/DndContext.tsx | 6 +- .../TypebotContext/TypebotContext.tsx | 9 +- .../contexts/TypebotContext/actions/blocks.ts | 17 +- .../contexts/TypebotContext/actions/steps.ts | 7 +- .../TypebotContext/actions/variables.ts | 60 +++++++ .../fixtures/typebots/logic/setVariable.json | 129 +++++++++++++ .../fixtures/typebots/singleChoiceTarget.json | 1 + apps/builder/cypress/plugins/database.ts | 1 + apps/builder/cypress/plugins/utils.ts | 1 + apps/builder/cypress/support/index.ts | 8 + apps/builder/cypress/tests/inputs.ts | 17 +- apps/builder/cypress/tests/logic.ts | 45 +++++ apps/builder/libs/chakra.ts | 5 + apps/builder/libs/plate.ts | 28 +-- apps/builder/services/publicTypebot.tsx | 1 + apps/builder/services/typebots.ts | 20 +-- apps/builder/services/utils.ts | 3 + .../src/components/ChatBlock/ChatBlock.tsx | 82 ++++++--- .../ChatStep/bubbles/HostMessageBubble.tsx | 18 +- .../src/contexts/TypebotContext.tsx | 20 ++- packages/bot-engine/src/services/variable.ts | 46 +++++ packages/bot-engine/tsconfig.json | 3 +- packages/db/prisma/schema.draft.prisma | 87 --------- packages/db/prisma/schema.prisma | 2 + packages/models/src/publicTypebot.ts | 10 +- packages/models/src/typebot/index.ts | 1 + packages/models/src/typebot/steps/index.ts | 1 + packages/models/src/typebot/steps/inputs.ts | 47 ++--- packages/models/src/typebot/steps/logic.ts | 17 ++ packages/models/src/typebot/steps/steps.ts | 5 +- packages/models/src/typebot/typebot.ts | 4 +- packages/models/src/typebot/variable.ts | 5 + packages/utils/src/utils.ts | 9 + 55 files changed, 1024 insertions(+), 223 deletions(-) create mode 100644 apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/SetVariableSettingsBody.tsx create mode 100644 apps/builder/components/shared/DebouncedTextarea.tsx rename apps/builder/components/{theme/GeneralContent/FontSelector => shared}/SearchableDropdown.tsx (100%) create mode 100644 apps/builder/components/shared/VariableSearchInput.tsx create mode 100644 apps/builder/contexts/TypebotContext/actions/variables.ts create mode 100644 apps/builder/cypress/fixtures/typebots/logic/setVariable.json create mode 100644 apps/builder/cypress/tests/logic.ts create mode 100644 packages/bot-engine/src/services/variable.ts delete mode 100644 packages/db/prisma/schema.draft.prisma create mode 100644 packages/models/src/typebot/steps/logic.ts create mode 100644 packages/models/src/typebot/variable.ts diff --git a/apps/builder/components/board/StepTypesList/StepCard.tsx b/apps/builder/components/board/StepTypesList/StepCard.tsx index 9d27b5db2..c21160007 100644 --- a/apps/builder/components/board/StepTypesList/StepCard.tsx +++ b/apps/builder/components/board/StepTypesList/StepCard.tsx @@ -1,5 +1,5 @@ import { Button, ButtonProps, Flex, HStack } from '@chakra-ui/react' -import { BubbleStepType, InputStepType, StepType } from 'models' +import { BubbleStepType, InputStepType, StepType, LogicStepType } from 'models' import { useDnd } from 'contexts/DndContext' import React, { useEffect, useState } from 'react' import { StepIcon } from './StepIcon' @@ -9,10 +9,10 @@ export const StepCard = ({ type, onMouseDown, }: { - type: BubbleStepType | InputStepType + type: BubbleStepType | InputStepType | LogicStepType onMouseDown: ( e: React.MouseEvent, - type: BubbleStepType | InputStepType + type: BubbleStepType | InputStepType | LogicStepType ) => void }) => { const { draggedStepType } = useDnd() diff --git a/apps/builder/components/board/StepTypesList/StepIcon.tsx b/apps/builder/components/board/StepTypesList/StepIcon.tsx index e83ed5559..1e23a3a64 100644 --- a/apps/builder/components/board/StepTypesList/StepIcon.tsx +++ b/apps/builder/components/board/StepTypesList/StepIcon.tsx @@ -3,6 +3,7 @@ import { CalendarIcon, ChatIcon, CheckSquareIcon, + EditIcon, EmailIcon, FlagIcon, GlobeIcon, @@ -10,7 +11,7 @@ import { PhoneIcon, TextIcon, } from 'assets/icons' -import { BubbleStepType, InputStepType, StepType } from 'models' +import { BubbleStepType, InputStepType, LogicStepType, StepType } from 'models' import React from 'react' type StepIconProps = { type: StepType } & IconProps @@ -41,6 +42,9 @@ export const StepIcon = ({ type, ...props }: StepIconProps) => { case InputStepType.CHOICE: { return } + case LogicStepType.SET_VARIABLE: { + return + } case 'start': { return } diff --git a/apps/builder/components/board/StepTypesList/StepTypeLabel.tsx b/apps/builder/components/board/StepTypesList/StepTypeLabel.tsx index d6c0db4c2..53d0f0cb8 100644 --- a/apps/builder/components/board/StepTypesList/StepTypeLabel.tsx +++ b/apps/builder/components/board/StepTypesList/StepTypeLabel.tsx @@ -1,5 +1,5 @@ import { Text } from '@chakra-ui/react' -import { BubbleStepType, InputStepType, StepType } from 'models' +import { BubbleStepType, InputStepType, LogicStepType, StepType } from 'models' import React from 'react' type Props = { type: StepType } @@ -28,6 +28,9 @@ export const StepTypeLabel = ({ type }: Props) => { case InputStepType.CHOICE: { return Button } + case LogicStepType.SET_VARIABLE: { + return Set variable + } default: { return <> } diff --git a/apps/builder/components/board/StepTypesList/StepTypesList.tsx b/apps/builder/components/board/StepTypesList/StepTypesList.tsx index 5b79e734d..3ad3559b1 100644 --- a/apps/builder/components/board/StepTypesList/StepTypesList.tsx +++ b/apps/builder/components/board/StepTypesList/StepTypesList.tsx @@ -5,7 +5,7 @@ import { SimpleGrid, useEventListener, } from '@chakra-ui/react' -import { BubbleStepType, InputStepType } from 'models' +import { BubbleStepType, InputStepType, LogicStepType } from 'models' import { useDnd } from 'contexts/DndContext' import React, { useState } from 'react' import { StepCard, StepCardOverlay } from './StepCard' @@ -31,7 +31,7 @@ export const StepTypesList = () => { const handleMouseDown = ( e: React.MouseEvent, - type: BubbleStepType | InputStepType + type: BubbleStepType | InputStepType | LogicStepType ) => { const element = e.currentTarget as HTMLDivElement const rect = element.getBoundingClientRect() @@ -85,6 +85,15 @@ export const StepTypesList = () => { ))} + + + Logic + + + {Object.values(LogicStepType).map((type) => ( + + ))} + {draggedStepType && ( { const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation() @@ -35,7 +40,7 @@ export const SettingsPopoverContent = ({ step }: Props) => { const SettingsPopoverBodyContent = ({ step }: Props) => { const { updateStep } = useTypebot() const handleOptionsChange = ( - options: TextInputOptions | ChoiceInputOptions + options: TextInputOptions | ChoiceInputOptions | SetVariableOptions ) => updateStep(step.id, { options } as Partial) switch (step.type) { @@ -95,6 +100,14 @@ const SettingsPopoverBodyContent = ({ step }: Props) => { /> ) } + case LogicStepType.SET_VARIABLE: { + return ( + + ) + } default: { return <> } diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ChoiceInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ChoiceInputSettingsBody.tsx index 5de3c0939..17d24add2 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ChoiceInputSettingsBody.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/ChoiceInputSettingsBody.tsx @@ -1,7 +1,8 @@ import { FormLabel, Stack } from '@chakra-ui/react' import { DebouncedInput } from 'components/shared/DebouncedInput' import { SwitchWithLabel } from 'components/shared/SwitchWithLabel' -import { ChoiceInputOptions } from 'models' +import { VariableSearchInput } from 'components/shared/VariableSearchInput' +import { ChoiceInputOptions, Variable } from 'models' import React from 'react' type ChoiceInputSettingsBodyProps = { @@ -17,6 +18,8 @@ export const ChoiceInputSettingsBody = ({ options && onOptionsChange({ ...options, isMultipleChoice }) const handleButtonLabelChange = (buttonLabel: string) => options && onOptionsChange({ ...options, buttonLabel }) + const handleVariableChange = (variable: Variable) => + options && onOptionsChange({ ...options, variableId: variable.id }) return ( @@ -39,6 +42,15 @@ export const ChoiceInputSettingsBody = ({ /> )} + + + Save answer in a variable: + + + ) } diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/DateInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/DateInputSettingsBody.tsx index 223253ef6..1649f5f49 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/DateInputSettingsBody.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/DateInputSettingsBody.tsx @@ -1,7 +1,8 @@ import { FormLabel, Stack } from '@chakra-ui/react' import { DebouncedInput } from 'components/shared/DebouncedInput' import { SwitchWithLabel } from 'components/shared/SwitchWithLabel' -import { DateInputOptions } from 'models' +import { VariableSearchInput } from 'components/shared/VariableSearchInput' +import { DateInputOptions, Variable } from 'models' import React from 'react' type DateInputSettingsBodyProps = { @@ -23,6 +24,8 @@ export const DateInputSettingsBody = ({ onOptionsChange({ ...options, isRange }) const handleHasTimeChange = (hasTime: boolean) => onOptionsChange({ ...options, hasTime }) + const handleVariableChange = (variable: Variable) => + onOptionsChange({ ...options, variableId: variable.id }) return ( @@ -75,6 +78,15 @@ export const DateInputSettingsBody = ({ onChange={handleButtonLabelChange} /> + + + Save answer in a variable: + + + ) } diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/EmailInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/EmailInputSettingsBody.tsx index dd8aaeeee..67252c296 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/EmailInputSettingsBody.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/EmailInputSettingsBody.tsx @@ -1,6 +1,7 @@ import { FormLabel, Stack } from '@chakra-ui/react' import { DebouncedInput } from 'components/shared/DebouncedInput' -import { EmailInputOptions } from 'models' +import { VariableSearchInput } from 'components/shared/VariableSearchInput' +import { EmailInputOptions, Variable } from 'models' import React from 'react' type EmailInputSettingsBodyProps = { @@ -16,6 +17,8 @@ export const EmailInputSettingsBody = ({ onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } }) const handleButtonLabelChange = (button: string) => onOptionsChange({ ...options, labels: { ...options?.labels, button } }) + const handleVariableChange = (variable: Variable) => + onOptionsChange({ ...options, variableId: variable.id }) return ( @@ -41,6 +44,15 @@ export const EmailInputSettingsBody = ({ onChange={handleButtonLabelChange} /> + + + Save answer in a variable: + + + ) } diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/NumberInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/NumberInputSettingsBody.tsx index 7e022f3a1..404c5cca1 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/NumberInputSettingsBody.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/NumberInputSettingsBody.tsx @@ -1,7 +1,8 @@ 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 { VariableSearchInput } from 'components/shared/VariableSearchInput' +import { NumberInputOptions, Variable } from 'models' import React from 'react' import { removeUndefinedFields } from 'services/utils' @@ -24,6 +25,8 @@ export const NumberInputSettingsBody = ({ onOptionsChange(removeUndefinedFields({ ...options, max })) const handleStepChange = (step?: number) => onOptionsChange(removeUndefinedFields({ ...options, step })) + const handleVariableChange = (variable: Variable) => + onOptionsChange({ ...options, variableId: variable.id }) return ( @@ -79,6 +82,15 @@ export const NumberInputSettingsBody = ({ onValueChange={handleStepChange} /> + + + Save answer in a variable: + + + ) } diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/PhoneNumberSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/PhoneNumberSettingsBody.tsx index 8a352b885..2961c7ebd 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/PhoneNumberSettingsBody.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/PhoneNumberSettingsBody.tsx @@ -1,6 +1,7 @@ import { FormLabel, Stack } from '@chakra-ui/react' import { DebouncedInput } from 'components/shared/DebouncedInput' -import { EmailInputOptions } from 'models' +import { VariableSearchInput } from 'components/shared/VariableSearchInput' +import { EmailInputOptions, Variable } from 'models' import React from 'react' type PhoneNumberSettingsBodyProps = { @@ -16,6 +17,8 @@ export const PhoneNumberSettingsBody = ({ onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } }) const handleButtonLabelChange = (button: string) => onOptionsChange({ ...options, labels: { ...options?.labels, button } }) + const handleVariableChange = (variable: Variable) => + onOptionsChange({ ...options, variableId: variable.id }) return ( @@ -41,6 +44,15 @@ export const PhoneNumberSettingsBody = ({ onChange={handleButtonLabelChange} /> + + + Save answer in a variable: + + + ) } diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/SetVariableSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/SetVariableSettingsBody.tsx new file mode 100644 index 000000000..4b4f51a2a --- /dev/null +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/SetVariableSettingsBody.tsx @@ -0,0 +1,46 @@ +import { FormLabel, Stack } from '@chakra-ui/react' +import { DebouncedTextarea } from 'components/shared/DebouncedTextarea' +import { VariableSearchInput } from 'components/shared/VariableSearchInput' +import { SetVariableOptions, Variable } from 'models' +import React from 'react' + +type Props = { + options?: SetVariableOptions + onOptionsChange: (options: SetVariableOptions) => void +} + +export const SetVariableSettingsBody = ({ + options, + onOptionsChange, +}: Props) => { + const handleVariableChange = (variable: Variable) => + onOptionsChange({ ...options, variableId: variable.id }) + const handleExpressionChange = (expressionToEvaluate: string) => + onOptionsChange({ ...options, expressionToEvaluate }) + + return ( + + + + Search or create variable: + + + + + + Value / Expression: + + + + + ) +} diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/TextInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/TextInputSettingsBody.tsx index ec72abf13..2a8dfb2fc 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/TextInputSettingsBody.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/TextInputSettingsBody.tsx @@ -1,7 +1,8 @@ import { FormLabel, Stack } from '@chakra-ui/react' import { DebouncedInput } from 'components/shared/DebouncedInput' import { SwitchWithLabel } from 'components/shared/SwitchWithLabel' -import { TextInputOptions } from 'models' +import { VariableSearchInput } from 'components/shared/VariableSearchInput' +import { TextInputOptions, Variable } from 'models' import React from 'react' type TextInputSettingsBodyProps = { @@ -19,6 +20,8 @@ export const TextInputSettingsBody = ({ onOptionsChange({ ...options, labels: { ...options?.labels, button } }) const handleLongChange = (isLong: boolean) => onOptionsChange({ ...options, isLong }) + const handleVariableChange = (variable: Variable) => + onOptionsChange({ ...options, variableId: variable.id }) return ( @@ -50,6 +53,15 @@ export const TextInputSettingsBody = ({ onChange={handleButtonLabelChange} /> + + + Save answer in a variable: + + + ) } diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/UrlInputSettingsBody.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/UrlInputSettingsBody.tsx index 33465d06f..063a5b262 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/UrlInputSettingsBody.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/SettingsPopoverContent/bodies/UrlInputSettingsBody.tsx @@ -1,6 +1,7 @@ import { FormLabel, Stack } from '@chakra-ui/react' import { DebouncedInput } from 'components/shared/DebouncedInput' -import { UrlInputOptions } from 'models' +import { VariableSearchInput } from 'components/shared/VariableSearchInput' +import { UrlInputOptions, Variable } from 'models' import React from 'react' type UrlInputSettingsBodyProps = { @@ -16,6 +17,8 @@ export const UrlInputSettingsBody = ({ onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } }) const handleButtonLabelChange = (button: string) => onOptionsChange({ ...options, labels: { ...options?.labels, button } }) + const handleVariableChange = (variable: Variable) => + onOptionsChange({ ...options, variableId: variable.id }) return ( @@ -41,6 +44,15 @@ export const UrlInputSettingsBody = ({ onChange={handleButtonLabelChange} /> + + + Save answer in a variable: + + + ) } diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx index b2b6dea0b..1d3e8485a 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/StepNode.tsx @@ -10,7 +10,13 @@ import React, { useEffect, useMemo, useState } from 'react' import { Block, Step } from 'models' import { useGraph } from 'contexts/GraphContext' import { StepIcon } from 'components/board/StepTypesList/StepIcon' -import { isChoiceInput, isDefined, isInputStep, isTextBubbleStep } from 'utils' +import { + isChoiceInput, + isDefined, + isInputStep, + isLogicStep, + isTextBubbleStep, +} from 'utils' import { Coordinates } from '@dnd-kit/core/dist/types' import { TextEditor } from './TextEditor/TextEditor' import { StepNodeContent } from './StepNodeContent' @@ -212,7 +218,9 @@ export const StepNode = ({ )} - {isInputStep(step) && } + {(isInputStep(step) || isLogicStep(step)) && ( + + )} )} diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent.tsx index a3b55f9bc..fd343a1d5 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/StepNodeContent.tsx @@ -1,5 +1,13 @@ import { Flex, Text } from '@chakra-ui/react' -import { Step, StartStep, BubbleStepType, InputStepType } from 'models' +import { useTypebot } from 'contexts/TypebotContext' +import { + Step, + StartStep, + BubbleStepType, + InputStepType, + LogicStepType, + SetVariableStep, +} from 'models' import { ChoiceItemsList } from './ChoiceInputStepNode/ChoiceItemsList' type Props = { @@ -68,6 +76,9 @@ export const StepNodeContent = ({ step }: Props) => { case InputStepType.CHOICE: { return } + case LogicStepType.SET_VARIABLE: { + return + } case 'start': { return {step.label} } @@ -76,3 +87,17 @@ export const StepNodeContent = ({ step }: Props) => { } } } + +const SetVariableNodeContent = ({ step }: { step: SetVariableStep }) => { + const { typebot } = useTypebot() + const variableName = + typebot?.variables.byId[step.options?.variableId ?? '']?.name ?? '' + const expression = step.options?.expressionToEvaluate ?? '' + return ( + + {variableName === '' && expression === '' + ? 'Click to edit...' + : `${variableName} = ${expression}`} + + ) +} diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/TextEditor/TextEditor.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/TextEditor/TextEditor.tsx index 9d868bf59..b199ef4ea 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/TextEditor/TextEditor.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/TextEditor/TextEditor.tsx @@ -1,4 +1,4 @@ -import { Stack, useOutsideClick } from '@chakra-ui/react' +import { Flex, Stack, useOutsideClick } from '@chakra-ui/react' import React, { useEffect, useMemo, useRef, useState } from 'react' import { Plate, @@ -10,10 +10,12 @@ import { import { editorStyle, platePlugins } from 'libs/plate' import { useDebounce } from 'use-debounce' import { useTypebot } from 'contexts/TypebotContext/TypebotContext' -import { createEditor } from 'slate' +import { BaseSelection, createEditor, Transforms } from 'slate' import { ToolBar } from './ToolBar' import { parseHtmlStringToPlainText } from 'services/utils' -import { TextStep } from 'models' +import { TextStep, Variable } from 'models' +import { VariableSearchInput } from 'components/shared/VariableSearchInput' +import { ReactEditor } from 'slate-react' type TextEditorProps = { stepId: string @@ -26,14 +28,20 @@ export const TextEditor = ({ stepId, onClose, }: TextEditorProps) => { + const randomEditorId = useMemo(() => Math.random().toString(), []) const editor = useMemo( - () => withPlate(createEditor(), { id: stepId, plugins: platePlugins }), + () => + withPlate(createEditor(), { id: randomEditorId, plugins: platePlugins }), // eslint-disable-next-line react-hooks/exhaustive-deps [] ) const { updateStep } = useTypebot() const [value, setValue] = useState(initialValue) const [debouncedValue] = useDebounce(value, 500) + const varDropdownRef = useRef(null) + const rememberedSelection = useRef(null) + const [isVariableDropdownOpen, setIsVariableDropdownOpen] = useState(false) + const textEditorRef = useRef(null) useOutsideClick({ ref: textEditorRef, @@ -48,6 +56,29 @@ export const TextEditor = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [debouncedValue]) + useEffect(() => { + if (!isVariableDropdownOpen) return + const el = varDropdownRef.current + if (!el) return + const { top, left } = computeTargetCoord() + el.style.top = `${top}px` + el.style.left = `${left}px` + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isVariableDropdownOpen]) + + const computeTargetCoord = () => { + const selection = window.getSelection() + const relativeParent = textEditorRef.current + if (!selection || !relativeParent) return { top: 0, left: 0 } + const range = selection.getRangeAt(0) + const selectionBoundingRect = range.getBoundingClientRect() + const relativeRect = relativeParent.getBoundingClientRect() + return { + top: selectionBoundingRect.bottom - relativeRect.top, + left: selectionBoundingRect.left - relativeRect.left, + } + } + const save = (value: unknown[]) => { if (value.length === 0) return const html = serializeHtml(editor, { @@ -65,6 +96,19 @@ export const TextEditor = ({ const handleMouseDown = (e: React.MouseEvent) => { e.stopPropagation() } + + const handleVariableSelected = (variable: Variable) => { + setIsVariableDropdownOpen(false) + if (!rememberedSelection.current) return + Transforms.select(editor, rememberedSelection.current) + Transforms.insertText(editor, '{{' + variable.name + '}}') + ReactEditor.focus(editor as unknown as ReactEditor) + } + + const handleChangeEditorContent = (val: unknown[]) => { + setValue(val) + setIsVariableDropdownOpen(false) + } return ( - + setIsVariableDropdownOpen(true)} /> { + rememberedSelection.current = editor.selection + }, }} initialValue={ initialValue.length === 0 ? [{ type: 'p', children: [{ text: '' }] }] : initialValue } - onChange={setValue} + onChange={handleChangeEditorContent} editor={editor} /> + {isVariableDropdownOpen && ( + + + + )} ) } diff --git a/apps/builder/components/board/graph/BlockNode/StepNode/TextEditor/ToolBar.tsx b/apps/builder/components/board/graph/BlockNode/StepNode/TextEditor/ToolBar.tsx index d374b85f5..4c98fae29 100644 --- a/apps/builder/components/board/graph/BlockNode/StepNode/TextEditor/ToolBar.tsx +++ b/apps/builder/components/board/graph/BlockNode/StepNode/TextEditor/ToolBar.tsx @@ -9,8 +9,14 @@ import { LinkToolbarButton } from '@udecode/plate-ui-link' import { MarkToolbarButton } from '@udecode/plate-ui-toolbar' import { BoldIcon, ItalicIcon, UnderlineIcon, LinkIcon } from 'assets/icons' -export const ToolBar = (props: StackProps) => { +type Props = { onVariablesButtonClick: () => void } & StackProps +export const ToolBar = (props: Props) => { const editor = usePlateEditorRef() + + const handleVariablesButtonMouseDown = (e: React.MouseEvent) => { + e.preventDefault() + props.onVariablesButtonClick() + } return ( { borderBottomWidth={1} {...props} > - + { const [restartKey, setRestartKey] = useState(0) const publicTypebot = useMemo( - () => (typebot ? parseTypebotToPublicTypebot(typebot) : undefined), + () => (typebot ? { ...parseTypebotToPublicTypebot(typebot) } : undefined), [typebot] ) diff --git a/apps/builder/components/shared/DebouncedTextarea.tsx b/apps/builder/components/shared/DebouncedTextarea.tsx new file mode 100644 index 000000000..294c2357c --- /dev/null +++ b/apps/builder/components/shared/DebouncedTextarea.tsx @@ -0,0 +1,38 @@ +import { Textarea, TextareaProps } from '@chakra-ui/react' +import { ChangeEvent, useEffect, useState } from 'react' +import { useDebounce } from 'use-debounce' + +type Props = Omit & { + delay: number + initialValue: string + onChange: (debouncedValue: string) => void +} + +export const DebouncedTextarea = ({ + 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 ( +