diff --git a/apps/builder/components/editor/BoardMenuButton.tsx b/apps/builder/components/editor/BoardMenuButton.tsx index 921854422..484b25010 100644 --- a/apps/builder/components/editor/BoardMenuButton.tsx +++ b/apps/builder/components/editor/BoardMenuButton.tsx @@ -6,6 +6,7 @@ import { MenuItem, MenuList, } from '@chakra-ui/react' +import assert from 'assert' import { DownloadIcon, MoreVerticalIcon } from 'assets/icons' import { useTypebot } from 'contexts/TypebotContext' import React, { useState } from 'react' @@ -16,7 +17,7 @@ export const BoardMenuButton = (props: MenuButtonProps) => { const [isDownloading, setIsDownloading] = useState(false) const downloadFlow = () => { - if (!typebot) return + assert(typebot) setIsDownloading(true) const data = 'data:application/json;charset=utf-8,' + @@ -39,6 +40,7 @@ export const BoardMenuButton = (props: MenuButtonProps) => { colorScheme="blue" icon={} isLoading={isDownloading} + size="sm" {...props} /> diff --git a/apps/builder/components/editor/StepsSideBar/StepCard.tsx b/apps/builder/components/editor/StepsSideBar/StepCard.tsx index 1c7c41fd3..5b8b98bed 100644 --- a/apps/builder/components/editor/StepsSideBar/StepCard.tsx +++ b/apps/builder/components/editor/StepsSideBar/StepCard.tsx @@ -1,6 +1,6 @@ import { Flex, HStack, StackProps, Text } from '@chakra-ui/react' import { StepType, DraggableStepType } from 'models' -import { useStepDnd } from 'contexts/StepDndContext' +import { useStepDnd } from 'contexts/GraphDndContext' import React, { useEffect, useState } from 'react' import { StepIcon } from './StepIcon' import { StepTypeLabel } from './StepTypeLabel' diff --git a/apps/builder/components/editor/StepsSideBar/StepSideBar.tsx b/apps/builder/components/editor/StepsSideBar/StepSideBar.tsx index 4482e6504..1203f6706 100644 --- a/apps/builder/components/editor/StepsSideBar/StepSideBar.tsx +++ b/apps/builder/components/editor/StepsSideBar/StepSideBar.tsx @@ -16,7 +16,7 @@ import { IntegrationStepType, LogicStepType, } from 'models' -import { useStepDnd } from 'contexts/StepDndContext' +import { useStepDnd } from 'contexts/GraphDndContext' import React, { useState } from 'react' import { StepCard, StepCardOverlay } from './StepCard' import { LockedIcon, UnlockedIcon } from 'assets/icons' diff --git a/apps/builder/components/editor/preview/PreviewDrawer.tsx b/apps/builder/components/editor/preview/PreviewDrawer.tsx index 842bf5627..50a3fab01 100644 --- a/apps/builder/components/editor/preview/PreviewDrawer.tsx +++ b/apps/builder/components/editor/preview/PreviewDrawer.tsx @@ -19,7 +19,7 @@ import { parseTypebotToPublicTypebot } from 'services/publicTypebot' export const PreviewDrawer = () => { const { typebot } = useTypebot() const { setRightPanel } = useEditor() - const { setPreviewingEdgeId } = useGraph() + const { setPreviewingEdge } = useGraph() const [isResizing, setIsResizing] = useState(false) const [width, setWidth] = useState(500) const [isResizeHandleVisible, setIsResizeHandleVisible] = useState(false) @@ -45,10 +45,13 @@ export const PreviewDrawer = () => { } useEventListener('mouseup', handleMouseUp) - const handleNewBlockVisible = (edgeId: string) => setPreviewingEdgeId(edgeId) - const handleRestartClick = () => setRestartKey((key) => key + 1) + const handleCloseClick = () => { + setPreviewingEdge(undefined) + setRightPanel(undefined) + } + return ( { - setRightPanel(undefined)} /> + {publicTypebot && ( @@ -89,7 +92,7 @@ export const PreviewDrawer = () => { > )} diff --git a/apps/builder/components/results/SubmissionsTable/SubmissionsTable.tsx b/apps/builder/components/results/SubmissionsTable/SubmissionsTable.tsx index b2bb0ca33..bec76199e 100644 --- a/apps/builder/components/results/SubmissionsTable/SubmissionsTable.tsx +++ b/apps/builder/components/results/SubmissionsTable/SubmissionsTable.tsx @@ -24,7 +24,7 @@ export const SubmissionsTable = ({ }: SubmissionsTableProps) => { const { publishedTypebot } = useTypebot() const columns: any = useMemo( - () => parseSubmissionsColumns(publishedTypebot), + () => (publishedTypebot ? parseSubmissionsColumns(publishedTypebot) : []), [publishedTypebot] ) diff --git a/apps/builder/components/settings/GeneralSettingsForm.tsx b/apps/builder/components/settings/GeneralSettingsForm.tsx index 80886fa43..2bea5cc4c 100644 --- a/apps/builder/components/settings/GeneralSettingsForm.tsx +++ b/apps/builder/components/settings/GeneralSettingsForm.tsx @@ -1,4 +1,4 @@ -import { Flex, FormLabel, Stack, Switch, Text } from '@chakra-ui/react' +import { Flex, FormLabel, Stack, Switch } from '@chakra-ui/react' import { GeneralSettings } from 'models' import React from 'react' diff --git a/apps/builder/components/settings/TypingEmulationForm.tsx b/apps/builder/components/settings/TypingEmulationForm.tsx index 49ad7f0ab..1490b0f6f 100644 --- a/apps/builder/components/settings/TypingEmulationForm.tsx +++ b/apps/builder/components/settings/TypingEmulationForm.tsx @@ -1,4 +1,4 @@ -import { Flex, FormLabel, Stack, Switch, Text } from '@chakra-ui/react' +import { Flex, FormLabel, Stack, Switch } from '@chakra-ui/react' import { TypingEmulation } from 'models' import React from 'react' import { isDefined } from 'utils' diff --git a/apps/builder/components/shared/Graph/Edges/DrawingEdge.tsx b/apps/builder/components/shared/Graph/Edges/DrawingEdge.tsx index 28cf0787f..1ca782338 100644 --- a/apps/builder/components/shared/Graph/Edges/DrawingEdge.tsx +++ b/apps/builder/components/shared/Graph/Edges/DrawingEdge.tsx @@ -3,7 +3,6 @@ import assert from 'assert' import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader' import { useGraph, ConnectingIds } from 'contexts/GraphContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext' -import { Target } from 'models' import React, { useMemo, useState } from 'react' import { computeConnectingEdgePath, @@ -20,28 +19,26 @@ export const DrawingEdge = () => { targetEndpoints, blocksCoordinates, } = useGraph() - const { typebot, createEdge } = useTypebot() + const { createEdge } = useTypebot() const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }) - const sourceBlock = useMemo( - () => connectingIds && typebot?.blocks.byId[connectingIds.source.blockId], - // eslint-disable-next-line react-hooks/exhaustive-deps - [connectingIds] - ) + const sourceBlockCoordinates = + blocksCoordinates && blocksCoordinates[connectingIds?.source.blockId ?? ''] + const targetBlockCoordinates = + blocksCoordinates && blocksCoordinates[connectingIds?.target?.blockId ?? ''] const sourceTop = useMemo(() => { - if (!sourceBlock || !connectingIds) return 0 + if (!connectingIds) return 0 return getEndpointTopOffset( graphPosition, sourceEndpoints, - connectingIds.source.buttonId ?? - connectingIds.source.stepId + (connectingIds.source.conditionType ?? '') + connectingIds.source.itemId ?? connectingIds.source.stepId ) // eslint-disable-next-line react-hooks/exhaustive-deps }, [graphPosition, sourceEndpoints, connectingIds]) const targetTop = useMemo(() => { - if (!sourceBlock || !connectingIds) return 0 + if (!connectingIds) return 0 return getEndpointTopOffset( graphPosition, targetEndpoints, @@ -51,35 +48,24 @@ export const DrawingEdge = () => { }, [graphPosition, targetEndpoints, connectingIds]) const path = useMemo(() => { - if ( - !sourceBlock || - !typebot || - !connectingIds || - !blocksCoordinates || - !sourceTop - ) - return `` + if (!sourceTop || !sourceBlockCoordinates) return `` - return connectingIds?.target + return targetBlockCoordinates ? computeConnectingEdgePath({ - connectingIds: connectingIds as Omit & { - target: Target - }, + sourceBlockCoordinates, + targetBlockCoordinates, sourceTop, targetTop, - blocksCoordinates, }) : computeEdgePathToMouse({ - blockPosition: blocksCoordinates.byId[sourceBlock.id], + sourceBlockCoordinates, mousePosition, sourceTop, }) }, [ - sourceBlock, - typebot, - connectingIds, - blocksCoordinates, sourceTop, + sourceBlockCoordinates, + targetBlockCoordinates, targetTop, mousePosition, ]) diff --git a/apps/builder/components/shared/Graph/Edges/Edge.tsx b/apps/builder/components/shared/Graph/Edges/Edge.tsx index 3c68e54df..fef975c69 100644 --- a/apps/builder/components/shared/Graph/Edges/Edge.tsx +++ b/apps/builder/components/shared/Graph/Edges/Edge.tsx @@ -1,5 +1,4 @@ import { Coordinates, useGraph } from 'contexts/GraphContext' -import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import React, { useMemo } from 'react' import { getAnchorsPosition, @@ -7,6 +6,7 @@ import { getEndpointTopOffset, getSourceEndpointId, } from 'services/graph' +import { Edge as EdgeProps } from 'models' export type AnchorsPositionProps = { sourcePosition: Coordinates @@ -15,28 +15,20 @@ export type AnchorsPositionProps = { totalSegments: number } -export const Edge = ({ edgeId }: { edgeId: string }) => { - const { typebot } = useTypebot() +export const Edge = ({ edge }: { edge: EdgeProps }) => { const { - previewingEdgeId, + previewingEdge, sourceEndpoints, targetEndpoints, graphPosition, blocksCoordinates, } = useGraph() - const edge = useMemo( - () => typebot?.edges.byId[edgeId], - [edgeId, typebot?.edges.byId] - ) - const isPreviewing = previewingEdgeId === edgeId - - const sourceBlock = edge && typebot?.blocks.byId[edge.from.blockId] - const targetBlock = edge && typebot?.blocks.byId[edge.to.blockId] + const isPreviewing = previewingEdge?.id === edge.id const sourceBlockCoordinates = - sourceBlock && blocksCoordinates?.byId[sourceBlock.id] + blocksCoordinates && blocksCoordinates[edge.from.blockId] const targetBlockCoordinates = - targetBlock && blocksCoordinates?.byId[targetBlock.id] + blocksCoordinates && blocksCoordinates[edge.to.blockId] const sourceTop = useMemo( () => @@ -77,6 +69,7 @@ export const Edge = ({ edgeId }: { edgeId: string }) => { if (sourceTop === 0) return <> return ( { - const { typebot } = useTypebot() - +type Props = { + edges: EdgeProps[] +} +export const Edges = ({ edges }: Props) => { return ( { top="0" > - {typebot?.edges.allIds.map((edgeId) => ( - + {edges.map((edge) => ( + ))} { - if (ranOnce || !ref.current) return - const id = source.buttonId ?? source.stepId + (source.conditionType ?? '') + if (ranOnce || !ref.current || Object.keys(blocksCoordinates).length === 0) + return + const id = source.itemId ?? source.stepId addSourceEndpoint({ id, ref, }) setRanOnce(true) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ref.current]) + }, [ref.current, blocksCoordinates]) if (!blocksCoordinates) return <> return ( diff --git a/apps/builder/components/shared/Graph/Graph.tsx b/apps/builder/components/shared/Graph/Graph.tsx index d2482013b..baa2b5af1 100644 --- a/apps/builder/components/shared/Graph/Graph.tsx +++ b/apps/builder/components/shared/Graph/Graph.tsx @@ -2,7 +2,7 @@ import { Flex, FlexProps, useEventListener } from '@chakra-ui/react' import React, { useRef, useMemo, useEffect } from 'react' import { blockWidth, useGraph } from 'contexts/GraphContext' import { BlockNode } from './Nodes/BlockNode/BlockNode' -import { useStepDnd } from 'contexts/StepDndContext' +import { useStepDnd } from 'contexts/GraphDndContext' import { Edges } from './Edges' import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader' @@ -58,6 +58,7 @@ export const Graph = ({ useEventListener('wheel', handleMouseWheel, graphContainerRef.current) const handleMouseUp = (e: MouseEvent) => { + if (!typebot) return if (!draggedStep && !draggedStepType) return const coordinates = { x: e.clientX - graphPosition.x - blockWidth / 3, @@ -69,6 +70,7 @@ export const Graph = ({ id, ...coordinates, step: draggedStep ?? (draggedStepType as DraggableStepType), + indices: { blockIndex: typebot.blocks.length, stepIndex: 0 }, }) setDraggedStep(undefined) setDraggedStepType(undefined) @@ -84,7 +86,6 @@ export const Graph = ({ const handleClick = () => setOpenedStepId(undefined) useEventListener('click', handleClick, editorContainerRef.current) - if (!typebot) return <> return ( - - {typebot.blocks.allIds.map((blockId) => ( - + + {typebot?.blocks.map((block, idx) => ( + ))} {answersCounts?.map((answersCount) => ( { +export const BlockNode = ({ block, blockIndex }: Props) => { const { connectingIds, setConnectingIds, - previewingEdgeId, + previewingEdge, blocksCoordinates, updateBlockCoordinates, isReadOnly, } = useGraph() const { typebot, updateBlock } = useTypebot() - const { setMouseOverBlockId } = useStepDnd() - const { draggedStep, draggedStepType } = useStepDnd() + const { setMouseOverBlock, mouseOverBlock } = useStepDnd() const [isMouseDown, setIsMouseDown] = useState(false) const [isConnecting, setIsConnecting] = useState(false) - const isPreviewing = useMemo(() => { - if (!previewingEdgeId) return - const edge = typebot?.edges.byId[previewingEdgeId] - return edge?.to.blockId === block.id || edge?.from.blockId === block.id - }, [block.id, previewingEdgeId, typebot?.edges.byId]) + const isPreviewing = + previewingEdge?.to.blockId === block.id || + previewingEdge?.from.blockId === block.id + const isStartBlock = + block.steps.length === 1 && block.steps[0].type === 'start' - const blockCoordinates = useMemo( - () => blocksCoordinates?.byId[block.id], - [block.id, blocksCoordinates?.byId] - ) + const blockCoordinates = blocksCoordinates[block.id] + const blockRef = useRef(null) const [debouncedBlockPosition] = useDebounce(blockCoordinates, 100) useEffect(() => { if (!debouncedBlockPosition || isReadOnly) return @@ -52,7 +51,7 @@ export const BlockNode = ({ block }: Props) => { debouncedBlockPosition.y === block.graphCoordinates.y ) return - updateBlock(block.id, { graphCoordinates: debouncedBlockPosition }) + updateBlock(blockIndex, { graphCoordinates: debouncedBlockPosition }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [debouncedBlockPosition]) @@ -63,7 +62,8 @@ export const BlockNode = ({ block }: Props) => { ) }, [block.id, connectingIds]) - const handleTitleSubmit = (title: string) => updateBlock(block.id, { title }) + const handleTitleSubmit = (title: string) => + updateBlock(blockIndex, { title }) const handleMouseDown = () => { setIsMouseDown(true) @@ -87,24 +87,26 @@ export const BlockNode = ({ block }: Props) => { useEventListener('mousemove', handleMouseMove) const handleMouseEnter = () => { - if (draggedStepType || draggedStep) setMouseOverBlockId(block.id) + if (mouseOverBlock?.id !== block.id && !isStartBlock) + setMouseOverBlock({ id: block.id, ref: blockRef }) if (connectingIds) setConnectingIds({ ...connectingIds, target: { blockId: block.id } }) } const handleMouseLeave = () => { - setMouseOverBlockId(undefined) + setMouseOverBlock(undefined) if (connectingIds) setConnectingIds({ ...connectingIds, target: undefined }) } return ( - renderMenu={() => } + renderMenu={() => } isDisabled={isReadOnly} > {(ref, isOpened) => ( { {typebot && ( - + )} )} diff --git a/apps/builder/components/shared/Graph/Nodes/BlockNode/BlockNodeContextMenu.tsx b/apps/builder/components/shared/Graph/Nodes/BlockNode/BlockNodeContextMenu.tsx index 8e404b55c..997fdec71 100644 --- a/apps/builder/components/shared/Graph/Nodes/BlockNode/BlockNodeContextMenu.tsx +++ b/apps/builder/components/shared/Graph/Nodes/BlockNode/BlockNodeContextMenu.tsx @@ -2,10 +2,14 @@ import { MenuList, MenuItem } from '@chakra-ui/react' import { TrashIcon } from 'assets/icons' import { useTypebot } from 'contexts/TypebotContext/TypebotContext' -export const BlockNodeContextMenu = ({ blockId }: { blockId: string }) => { +export const BlockNodeContextMenu = ({ + blockIndex, +}: { + blockIndex: number +}) => { const { deleteBlock } = useTypebot() - const handleDeleteClick = () => deleteBlock(blockId) + const handleDeleteClick = () => deleteBlock(blockIndex) return ( diff --git a/apps/builder/components/shared/Graph/Nodes/ButtonNode/ButtonNode.tsx b/apps/builder/components/shared/Graph/Nodes/ButtonNode/ButtonNode.tsx deleted file mode 100644 index 51e063a19..000000000 --- a/apps/builder/components/shared/Graph/Nodes/ButtonNode/ButtonNode.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { - EditablePreview, - EditableInput, - Editable, - useEventListener, - Flex, - Fade, - IconButton, -} from '@chakra-ui/react' -import { PlusIcon } from 'assets/icons' -import { ContextMenu } from 'components/shared/ContextMenu' -import { Coordinates } from 'contexts/GraphContext' -import { useTypebot } from 'contexts/TypebotContext' -import { ChoiceInputStep, ChoiceItem } from 'models' -import React, { useState } from 'react' -import { isNotDefined, isSingleChoiceInput } from 'utils' -import { SourceEndpoint } from '../../Endpoints/SourceEndpoint' -import { ButtonNodeContextMenu } from './ButtonNodeContextMenu' - -type Props = { - item: ChoiceItem - onMouseMoveBottomOfElement?: () => void - onMouseMoveTopOfElement?: () => void - onMouseDown?: ( - stepNodePosition: { absolute: Coordinates; relative: Coordinates }, - item: ChoiceItem - ) => void -} - -export const ButtonNode = ({ - item, - onMouseDown, - onMouseMoveBottomOfElement, - onMouseMoveTopOfElement, -}: Props) => { - const { deleteChoiceItem, updateChoiceItem, typebot, createChoiceItem } = - useTypebot() - const [mouseDownEvent, setMouseDownEvent] = - useState<{ absolute: Coordinates; relative: Coordinates }>() - const [isMouseOver, setIsMouseOver] = useState(false) - - const handleMouseDown = (e: React.MouseEvent) => { - if (!onMouseDown) return - e.stopPropagation() - const element = e.currentTarget as HTMLDivElement - const rect = element.getBoundingClientRect() - const relativeX = e.clientX - rect.left - const relativeY = e.clientY - rect.top - setMouseDownEvent({ - absolute: { x: e.clientX + relativeX, y: e.clientY + relativeY }, - relative: { x: relativeX, y: relativeY }, - }) - } - - const handleGlobalMouseUp = () => { - setMouseDownEvent(undefined) - } - useEventListener('mouseup', handleGlobalMouseUp) - - const handleMouseMove = (event: React.MouseEvent) => { - if (!onMouseMoveBottomOfElement || !onMouseMoveTopOfElement) return - const isMovingAndIsMouseDown = - mouseDownEvent && - onMouseDown && - (event.movementX > 0 || event.movementY > 0) - if (isMovingAndIsMouseDown) { - onMouseDown(mouseDownEvent, item) - deleteChoiceItem(item.id) - setMouseDownEvent(undefined) - } - const element = event.currentTarget as HTMLDivElement - const rect = element.getBoundingClientRect() - const y = event.clientY - rect.top - if (y > rect.height / 2) onMouseMoveBottomOfElement() - else onMouseMoveTopOfElement() - } - - const handleInputSubmit = (content: string) => - updateChoiceItem(item.id, { content: content === '' ? undefined : content }) - - const handlePlusClick = () => { - const nextIndex = - ( - typebot?.steps.byId[item.stepId] as ChoiceInputStep - ).options.itemIds.indexOf(item.id) + 1 - createChoiceItem({ stepId: item.stepId }, nextIndex) - } - - const handleMouseEnter = () => setIsMouseOver(true) - const handleMouseLeave = () => setIsMouseOver(false) - - return ( - - renderMenu={() => } - > - {(ref, isOpened) => ( - - - - - - {typebot && isSingleChoiceInput(typebot.steps.byId[item.stepId]) && ( - - )} - - } - size="xs" - shadow="md" - colorScheme="blue" - onClick={handlePlusClick} - /> - - - )} - - ) -} diff --git a/apps/builder/components/shared/Graph/Nodes/ButtonNode/ButtonNodesList.tsx b/apps/builder/components/shared/Graph/Nodes/ButtonNode/ButtonNodesList.tsx deleted file mode 100644 index 30a55425f..000000000 --- a/apps/builder/components/shared/Graph/Nodes/ButtonNode/ButtonNodesList.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { Flex, Portal, Stack, Text, useEventListener } from '@chakra-ui/react' -import { useStepDnd } from 'contexts/StepDndContext' -import { Coordinates } from 'contexts/GraphContext' -import { useTypebot } from 'contexts/TypebotContext' -import { ChoiceInputStep, ChoiceItem } from 'models' -import React, { useMemo, useState } from 'react' -import { ButtonNode } from './ButtonNode' -import { SourceEndpoint } from '../../Endpoints' -import { ButtonNodeOverlay } from './ButtonNodeOverlay' - -type ChoiceItemsListProps = { - step: ChoiceInputStep -} - -export const ButtonNodesList = ({ step }: ChoiceItemsListProps) => { - const { typebot, createChoiceItem } = useTypebot() - const { - draggedChoiceItem, - mouseOverBlockId, - setDraggedChoiceItem, - setMouseOverBlockId, - } = useStepDnd() - const showSortPlaceholders = useMemo( - () => mouseOverBlockId === step.blockId && draggedChoiceItem, - [draggedChoiceItem, mouseOverBlockId, step.blockId] - ) - const [position, setPosition] = useState({ - x: 0, - y: 0, - }) - const [relativeCoordinates, setRelativeCoordinates] = useState({ x: 0, y: 0 }) - const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState< - number | undefined - >() - - const handleStepMove = (event: MouseEvent) => { - if (!draggedChoiceItem) return - const { clientX, clientY } = event - setPosition({ - ...position, - x: clientX - relativeCoordinates.x, - y: clientY - relativeCoordinates.y, - }) - } - useEventListener('mousemove', handleStepMove) - - const handleMouseUp = (e: MouseEvent) => { - if (!draggedChoiceItem) return - if (expandedPlaceholderIndex !== -1) { - e.stopPropagation() - createChoiceItem(draggedChoiceItem, expandedPlaceholderIndex) - } - setMouseOverBlockId(undefined) - setExpandedPlaceholderIndex(undefined) - setDraggedChoiceItem(undefined) - } - useEventListener('mouseup', handleMouseUp, undefined, { capture: true }) - - const handleStepMouseDown = ( - { absolute, relative }: { absolute: Coordinates; relative: Coordinates }, - item: ChoiceItem - ) => { - setPosition(absolute) - setRelativeCoordinates(relative) - setMouseOverBlockId(typebot?.steps.byId[item.stepId].blockId) - setDraggedChoiceItem(item) - } - - const handleMouseOnTopOfStep = (stepIndex: number) => { - if (!draggedChoiceItem) return - setExpandedPlaceholderIndex(stepIndex === 0 ? 0 : stepIndex) - } - - const handleMouseOnBottomOfStep = (stepIndex: number) => { - if (!draggedChoiceItem) return - setExpandedPlaceholderIndex(stepIndex + 1) - } - - const stopPropagating = (e: React.MouseEvent) => e.stopPropagation() - - return ( - - - {step.options.itemIds.map((itemId, idx) => ( - - {typebot?.choiceItems.byId[itemId] && ( - handleMouseOnTopOfStep(idx)} - onMouseMoveBottomOfElement={() => { - handleMouseOnBottomOfStep(idx) - }} - onMouseDown={handleStepMouseDown} - /> - )} - - - ))} - - - Default - - - - - {draggedChoiceItem && draggedChoiceItem.stepId === step.id && ( - - - - )} - - ) -} diff --git a/apps/builder/components/shared/Graph/Nodes/ButtonNode/index.tsx b/apps/builder/components/shared/Graph/Nodes/ButtonNode/index.tsx deleted file mode 100644 index 5aa29c974..000000000 --- a/apps/builder/components/shared/Graph/Nodes/ButtonNode/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { ButtonNodesList } from './ButtonNodesList' diff --git a/apps/builder/components/shared/Graph/Nodes/DropOffNode.tsx b/apps/builder/components/shared/Graph/Nodes/DropOffNode.tsx index 179a34cff..4a733cbce 100644 --- a/apps/builder/components/shared/Graph/Nodes/DropOffNode.tsx +++ b/apps/builder/components/shared/Graph/Nodes/DropOffNode.tsx @@ -3,7 +3,7 @@ import { useTypebot } from 'contexts/TypebotContext' import React, { useMemo } from 'react' import { AnswersCount } from 'services/analytics' import { computeSourceCoordinates } from 'services/graph' -import { isDefined } from 'utils' +import { byId, isDefined } from 'utils' type Props = { answersCounts: AnswersCount[] @@ -21,11 +21,10 @@ export const DropOffNode = ({ answersCounts, blockId }: Props) => { const { totalDroppedUser, dropOffRate } = useMemo(() => { if (!typebot || totalAnswers === undefined) return { previousTotal: undefined, dropOffRate: undefined } - const previousBlockIds = typebot.edges.allIds - .map((edgeId) => { - const edge = typebot.edges.byId[edgeId] - return edge.to.blockId === blockId ? edge.from.blockId : undefined - }) + const previousBlockIds = typebot.edges + .map((edge) => + edge.to.blockId === blockId ? edge.from.blockId : undefined + ) .filter((blockId) => isDefined(blockId)) const previousTotal = answersCounts .filter((a) => previousBlockIds.includes(a.blockId)) @@ -41,12 +40,11 @@ export const DropOffNode = ({ answersCounts, blockId }: Props) => { }, [answersCounts, blockId, totalAnswers, typebot]) const labelCoordinates = useMemo(() => { - if (!typebot) return { x: 0, y: 0 } - const sourceBlock = typebot?.blocks.byId[blockId] + const sourceBlock = typebot?.blocks.find(byId(blockId)) if (!sourceBlock) return return computeSourceCoordinates( sourceBlock?.graphCoordinates, - sourceBlock?.stepIds.length - 1 + sourceBlock?.steps.length - 1 ) }, [blockId, typebot]) diff --git a/apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNode.tsx b/apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNode.tsx new file mode 100644 index 000000000..99076c64f --- /dev/null +++ b/apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNode.tsx @@ -0,0 +1,90 @@ +import { Flex } from '@chakra-ui/react' +import { ContextMenu } from 'components/shared/ContextMenu' +import { Coordinates } from 'contexts/GraphContext' +import { NodePosition, useDragDistance } from 'contexts/GraphDndContext' +import { useTypebot } from 'contexts/TypebotContext' +import { ButtonItem, Item, ItemIndices, ItemType } from 'models' +import React, { useRef, useState } from 'react' +import { setMultipleRefs } from 'services/utils' +import { SourceEndpoint } from '../../Endpoints/SourceEndpoint' +import { ItemNodeContent } from './ItemNodeContent' +import { ItemNodeContextMenu } from './ItemNodeContextMenu' + +type Props = { + item: Item + indices: ItemIndices + isReadOnly: boolean + isLastItem: boolean + onMouseDown?: ( + stepNodePosition: { absolute: Coordinates; relative: Coordinates }, + item: ButtonItem + ) => void +} + +export const ItemNode = ({ + item, + indices, + isReadOnly, + isLastItem, + onMouseDown, +}: Props) => { + const { typebot } = useTypebot() + const [isMouseOver, setIsMouseOver] = useState(false) + const itemRef = useRef(null) + const onDrag = (position: NodePosition) => { + if (!onMouseDown || item.type !== ItemType.BUTTON) return + onMouseDown(position, item) + } + useDragDistance({ + ref: itemRef, + onDrag, + isDisabled: !onMouseDown || item.type !== ItemType.BUTTON, + }) + + const handleMouseEnter = () => setIsMouseOver(true) + const handleMouseLeave = () => setIsMouseOver(false) + + return ( + + renderMenu={() => } + > + {(ref, isOpened) => ( + + + {typebot && ( + + )} + + )} + + ) +} diff --git a/apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNodeContent/ItemNodeContent.tsx b/apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNodeContent/ItemNodeContent.tsx new file mode 100644 index 000000000..2220ab8c6 --- /dev/null +++ b/apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNodeContent/ItemNodeContent.tsx @@ -0,0 +1,32 @@ +import { Item, ItemIndices, ItemType } from 'models' +import React from 'react' +import { ButtonNodeContent } from './contents/ButtonNodeContent' +import { ConditionNodeContent } from './contents/ConditionNodeContent' + +type Props = { + item: Item + indices: ItemIndices + isMouseOver: boolean + isLastItem: boolean +} + +export const ItemNodeContent = ({ + item, + indices, + isMouseOver, + isLastItem, +}: Props) => { + switch (item.type) { + case ItemType.BUTTON: + return ( + + ) + case ItemType.CONDITION: + return + } +} diff --git a/apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNodeContent/contents/ButtonNodeContent.tsx b/apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNodeContent/contents/ButtonNodeContent.tsx new file mode 100644 index 000000000..767071373 --- /dev/null +++ b/apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNodeContent/contents/ButtonNodeContent.tsx @@ -0,0 +1,92 @@ +import { + EditablePreview, + EditableInput, + Editable, + Fade, + IconButton, + Flex, +} from '@chakra-ui/react' +import { PlusIcon } from 'assets/icons' +import { useTypebot } from 'contexts/TypebotContext' +import { ButtonItem, ItemIndices, ItemType } from 'models' +import React, { useRef, useState } from 'react' +import { isNotDefined } from 'utils' + +type Props = { + item: ButtonItem + indices: ItemIndices + isLastItem: boolean + isMouseOver: boolean +} + +export const ButtonNodeContent = ({ + item, + indices, + isMouseOver, + isLastItem, +}: Props) => { + const { deleteItem, updateItem, createItem } = useTypebot() + const [initialContent] = useState(item.content ?? '') + const [itemValue, setItemValue] = useState(item.content ?? 'Click to edit') + const editableRef = useRef(null) + + const handleInputSubmit = () => { + if (itemValue === '') deleteItem(indices) + else + updateItem(indices, { content: itemValue === '' ? undefined : itemValue }) + } + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Escape' && itemValue === 'Click to edit') deleteItem(indices) + if ( + e.key === 'Enter' && + itemValue !== '' && + isLastItem && + initialContent === '' + ) + handlePlusClick() + } + + const handlePlusClick = () => { + const itemIndex = indices.itemIndex + 1 + createItem( + { stepId: item.stepId, type: ItemType.BUTTON }, + { ...indices, itemIndex } + ) + } + + return ( + + + + + + + } + size="xs" + shadow="md" + colorScheme="blue" + onClick={handlePlusClick} + /> + + + ) +} diff --git a/apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNodeContent/contents/ConditionNodeContent.tsx b/apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNodeContent/contents/ConditionNodeContent.tsx new file mode 100644 index 000000000..f99b3c64a --- /dev/null +++ b/apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNodeContent/contents/ConditionNodeContent.tsx @@ -0,0 +1,73 @@ +import { Stack, Tag, Text, Flex, Wrap } from '@chakra-ui/react' +import { useTypebot } from 'contexts/TypebotContext' +import { Comparison, ConditionItem, ComparisonOperators } from 'models' +import React from 'react' +import { byId, isNotDefined } from 'utils' + +type Props = { + item: ConditionItem +} + +export const ConditionNodeContent = ({ item }: Props) => { + const { typebot } = useTypebot() + return ( + + {item.content.comparisons.length === 0 || + comparisonIsEmpty(item.content.comparisons[0]) ? ( + Configure... + ) : ( + + {item.content.comparisons.map((comparison, idx) => { + const variable = typebot?.variables.find( + byId(comparison.variableId) + ) + return ( + + {idx > 0 && {item.content.logicalOperator ?? ''}} + {variable?.name && ( + + {variable.name} + + )} + {comparison.comparisonOperator && ( + + {parseComparisonOperatorSymbol( + comparison.comparisonOperator + )} + + )} + {comparison?.value && ( + + {comparison.value} + + )} + + ) + })} + + )} + + ) +} + +const comparisonIsEmpty = (comparison: Comparison) => + isNotDefined(comparison.comparisonOperator) && + isNotDefined(comparison.value) && + isNotDefined(comparison.variableId) + +const parseComparisonOperatorSymbol = (operator: ComparisonOperators) => { + switch (operator) { + case ComparisonOperators.CONTAINS: + return 'contains' + case ComparisonOperators.EQUAL: + return '=' + case ComparisonOperators.GREATER: + return '>' + case ComparisonOperators.IS_SET: + return 'is set' + case ComparisonOperators.LESS: + return '<' + case ComparisonOperators.NOT_EQUAL: + return '!=' + } +} diff --git a/apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNodeContent/index.tsx b/apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNodeContent/index.tsx new file mode 100644 index 000000000..676a3af81 --- /dev/null +++ b/apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNodeContent/index.tsx @@ -0,0 +1 @@ +export { ItemNodeContent } from './ItemNodeContent' diff --git a/apps/builder/components/shared/Graph/Nodes/ButtonNode/ButtonNodeContextMenu.tsx b/apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNodeContextMenu.tsx similarity index 57% rename from apps/builder/components/shared/Graph/Nodes/ButtonNode/ButtonNodeContextMenu.tsx rename to apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNodeContextMenu.tsx index 5b7108a14..412674b62 100644 --- a/apps/builder/components/shared/Graph/Nodes/ButtonNode/ButtonNodeContextMenu.tsx +++ b/apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNodeContextMenu.tsx @@ -1,11 +1,15 @@ import { MenuList, MenuItem } from '@chakra-ui/react' import { TrashIcon } from 'assets/icons' import { useTypebot } from 'contexts/TypebotContext/TypebotContext' +import { ItemIndices } from 'models' -export const ButtonNodeContextMenu = ({ itemId }: { itemId: string }) => { - const { deleteChoiceItem } = useTypebot() +type Props = { + indices: ItemIndices +} +export const ItemNodeContextMenu = ({ indices }: Props) => { + const { deleteItem } = useTypebot() - const handleDeleteClick = () => deleteChoiceItem(itemId) + const handleDeleteClick = () => deleteItem(indices) return ( diff --git a/apps/builder/components/shared/Graph/Nodes/ButtonNode/ButtonNodeOverlay.tsx b/apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNodeOverlay.tsx similarity index 76% rename from apps/builder/components/shared/Graph/Nodes/ButtonNode/ButtonNodeOverlay.tsx rename to apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNodeOverlay.tsx index dafff71ae..09068a963 100644 --- a/apps/builder/components/shared/Graph/Nodes/ButtonNode/ButtonNodeOverlay.tsx +++ b/apps/builder/components/shared/Graph/Nodes/ItemNode/ItemNodeOverlay.tsx @@ -1,12 +1,12 @@ import { Flex, FlexProps } from '@chakra-ui/react' -import { ChoiceItem } from 'models' +import { Item } from 'models' import React from 'react' type Props = { - item: ChoiceItem + item: Item } & FlexProps -export const ButtonNodeOverlay = ({ item, ...props }: Props) => { +export const ItemNodeOverlay = ({ item, ...props }: Props) => { return ( { + const { typebot, createItem, deleteItem } = useTypebot() + const { draggedItem, setDraggedItem, mouseOverBlock } = useStepDnd() + const placeholderRefs = useRef([]) + const blockId = typebot?.blocks[blockIndex].id + const isDraggingOnCurrentBlock = + (draggedItem && mouseOverBlock?.id === blockId) ?? false + const showPlaceholders = draggedItem && !isReadOnly + + const [position, setPosition] = useState({ + x: 0, + y: 0, + }) + const [relativeCoordinates, setRelativeCoordinates] = useState({ x: 0, y: 0 }) + const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState< + number | undefined + >() + + const handleGlobalMouseMove = (event: MouseEvent) => { + if (!draggedItem || draggedItem.stepId !== step.id) return + const { clientX, clientY } = event + setPosition({ + ...position, + x: clientX - relativeCoordinates.x, + y: clientY - relativeCoordinates.y, + }) + } + useEventListener('mousemove', handleGlobalMouseMove) + + useEffect(() => { + if (mouseOverBlock?.id !== step.blockId) + setExpandedPlaceholderIndex(undefined) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mouseOverBlock?.id]) + + const handleMouseMoveOnBlock = (event: MouseEvent) => { + if (!isDraggingOnCurrentBlock || isReadOnly) return + const index = computeNearestPlaceholderIndex(event.pageY, placeholderRefs) + setExpandedPlaceholderIndex(index) + } + useEventListener( + 'mousemove', + handleMouseMoveOnBlock, + mouseOverBlock?.ref.current + ) + + const handleMouseUpOnBlock = (e: MouseEvent) => { + setExpandedPlaceholderIndex(undefined) + if (!isDraggingOnCurrentBlock) return + const itemIndex = computeNearestPlaceholderIndex(e.pageY, placeholderRefs) + e.stopPropagation() + createItem(draggedItem as ButtonItem, { + blockIndex, + stepIndex, + itemIndex, + }) + setDraggedItem(undefined) + } + useEventListener( + 'mouseup', + handleMouseUpOnBlock, + mouseOverBlock?.ref.current, + { + capture: true, + } + ) + + const handleStepMouseDown = + (itemIndex: number) => + ( + { absolute, relative }: { absolute: Coordinates; relative: Coordinates }, + item: ButtonItem + ) => { + if (!typebot || isReadOnly) return + deleteItem({ blockIndex, stepIndex, itemIndex }) + setPosition(absolute) + setRelativeCoordinates(relative) + setDraggedItem(item) + } + + const stopPropagating = (e: React.MouseEvent) => e.stopPropagation() + + const handlePushElementRef = + (idx: number) => (elem: HTMLDivElement | null) => { + elem && (placeholderRefs.current[idx] = elem) + } + + return ( + + + {step.items.map((item, idx) => ( + + + + + ))} + + + Default + + + + + {draggedItem && draggedItem.stepId === step.id && ( + + + + )} + + ) +} diff --git a/apps/builder/components/shared/Graph/Nodes/ItemNode/index.tsx b/apps/builder/components/shared/Graph/Nodes/ItemNode/index.tsx new file mode 100644 index 000000000..b2d18396c --- /dev/null +++ b/apps/builder/components/shared/Graph/Nodes/ItemNode/index.tsx @@ -0,0 +1 @@ +export { ItemNodesList } from './ItemNodesList' diff --git a/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/SettingsPopoverContent.tsx b/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/SettingsPopoverContent.tsx index d20d275ef..ac2778873 100644 --- a/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/SettingsPopoverContent.tsx +++ b/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/SettingsPopoverContent.tsx @@ -8,13 +8,17 @@ import { } from '@chakra-ui/react' import { ExpandIcon } from 'assets/icons' import { + ConditionItem, + ConditionStep, InputStepType, IntegrationStepType, LogicStepType, Step, + StepIndices, StepOptions, TextBubbleStep, Webhook, + WebhookStep, } from 'models' import { useRef } from 'react' import { @@ -37,9 +41,9 @@ type Props = { step: Exclude webhook?: Webhook onExpandClick: () => void - onOptionsChange: (options: StepOptions) => void - onWebhookChange: (updates: Partial) => void + onStepChange: (updates: Partial) => void onTestRequestClick: () => void + indices: StepIndices } export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => { @@ -79,23 +83,35 @@ export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => { export const StepSettings = ({ step, - webhook, - onOptionsChange, - onWebhookChange, + onStepChange, onTestRequestClick, + indices, }: { step: Step webhook?: Webhook - onOptionsChange: (options: StepOptions) => void - onWebhookChange: (updates: Partial) => void + onStepChange: (step: Partial) => void onTestRequestClick: () => void + indices: StepIndices }) => { + const handleOptionsChange = (options: StepOptions) => { + onStepChange({ options } as Partial) + } + const handleWebhookChange = (updates: Partial) => { + onStepChange({ + webhook: { ...(step as WebhookStep).webhook, ...updates }, + } as Partial) + } + const handleItemChange = (updates: Partial) => { + onStepChange({ + items: [{ ...(step as ConditionStep).items[0], ...updates }], + } as Partial) + } switch (step.type) { case InputStepType.TEXT: { return ( ) } @@ -103,7 +119,7 @@ export const StepSettings = ({ return ( ) } @@ -111,7 +127,7 @@ export const StepSettings = ({ return ( ) } @@ -119,7 +135,7 @@ export const StepSettings = ({ return ( ) } @@ -127,7 +143,7 @@ export const StepSettings = ({ return ( ) } @@ -135,7 +151,7 @@ export const StepSettings = ({ return ( ) } @@ -143,7 +159,7 @@ export const StepSettings = ({ return ( ) } @@ -151,23 +167,20 @@ export const StepSettings = ({ return ( ) } case LogicStepType.CONDITION: { return ( - + ) } case LogicStepType.REDIRECT: { return ( ) } @@ -175,7 +188,7 @@ export const StepSettings = ({ return ( ) @@ -184,18 +197,18 @@ export const StepSettings = ({ return ( ) } case IntegrationStepType.WEBHOOK: { return ( ) } diff --git a/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ConditonSettingsBody.tsx b/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ConditonSettingsBody.tsx index add776d11..32da4ad2c 100644 --- a/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ConditonSettingsBody.tsx +++ b/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/ConditionSettingsBody/ConditonSettingsBody.tsx @@ -1,33 +1,40 @@ import { Flex } from '@chakra-ui/react' import { DropdownList } from 'components/shared/DropdownList' import { TableList } from 'components/shared/TableList' -import { Comparison, ConditionOptions, LogicalOperator, Table } from 'models' +import { + Comparison, + ConditionItem, + ConditionStep, + LogicalOperator, +} from 'models' import React from 'react' import { ComparisonItem } from './ComparisonsItem' type ConditionSettingsBodyProps = { - options: ConditionOptions - onOptionsChange: (options: ConditionOptions) => void + step: ConditionStep + onItemChange: (updates: Partial) => void } export const ConditionSettingsBody = ({ - options, - onOptionsChange, + step, + onItemChange, }: ConditionSettingsBodyProps) => { - const handleComparisonsChange = (comparisons: Table) => - onOptionsChange({ ...options, comparisons }) + const itemContent = step.items[0].content + + const handleComparisonsChange = (comparisons: Comparison[]) => + onItemChange({ content: { ...itemContent, comparisons } }) const handleLogicalOperatorChange = (logicalOperator: LogicalOperator) => - onOptionsChange({ ...options, logicalOperator }) + onItemChange({ content: { ...itemContent, logicalOperator } }) return ( - initialItems={options.comparisons} + initialItems={itemContent.comparisons} onItemsChange={handleComparisonsChange} Item={ComparisonItem} ComponentBetweenItems={() => ( - currentItem={options.logicalOperator} + currentItem={itemContent.logicalOperator} onItemSelect={handleLogicalOperatorChange} items={Object.values(LogicalOperator)} /> diff --git a/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/GoogleSheetsSettingsBody.tsx b/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/GoogleSheetsSettingsBody.tsx index d4ebf90d8..cdfe322bb 100644 --- a/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/GoogleSheetsSettingsBody.tsx +++ b/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/GoogleSheetsSettingsBody/GoogleSheetsSettingsBody.tsx @@ -6,14 +6,12 @@ import { useTypebot } from 'contexts/TypebotContext' import { CredentialsType } from 'db' import { Cell, - defaultTable, ExtractingCell, GoogleSheetsAction, GoogleSheetsGetOptions, GoogleSheetsInsertRowOptions, GoogleSheetsOptions, GoogleSheetsUpdateRowOptions, - Table, } from 'models' import React, { useMemo } from 'react' import { @@ -60,7 +58,7 @@ export const GoogleSheetsSettingsBody = ({ const newOptions: GoogleSheetsGetOptions = { ...options, action, - cellsToExtract: defaultTable, + cellsToExtract: [], } return onOptionsChange({ ...newOptions }) } @@ -68,7 +66,7 @@ export const GoogleSheetsSettingsBody = ({ const newOptions: GoogleSheetsInsertRowOptions = { ...options, action, - cellsToInsert: defaultTable, + cellsToInsert: [], } return onOptionsChange({ ...newOptions }) } @@ -76,7 +74,7 @@ export const GoogleSheetsSettingsBody = ({ const newOptions: GoogleSheetsUpdateRowOptions = { ...options, action, - cellsToUpsert: defaultTable, + cellsToUpsert: [], } return onOptionsChange({ ...newOptions }) } @@ -155,16 +153,16 @@ const ActionOptions = ({ sheet: Sheet onOptionsChange: (options: GoogleSheetsOptions) => void }) => { - const handleInsertColumnsChange = (cellsToInsert: Table) => + const handleInsertColumnsChange = (cellsToInsert: Cell[]) => onOptionsChange({ ...options, cellsToInsert } as GoogleSheetsOptions) - const handleUpsertColumnsChange = (cellsToUpsert: Table) => + const handleUpsertColumnsChange = (cellsToUpsert: Cell[]) => onOptionsChange({ ...options, cellsToUpsert } as GoogleSheetsOptions) const handleReferenceCellChange = (referenceCell: Cell) => onOptionsChange({ ...options, referenceCell } as GoogleSheetsOptions) - const handleExtractingCellsChange = (cellsToExtract: Table) => + const handleExtractingCellsChange = (cellsToExtract: ExtractingCell[]) => onOptionsChange({ ...options, cellsToExtract } as GoogleSheetsOptions) const UpdatingCellItem = useMemo( @@ -194,9 +192,8 @@ const ActionOptions = ({ Row to select Cells to update @@ -213,9 +210,8 @@ const ActionOptions = ({ Row to select Cells to extract diff --git a/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/RedirectSettings.tsx b/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/RedirectSettings.tsx index 5f927d568..e091572ba 100644 --- a/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/RedirectSettings.tsx +++ b/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/RedirectSettings.tsx @@ -1,5 +1,4 @@ import { FormLabel, Stack } from '@chakra-ui/react' -import { DebouncedInput } from 'components/shared/DebouncedInput' import { SwitchWithLabel } from 'components/shared/SwitchWithLabel' import { InputWithVariableButton } from 'components/shared/TextboxWithVariableButton' import { RedirectOptions } from 'models' diff --git a/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/WebhookSettings/KeyValueInputs.tsx b/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/WebhookSettings/KeyValueInputs.tsx index dba14789a..8af631cba 100644 --- a/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/WebhookSettings/KeyValueInputs.tsx +++ b/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/WebhookSettings/KeyValueInputs.tsx @@ -20,7 +20,6 @@ export const HeadersInputs = (props: TableListItemProps) => ( ) export const KeyValueInputs = ({ - id, item, onItemChange, keyPlaceholder, @@ -40,18 +39,18 @@ export const KeyValueInputs = ({ return ( - Key: + Key: - Value: + Value: ) => { @@ -18,17 +17,17 @@ export const VariableForTestInputs = ({ return ( - Variable name: + Variable name: - Test value: + Test value: diff --git a/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/WebhookSettings/WebhookSettings.tsx b/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/WebhookSettings/WebhookSettings.tsx index ba47544af..2d95591d0 100644 --- a/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/WebhookSettings/WebhookSettings.tsx +++ b/apps/builder/components/shared/Graph/Nodes/StepNode/SettingsPopoverContent/bodies/WebhookSettings/WebhookSettings.tsx @@ -15,11 +15,12 @@ import { useTypebot } from 'contexts/TypebotContext' import { HttpMethod, KeyValue, - Table, WebhookOptions, VariableForTest, Webhook, ResponseVariableMapping, + WebhookStep, + StepIndices, } from 'models' import { DropdownList } from 'components/shared/DropdownList' import { TableList, TableListItemProps } from 'components/shared/TableList' @@ -34,19 +35,19 @@ import { VariableForTestInputs } from './VariableForTestInputs' import { DataVariableInputs } from './ResponseMappingInputs' type Props = { - webhook: Webhook - options?: WebhookOptions + step: WebhookStep onOptionsChange: (options: WebhookOptions) => void onWebhookChange: (updates: Partial) => void onTestRequestClick: () => void + indices: StepIndices } export const WebhookSettings = ({ - options, + step: { webhook, options }, onOptionsChange, - webhook, onWebhookChange, onTestRequestClick, + indices, }: Props) => { const { typebot, save } = useTypebot() const [isTestResponseLoading, setIsTestResponseLoading] = useState(false) @@ -62,23 +63,23 @@ export const WebhookSettings = ({ const handleMethodChange = (method: HttpMethod) => onWebhookChange({ method }) - const handleQueryParamsChange = (queryParams: Table) => + const handleQueryParamsChange = (queryParams: KeyValue[]) => onWebhookChange({ queryParams }) - const handleHeadersChange = (headers: Table) => + const handleHeadersChange = (headers: KeyValue[]) => onWebhookChange({ headers }) const handleBodyChange = (body: string) => onWebhookChange({ body }) - const handleVariablesChange = (variablesForTest: Table) => - options && onOptionsChange({ ...options, variablesForTest }) + const handleVariablesChange = (variablesForTest: VariableForTest[]) => + onOptionsChange({ ...options, variablesForTest }) const handleResponseMappingChange = ( - responseVariableMapping: Table - ) => options && onOptionsChange({ ...options, responseVariableMapping }) + responseVariableMapping: ResponseVariableMapping[] + ) => onOptionsChange({ ...options, responseVariableMapping }) const handleTestRequestClick = async () => { - if (!typebot || !webhook) return + if (!typebot) return setIsTestResponseLoading(true) onTestRequestClick() await save() @@ -86,9 +87,10 @@ export const WebhookSettings = ({ typebot.id, webhook.id, convertVariableForTestToVariables( - options?.variablesForTest, + options.variablesForTest, typebot.variables - ) + ), + indices ) if (error) return toast({ title: error.name, description: error.message }) setTestResponse(JSON.stringify(data, undefined, 2)) @@ -196,9 +198,7 @@ export const WebhookSettings = ({ - initialItems={ - options?.responseVariableMapping ?? { byId: {}, allIds: [] } - } + initialItems={options.responseVariableMapping} onItemsChange={handleResponseMappingChange} Item={ResponseMappingInputs} addLabel="Add an entry" diff --git a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNode.tsx b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNode.tsx index 3d3313b8c..9d84e4729 100644 --- a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNode.tsx +++ b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNode.tsx @@ -4,21 +4,19 @@ import { Popover, PopoverTrigger, useDisclosure, - useEventListener, } from '@chakra-ui/react' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { BubbleStep, BubbleStepContent, DraggableStep, Step, - StepOptions, + TextBubbleContent, TextBubbleStep, - Webhook, } from 'models' -import { Coordinates, useGraph } from 'contexts/GraphContext' +import { useGraph } from 'contexts/GraphContext' import { StepIcon } from 'components/editor/StepsSideBar/StepIcon' -import { isBubbleStep, isTextBubbleStep, isWebhookStep } from 'utils' +import { isBubbleStep, isTextBubbleStep, stepHasItems } from 'utils' import { StepNodeContent } from './StepNodeContent/StepNodeContent' import { useTypebot } from 'contexts/TypebotContext' import { ContextMenu } from 'components/shared/ContextMenu' @@ -32,46 +30,41 @@ import { StepSettings } from './SettingsPopoverContent/SettingsPopoverContent' import { TextBubbleEditor } from './TextBubbleEditor' import { TargetEndpoint } from '../../Endpoints' import { MediaBubblePopoverContent } from './MediaBubblePopoverContent' +import { NodePosition, useDragDistance } from 'contexts/GraphDndContext' +import { setMultipleRefs } from 'services/utils' export const StepNode = ({ step, isConnectable, - onMouseMoveBottomOfElement, - onMouseMoveTopOfElement, + indices, onMouseDown, }: { step: Step isConnectable: boolean - onMouseMoveBottomOfElement?: () => void - onMouseMoveTopOfElement?: () => void - onMouseDown?: ( - stepNodePosition: { absolute: Coordinates; relative: Coordinates }, - step: DraggableStep - ) => void + indices: { stepIndex: number; blockIndex: number } + onMouseDown?: (stepNodePosition: NodePosition, step: DraggableStep) => void }) => { const { query } = useRouter() - const { - setConnectingIds, - connectingIds, - openedStepId, - setOpenedStepId, - blocksCoordinates, - } = useGraph() - const { detachStepFromBlock, updateStep, typebot, updateWebhook } = - useTypebot() + const { setConnectingIds, connectingIds, openedStepId, setOpenedStepId } = + useGraph() + const { updateStep } = useTypebot() const [localStep, setLocalStep] = useState(step) - const [localWebhook, setLocalWebhook] = useState( - isWebhookStep(step) - ? typebot?.webhooks.byId[step.options.webhookId ?? ''] - : undefined - ) const [isConnecting, setIsConnecting] = useState(false) const [isPopoverOpened, setIsPopoverOpened] = useState( openedStepId === step.id ) + const stepRef = useRef(null) + + const onDrag = (position: NodePosition) => { + if (step.type === 'start' || !onMouseDown) return + onMouseDown(position, step) + } + useDragDistance({ + ref: stepRef, + onDrag, + isDisabled: !onMouseDown || step.type === 'start', + }) - const [mouseDownEvent, setMouseDownEvent] = - useState<{ absolute: Coordinates; relative: Coordinates }>() const [isEditing, setIsEditing] = useState( isTextBubbleStep(step) && step.content.plainText === '' ) @@ -98,15 +91,15 @@ export const StepNode = ({ }, [connectingIds, step.blockId, step.id]) const handleModalClose = () => { - updateStep(localStep.id, { ...localStep }) + updateStep(indices, { ...localStep }) onModalClose() } const handleMouseEnter = () => { - if (connectingIds?.target) + if (connectingIds) setConnectingIds({ ...connectingIds, - target: { ...connectingIds.target, stepId: step.id }, + target: { blockId: step.blockId, stepId: step.id }, }) } @@ -118,54 +111,16 @@ export const StepNode = ({ }) } - const handleMouseDown = (e: React.MouseEvent) => { - if (!onMouseDown) return - e.stopPropagation() - const element = e.currentTarget as HTMLDivElement - const rect = element.getBoundingClientRect() - const relativeX = e.clientX - rect.left - const relativeY = e.clientY - rect.top - setMouseDownEvent({ - absolute: { x: e.clientX + relativeX, y: e.clientY + relativeY }, - relative: { x: relativeX, y: relativeY }, - }) - } - - const handleGlobalMouseUp = () => { - setMouseDownEvent(undefined) - } - useEventListener('mouseup', handleGlobalMouseUp) - - const handleMouseUp = () => { - if (mouseDownEvent) { - setIsEditing(true) - } - } - - const handleMouseMove = (event: React.MouseEvent) => { - if (!onMouseMoveBottomOfElement || !onMouseMoveTopOfElement) return - const isMovingAndIsMouseDown = - mouseDownEvent && - onMouseDown && - (event.movementX > 0 || event.movementY > 0) - if (isMovingAndIsMouseDown && step.type !== 'start') { - onMouseDown(mouseDownEvent, step) - detachStepFromBlock(step.id) - setMouseDownEvent(undefined) - } - const element = event.currentTarget as HTMLDivElement - const rect = element.getBoundingClientRect() - const y = event.clientY - rect.top - if (y > rect.height / 2) onMouseMoveBottomOfElement() - else onMouseMoveTopOfElement() - } - - const handleCloseEditor = () => { + const handleCloseEditor = (content: TextBubbleContent) => { + const updatedStep = { ...localStep, content } as Step + setLocalStep(updatedStep) + updateStep(indices, updatedStep) setIsEditing(false) } const handleClick = (e: React.MouseEvent) => { e.stopPropagation() + if (isTextBubbleStep(step)) setIsEditing(true) setOpenedStepId(step.id) } @@ -175,22 +130,16 @@ export const StepNode = ({ } const updateOptions = () => { - updateStep(localStep.id, { ...localStep }) - if (localWebhook) updateWebhook(localWebhook.id, { ...localWebhook }) + updateStep(indices, { ...localStep }) } - const handleOptionsChange = (options: StepOptions) => { - setLocalStep({ ...localStep, options } as Step) + const handleStepChange = (updates: Partial) => { + setLocalStep({ ...localStep, ...updates } as Step) } const handleContentChange = (content: BubbleStepContent) => setLocalStep({ ...localStep, content } as Step) - const handleWebhookChange = (updates: Partial) => { - if (!localWebhook) return - setLocalWebhook({ ...localWebhook, ...updates }) - } - useEffect(() => { if (isPopoverOpened && openedStepId !== step.id) updateOptions() setIsPopoverOpened(openedStepId === step.id) @@ -199,13 +148,12 @@ export const StepNode = ({ return isEditing && isTextBubbleStep(localStep) ? ( ) : ( - renderMenu={() => } + renderMenu={() => } > {(ref, isOpened) => ( - + - {blocksCoordinates && - isConnectable && - hasDefaultConnector(localStep) && ( - - )} + {isConnectable && hasDefaultConnector(localStep) && ( + + )} {hasSettingsPopover(localStep) && ( )} {isMediaBubbleStep(localStep) && ( @@ -286,10 +228,9 @@ export const StepNode = ({ diff --git a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/StepNodeContent.tsx b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/StepNodeContent.tsx index 7c2df0738..a16b2c461 100644 --- a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/StepNodeContent.tsx +++ b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/StepNodeContent.tsx @@ -6,11 +6,11 @@ import { InputStepType, LogicStepType, IntegrationStepType, + StepIndices, } from 'models' import { isInputStep } from 'utils' -import { ButtonNodesList } from '../../ButtonNode' +import { ItemNodesList } from '../../ItemNode' import { - ConditionContent, SetVariableContent, TextBubbleContent, VideoBubbleContent, @@ -23,9 +23,9 @@ import { PlaceholderContent } from './contents/PlaceholderContent' type Props = { step: Step | StartStep - isConnectable?: boolean + indices: StepIndices } -export const StepNodeContent = ({ step }: Props) => { +export const StepNodeContent = ({ step, indices }: Props) => { if (isInputStep(step) && step.options.variableId) { return } @@ -52,13 +52,13 @@ export const StepNodeContent = ({ step }: Props) => { return Pick a date... } case InputStepType.CHOICE: { - return + return } case LogicStepType.SET_VARIABLE: { return } case LogicStepType.CONDITION: { - return + return } case LogicStepType.REDIRECT: { return ( diff --git a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/contents/ConditionContent.tsx b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/contents/ConditionContent.tsx deleted file mode 100644 index 379e6e34a..000000000 --- a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/contents/ConditionContent.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Flex, Stack, HStack, Tag, Text } from '@chakra-ui/react' -import { useTypebot } from 'contexts/TypebotContext' -import { ConditionStep } from 'models' -import { SourceEndpoint } from '../../../../Endpoints/SourceEndpoint' - -export const ConditionContent = ({ step }: { step: ConditionStep }) => { - const { typebot } = useTypebot() - return ( - - {step.options?.comparisons.allIds.length === 0 ? ( - Configure... - ) : ( - - {step.options?.comparisons.allIds.map((comparisonId, idx) => { - const comparison = step.options?.comparisons.byId[comparisonId] - const variable = - typebot?.variables.byId[comparison?.variableId ?? ''] - return ( - - {idx > 0 && {step.options?.logicalOperator ?? ''}} - {variable?.name && ( - {variable.name} - )} - {comparison.comparisonOperator && ( - {comparison?.comparisonOperator} - )} - {comparison?.value && ( - {comparison.value} - )} - - ) - })} - - )} - - - - ) -} diff --git a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/contents/SetVariableContent.tsx b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/contents/SetVariableContent.tsx index a42313722..a2486cf15 100644 --- a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/contents/SetVariableContent.tsx +++ b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/contents/SetVariableContent.tsx @@ -1,12 +1,13 @@ import { Text } from '@chakra-ui/react' import { useTypebot } from 'contexts/TypebotContext' import { SetVariableStep } from 'models' +import { byId } from 'utils' export const SetVariableContent = ({ step }: { step: SetVariableStep }) => { const { typebot } = useTypebot() const variableName = - typebot?.variables.byId[step.options?.variableId ?? '']?.name ?? '' - const expression = step.options?.expressionToEvaluate ?? '' + typebot?.variables.find(byId(step.options.variableId))?.name ?? '' + const expression = step.options.expressionToEvaluate ?? '' return ( {variableName === '' && expression === '' diff --git a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/contents/TextBubbleContent.tsx b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/contents/TextBubbleContent.tsx index 8448326c4..7844ed093 100644 --- a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/contents/TextBubbleContent.tsx +++ b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/contents/TextBubbleContent.tsx @@ -10,6 +10,7 @@ type Props = { export const TextBubbleContent = ({ step }: Props) => { const { typebot } = useTypebot() + if (!typebot) return <> return ( { - const { typebot } = useTypebot() - const webhook = useMemo( - () => typebot?.webhooks.byId[step.options?.webhookId ?? ''], - [step.options?.webhookId, typebot?.webhooks.byId] - ) +export const WebhookContent = ({ step: { webhook } }: Props) => { if (!webhook?.url) return Configure... return ( diff --git a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/contents/WithVariableContent.tsx b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/contents/WithVariableContent.tsx index 96292d8cf..91149114f 100644 --- a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/contents/WithVariableContent.tsx +++ b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/contents/WithVariableContent.tsx @@ -2,6 +2,7 @@ import { InputStep } from 'models' import { chakra, Text } from '@chakra-ui/react' import React from 'react' import { useTypebot } from 'contexts/TypebotContext' +import { byId } from 'utils' type Props = { step: InputStep @@ -9,8 +10,10 @@ type Props = { export const WithVariableContent = ({ step }: Props) => { const { typebot } = useTypebot() - const variableName = - typebot?.variables.byId[step.options.variableId as string].name + const variableName = typebot?.variables.find( + byId(step.options.variableId) + )?.name + return ( Collect{' '} diff --git a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/contents/index.tsx b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/contents/index.tsx index 8e745336f..8f8675e38 100644 --- a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/contents/index.tsx +++ b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContent/contents/index.tsx @@ -1,4 +1,3 @@ -export * from './ConditionContent' export * from './SetVariableContent' export * from './WithVariableContent' export * from './VideoBubbleContent' diff --git a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContextMenu.tsx b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContextMenu.tsx index a50d3860b..1acfbe1ff 100644 --- a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContextMenu.tsx +++ b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContextMenu.tsx @@ -1,11 +1,13 @@ import { MenuList, MenuItem } from '@chakra-ui/react' import { TrashIcon } from 'assets/icons' import { useTypebot } from 'contexts/TypebotContext/TypebotContext' +import { StepIndices } from 'models' -export const StepNodeContextMenu = ({ stepId }: { stepId: string }) => { +type Props = { indices: StepIndices } +export const StepNodeContextMenu = ({ indices }: Props) => { const { deleteStep } = useTypebot() - const handleDeleteClick = () => deleteStep(stepId) + const handleDeleteClick = () => deleteStep(indices) return ( diff --git a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeOverlay.tsx b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeOverlay.tsx index 5d0178039..78c39b24b 100644 --- a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeOverlay.tsx +++ b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeOverlay.tsx @@ -1,12 +1,13 @@ import { StackProps, HStack } from '@chakra-ui/react' -import { StartStep, Step } from 'models' +import { StartStep, Step, StepIndices } from 'models' import { StepIcon } from 'components/editor/StepsSideBar/StepIcon' import { StepNodeContent } from './StepNodeContent/StepNodeContent' export const StepNodeOverlay = ({ step, + indices, ...props -}: { step: Step | StartStep } & StackProps) => { +}: { step: Step | StartStep; indices: StepIndices } & StackProps) => { return ( - + ) } diff --git a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodesList.tsx b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodesList.tsx index 5cf84dc22..aa91a4eb3 100644 --- a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodesList.tsx +++ b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodesList.tsx @@ -1,105 +1,123 @@ import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react' -import { DraggableStep } from 'models' -import { useStepDnd } from 'contexts/StepDndContext' +import { DraggableStep, DraggableStepType, Step } from 'models' +import { + computeNearestPlaceholderIndex, + useStepDnd, +} from 'contexts/GraphDndContext' import { Coordinates, useGraph } from 'contexts/GraphContext' -import { useMemo, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useTypebot } from 'contexts/TypebotContext' import { StepNode } from './StepNode' import { StepNodeOverlay } from './StepNodeOverlay' +type Props = { + blockId: string + steps: Step[] + blockIndex: number + blockRef: React.MutableRefObject + isStartBlock: boolean +} export const StepNodesList = ({ blockId, - stepIds, -}: { - blockId: string - stepIds: string[] -}) => { + steps, + blockIndex, + blockRef, + isStartBlock, +}: Props) => { const { draggedStep, setDraggedStep, draggedStepType, - mouseOverBlockId, + mouseOverBlock, setDraggedStepType, - setMouseOverBlockId, } = useStepDnd() - const { typebot, createStep } = useTypebot() + const { typebot, createStep, detachStepFromBlock } = useTypebot() const { isReadOnly } = useGraph() const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState< number | undefined >() - const showSortPlaceholders = useMemo( - () => mouseOverBlockId === blockId && (draggedStep || draggedStepType), - [mouseOverBlockId, blockId, draggedStep, draggedStepType] - ) + const placeholderRefs = useRef([]) const [position, setPosition] = useState({ x: 0, y: 0, }) - const [relativeCoordinates, setRelativeCoordinates] = useState({ x: 0, y: 0 }) + const [mousePositionInElement, setMousePositionInElement] = useState({ + x: 0, + y: 0, + }) + const isDraggingOnCurrentBlock = + (draggedStep || draggedStepType) && mouseOverBlock?.id === blockId + const showSortPlaceholders = !isStartBlock && (draggedStep || draggedStepType) - const handleStepMove = (event: MouseEvent) => { - if (!draggedStep) return + useEffect(() => { + if (mouseOverBlock?.id !== blockId) setExpandedPlaceholderIndex(undefined) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mouseOverBlock?.id]) + + const handleMouseMoveGlobal = (event: MouseEvent) => { + if (!draggedStep || draggedStep.blockId !== blockId) return const { clientX, clientY } = event setPosition({ - ...position, - x: clientX - relativeCoordinates.x, - y: clientY - relativeCoordinates.y, + x: clientX - mousePositionInElement.x, + y: clientY - mousePositionInElement.y, }) } - useEventListener('mousemove', handleStepMove) + useEventListener('mousemove', handleMouseMoveGlobal) - const handleMouseMove = (event: React.MouseEvent) => { - if (!draggedStep) return - const element = event.currentTarget as HTMLDivElement - const rect = element.getBoundingClientRect() - const y = event.clientY - rect.top - if (y < 20) setExpandedPlaceholderIndex(0) + const handleMouseMoveOnBlock = (event: MouseEvent) => { + if (!isDraggingOnCurrentBlock) return + setExpandedPlaceholderIndex( + computeNearestPlaceholderIndex(event.pageY, placeholderRefs) + ) } + useEventListener('mousemove', handleMouseMoveOnBlock, blockRef.current) - const handleMouseUp = (e: React.MouseEvent) => { - if (expandedPlaceholderIndex === undefined) return - e.stopPropagation() - setMouseOverBlockId(undefined) + const handleMouseUpOnBlock = (e: MouseEvent) => { setExpandedPlaceholderIndex(undefined) - if (!draggedStep && !draggedStepType) return + if (!isDraggingOnCurrentBlock) return + const stepIndex = computeNearestPlaceholderIndex(e.clientY, placeholderRefs) createStep( blockId, - draggedStep || draggedStepType, - expandedPlaceholderIndex + (draggedStep || draggedStepType) as DraggableStep | DraggableStepType, + { + blockIndex, + stepIndex, + } ) setDraggedStep(undefined) setDraggedStepType(undefined) } + useEventListener( + 'mouseup', + handleMouseUpOnBlock, + mouseOverBlock?.ref.current, + { + capture: true, + } + ) - const handleStepMouseDown = ( - { absolute, relative }: { absolute: Coordinates; relative: Coordinates }, - step: DraggableStep - ) => { - if (isReadOnly) return - setPosition(absolute) - setRelativeCoordinates(relative) - setMouseOverBlockId(blockId) - setDraggedStep(step) - } + const handleStepMouseDown = + (stepIndex: number) => + ( + { absolute, relative }: { absolute: Coordinates; relative: Coordinates }, + step: DraggableStep + ) => { + if (isReadOnly) return + detachStepFromBlock({ blockIndex, stepIndex }) + setPosition(absolute) + setMousePositionInElement(relative) + setDraggedStep(step) + } - const handleMouseOnTopOfStep = (stepIndex: number) => () => { - if (!draggedStep && !draggedStepType) return - setExpandedPlaceholderIndex(stepIndex === 0 ? 0 : stepIndex) - } - - const handleMouseOnBottomOfStep = (stepIndex: number) => () => { - if (!draggedStep && !draggedStepType) return - setExpandedPlaceholderIndex(stepIndex + 1) - } + const handlePushElementRef = + (idx: number) => (elem: HTMLDivElement | null) => { + elem && (placeholderRefs.current[idx] = elem) + } return ( - + {typebot && - stepIds.map((stepId, idx) => ( - + steps.map((step, idx) => ( + void + onClose: (newContent: TextBubbleContent) => void } -export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => { +export const TextBubbleEditor = ({ initialValue, onClose }: Props) => { const randomEditorId = useMemo(() => Math.random().toString(), []) const editor = useMemo( () => @@ -30,7 +28,6 @@ export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => { // eslint-disable-next-line react-hooks/exhaustive-deps [] ) - const { updateStep } = useTypebot() const [value, setValue] = useState(initialValue) const varDropdownRef = useRef(null) const rememberedSelection = useRef(null) @@ -38,12 +35,11 @@ export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => { const textEditorRef = useRef(null) + const closeEditor = () => onClose(convertValueToStepContent(value)) + useOutsideClick({ ref: textEditorRef, - handler: () => { - save(value) - onClose() - }, + handler: closeEditor, }) useEffect(() => { @@ -69,18 +65,16 @@ export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => { } } - const save = (value: unknown[]) => { - if (value.length === 0) return + const convertValueToStepContent = (value: unknown[]): TextBubbleContent => { + if (value.length === 0) defaultTextBubbleContent const html = serializeHtml(editor, { nodes: value, }) - updateStep(stepId, { - content: { - html, - richText: value, - plainText: parseHtmlStringToPlainText(html), - }, - } as TextBubbleStep) + return { + html, + richText: value, + plainText: parseHtmlStringToPlainText(html), + } } const handleMouseDown = (e: React.MouseEvent) => { @@ -99,6 +93,11 @@ export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => { setValue(val) setIsVariableDropdownOpen(false) } + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.shiftKey) return + if (e.key === 'Enter') closeEditor() + } + return ( { onBlur: () => { rememberedSelection.current = editor.selection }, + onKeyDown: handleKeyDown, }} initialValue={ initialValue.length === 0 diff --git a/apps/builder/components/shared/TableList.tsx b/apps/builder/components/shared/TableList.tsx index 4942eaa95..da6d1e45c 100644 --- a/apps/builder/components/shared/TableList.tsx +++ b/apps/builder/components/shared/TableList.tsx @@ -2,20 +2,20 @@ import { Box, Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react' import { TrashIcon, PlusIcon } from 'assets/icons' import { deepEqual } from 'fast-equals' import { Draft } from 'immer' -import { Table } from 'models' import React, { useEffect, useState } from 'react' import { generate } from 'short-uuid' import { useImmer } from 'use-immer' +type ItemWithId = T & { id: string } + export type TableListItemProps = { - id: string item: T onItemChange: (item: T) => void } type Props = { - initialItems: Table - onItemsChange: (items: Table) => void + initialItems: ItemWithId[] + onItemsChange: (items: ItemWithId[]) => void addLabel?: string Item: (props: TableListItemProps) => JSX.Element ComponentBetweenItems?: (props: unknown) => JSX.Element @@ -29,7 +29,7 @@ export const TableList = ({ ComponentBetweenItems = () => <>, }: Props) => { const [items, setItems] = useImmer(initialItems) - const [showDeleteId, setShowDeleteId] = useState() + const [showDeleteIndex, setShowDeleteIndex] = useState(null) useEffect(() => { if (deepEqual(items, initialItems)) return @@ -40,55 +40,47 @@ export const TableList = ({ const createItem = () => { setItems((items) => { const id = generate() - items.byId[id] = { id } as unknown as Draft - items.allIds.push(id) + const newItem = { id } as Draft> + items.push(newItem) }) } - const updateItem = (itemId: string, updates: Partial) => + const updateItem = (itemIndex: number, updates: Partial) => setItems((items) => { - items.byId[itemId] = { - ...items.byId[itemId], - ...updates, - } + items[itemIndex] = { ...items[itemIndex], ...updates } }) - const deleteItem = (itemId: string) => () => { + const deleteItem = (itemIndex: number) => () => { setItems((items) => { - delete items.byId[itemId] - const index = items.allIds.indexOf(itemId) - if (index !== -1) items.allIds.splice(index, 1) + items.splice(itemIndex, 1) }) } - const handleMouseEnter = (itemId: string) => () => setShowDeleteId(itemId) + const handleMouseEnter = (itemIndex: number) => () => + setShowDeleteIndex(itemIndex) - const handleCellChange = (itemId: string) => (item: T) => - updateItem(itemId, item) + const handleCellChange = (itemIndex: number) => (item: T) => + updateItem(itemIndex, item) - const handleMouseLeave = () => setShowDeleteId(undefined) + const handleMouseLeave = () => setShowDeleteIndex(null) return ( - {items.allIds.map((itemId, idx) => ( - - {idx !== 0 && } + {items.map((item, itemIndex) => ( + + {itemIndex !== 0 && } - - + + } aria-label="Remove cell" - onClick={deleteItem(itemId)} + onClick={deleteItem(itemIndex)} pos="absolute" left="-15px" top="-15px" diff --git a/apps/builder/components/shared/VariableSearchInput.tsx b/apps/builder/components/shared/VariableSearchInput.tsx index c72c3a637..8a15a3683 100644 --- a/apps/builder/components/shared/VariableSearchInput.tsx +++ b/apps/builder/components/shared/VariableSearchInput.tsx @@ -13,10 +13,10 @@ import { import { PlusIcon, TrashIcon } from 'assets/icons' import { useTypebot } from 'contexts/TypebotContext' import { Variable } from 'models' -import React, { useState, useRef, ChangeEvent, useMemo, useEffect } from 'react' +import React, { useState, useRef, ChangeEvent, useEffect } from 'react' import { generate } from 'short-uuid' import { useDebounce } from 'use-debounce' -import { isNotDefined } from 'utils' +import { byId, isNotDefined } from 'utils' type Props = { initialVariableId?: string @@ -34,16 +34,14 @@ export const VariableSearchInput = ({ }: Props) => { const { onOpen, onClose, isOpen } = useDisclosure() const { typebot, createVariable, deleteVariable } = useTypebot() - const variables = useMemo( - () => - typebot?.variables.allIds.map((id) => typebot.variables.byId[id]) ?? [], - [typebot?.variables] - ) + const variables = typebot?.variables ?? [] const [inputValue, setInputValue] = useState( - typebot?.variables.byId[initialVariableId ?? '']?.name ?? '' + variables.find(byId(initialVariableId))?.name ?? '' ) const [debouncedInputValue] = useDebounce(inputValue, 200) - const [filteredItems, setFilteredItems] = useState(variables) + const [filteredItems, setFilteredItems] = useState( + variables ?? [] + ) const dropdownRef = useRef(null) const inputRef = useRef(null) diff --git a/apps/builder/components/theme/ChatSettings/ButtonsTheme.tsx b/apps/builder/components/theme/ChatSettings/ButtonsTheme.tsx index 134cfa1da..7b9713fde 100644 --- a/apps/builder/components/theme/ChatSettings/ButtonsTheme.tsx +++ b/apps/builder/components/theme/ChatSettings/ButtonsTheme.tsx @@ -15,7 +15,7 @@ export const ButtonsTheme = ({ buttons, onButtonsChange }: Props) => { onButtonsChange({ ...buttons, color }) return ( - + Background: { onGuestBubblesChange({ ...guestBubbles, color }) return ( - + Background: { onHostBubblesChange({ ...hostBubbles, color }) return ( - + Background: { onInputsChange({ ...inputs, placeholderColor }) return ( - + Background: { const handleColorChange = (e: ChangeEvent) => setColor(e.target.value) + const handleClick = (color: string) => () => setColor(color) + return ( @@ -79,10 +81,8 @@ export const ColorPicker = ({ initialColor, onColorChange }: Props) => { minWidth="unset" borderRadius={3} _hover={{ background: c }} - onClick={() => { - setColor(c) - }} - > + onClick={handleClick(c)} + /> ))} } -export type BlocksCoordinates = { byId: { [key: string]: Coordinates } } +export type BlocksCoordinates = IdMap const graphContext = createContext<{ - blocksCoordinates?: BlocksCoordinates + blocksCoordinates: BlocksCoordinates updateBlockCoordinates: (blockId: string, newCoord: Coordinates) => void graphPosition: Position setGraphPosition: Dispatch> connectingIds: ConnectingIds | null setConnectingIds: Dispatch> - previewingEdgeId?: string - setPreviewingEdgeId: Dispatch> - sourceEndpoints: Table + previewingEdge?: Edge + setPreviewingEdge: Dispatch> + sourceEndpoints: IdMap addSourceEndpoint: (endpoint: Endpoint) => void - targetEndpoints: Table + targetEndpoints: IdMap addTargetEndpoint: (endpoint: Endpoint) => void openedStepId?: string setOpenedStepId: Dispatch> @@ -83,63 +82,55 @@ const graphContext = createContext<{ export const GraphProvider = ({ children, - typebot, + blocks, isReadOnly = false, }: { children: ReactNode - typebot?: Typebot + blocks: Block[] isReadOnly?: boolean }) => { const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue) const [connectingIds, setConnectingIds] = useState(null) - const [previewingEdgeId, setPreviewingEdgeId] = useState() - const [sourceEndpoints, setSourceEndpoints] = useState>({ - byId: {}, - allIds: [], - }) - const [targetEndpoints, setTargetEndpoints] = useState>({ - byId: {}, - allIds: [], - }) + const [previewingEdge, setPreviewingEdge] = useState() + const [sourceEndpoints, setSourceEndpoints] = useState>({}) + const [targetEndpoints, setTargetEndpoints] = useState>({}) const [openedStepId, setOpenedStepId] = useState() - const [blocksCoordinates, setBlocksCoordinates] = useImmer< - BlocksCoordinates | undefined - >(undefined) + const [blocksCoordinates, setBlocksCoordinates] = useState( + {} + ) useEffect(() => { setBlocksCoordinates( - typebot?.blocks.allIds.reduce( - (coords, blockId) => ({ - byId: { - ...coords.byId, - [blockId]: typebot.blocks.byId[blockId].graphCoordinates, - }, + blocks.reduce( + (coords, block) => ({ + ...coords, + [block.id]: block.graphCoordinates, }), - { byId: {} } + {} ) ) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [typebot?.blocks]) + }, [blocks]) const addSourceEndpoint = (endpoint: Endpoint) => { setSourceEndpoints((endpoints) => ({ - byId: { ...endpoints.byId, [endpoint.id]: endpoint }, - allIds: [...endpoints.allIds, endpoint.id], + ...endpoints, + [endpoint.id]: endpoint, })) } const addTargetEndpoint = (endpoint: Endpoint) => { setTargetEndpoints((endpoints) => ({ - byId: { ...endpoints.byId, [endpoint.id]: endpoint }, - allIds: [...endpoints.allIds, endpoint.id], + ...endpoints, + [endpoint.id]: endpoint, })) } const updateBlockCoordinates = (blockId: string, newCoord: Coordinates) => - setBlocksCoordinates((blocksCoordinates) => { - if (!blocksCoordinates) return - blocksCoordinates.byId[blockId] = newCoord - }) + setBlocksCoordinates((blocksCoordinates) => ({ + ...blocksCoordinates, + [blockId]: newCoord, + })) return ( +} + +const graphDndContext = createContext<{ + draggedStepType?: DraggableStepType + setDraggedStepType: Dispatch> + draggedStep?: DraggableStep + setDraggedStep: Dispatch> + draggedItem?: ButtonItem + setDraggedItem: Dispatch> + mouseOverBlock?: BlockInfo + setMouseOverBlock: Dispatch> + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore +}>({}) + +export type NodePosition = { absolute: Coordinates; relative: Coordinates } + +export const GraphDndContext = ({ children }: { children: ReactNode }) => { + const [draggedStep, setDraggedStep] = useState() + const [draggedStepType, setDraggedStepType] = useState< + DraggableStepType | undefined + >() + const [draggedItem, setDraggedItem] = useState() + const [mouseOverBlock, setMouseOverBlock] = useState() + + return ( + + {children} + + ) +} + +export const useDragDistance = ({ + ref, + onDrag, + distanceTolerance = 20, + isDisabled = false, +}: { + ref: React.MutableRefObject + onDrag: (position: { absolute: Coordinates; relative: Coordinates }) => void + distanceTolerance?: number + isDisabled: boolean +}) => { + const mouseDownPosition = + useRef<{ absolute: Coordinates; relative: Coordinates }>() + + const handleMouseUp = () => { + if (mouseDownPosition) mouseDownPosition.current = undefined + } + useEventListener('mouseup', handleMouseUp) + + const handleMouseDown = (e: MouseEvent) => { + if (isDisabled || !ref.current) return + e.stopPropagation() + const { top, left } = ref.current.getBoundingClientRect() + mouseDownPosition.current = { + absolute: { x: e.clientX, y: e.clientY }, + relative: { + x: e.clientX - left, + y: e.clientY - top, + }, + } + } + useEventListener('mousedown', handleMouseDown, ref.current) + + const handleMouseMove = (e: MouseEvent) => { + if (!mouseDownPosition.current) return + const { clientX, clientY } = e + if ( + Math.abs(mouseDownPosition.current.absolute.x - clientX) > + distanceTolerance || + Math.abs(mouseDownPosition.current.absolute.y - clientY) > + distanceTolerance + ) { + onDrag(mouseDownPosition.current) + } + } + useEventListener('mousemove', handleMouseMove) +} + +export const computeNearestPlaceholderIndex = ( + offsetY: number, + placeholderRefs: React.MutableRefObject +) => { + const { closestIndex } = placeholderRefs.current.reduce( + (prev, elem, index) => { + const elementTop = elem.getBoundingClientRect().top + const mouseDistanceFromPlaceholder = Math.abs(offsetY - elementTop) + return mouseDistanceFromPlaceholder < prev.value + ? { closestIndex: index, value: mouseDistanceFromPlaceholder } + : prev + }, + { closestIndex: 0, value: 100 } + ) + return closestIndex +} + +export const useStepDnd = () => useContext(graphDndContext) diff --git a/apps/builder/contexts/StepDndContext.tsx b/apps/builder/contexts/StepDndContext.tsx deleted file mode 100644 index d88e280ed..000000000 --- a/apps/builder/contexts/StepDndContext.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { ChoiceItem, DraggableStep, DraggableStepType } from 'models' -import { - createContext, - Dispatch, - ReactNode, - SetStateAction, - useContext, - useState, -} from 'react' - -const stepDndContext = createContext<{ - draggedStepType?: DraggableStepType - setDraggedStepType: Dispatch> - draggedStep?: DraggableStep - setDraggedStep: Dispatch> - draggedChoiceItem?: ChoiceItem - setDraggedChoiceItem: Dispatch> - mouseOverBlockId?: string - setMouseOverBlockId: Dispatch> - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore -}>({}) - -export const StepDndContext = ({ children }: { children: ReactNode }) => { - const [draggedStep, setDraggedStep] = useState() - const [draggedStepType, setDraggedStepType] = useState< - DraggableStepType | undefined - >() - const [draggedChoiceItem, setDraggedChoiceItem] = useState< - ChoiceItem | undefined - >() - const [mouseOverBlockId, setMouseOverBlockId] = useState() - - return ( - - {children} - - ) -} - -export const useStepDnd = () => useContext(stepDndContext) diff --git a/apps/builder/contexts/TypebotContext/TypebotContext.tsx b/apps/builder/contexts/TypebotContext/TypebotContext.tsx index 92c38ad29..c327e6343 100644 --- a/apps/builder/contexts/TypebotContext/TypebotContext.tsx +++ b/apps/builder/contexts/TypebotContext/TypebotContext.tsx @@ -25,13 +25,12 @@ import useSWR from 'swr' import { isDefined } from 'utils' import { BlocksActions, blocksActions } from './actions/blocks' import { stepsAction, StepsActions } from './actions/steps' -import { choiceItemsAction, ChoiceItemsActions } from './actions/choiceItems' import { variablesAction, VariablesActions } from './actions/variables' import { edgesAction, EdgesActions } from './actions/edges' -import { webhooksAction, WebhooksAction } from './actions/webhooks' import { useRegisterActions } from 'kbar' import useUndo from 'services/utils/useUndo' import { useDebounce } from 'use-debounce' +import { itemsAction, ItemsActions } from './actions/items' const autoSaveTimeout = 40000 type UpdateTypebotPayload = Partial<{ @@ -59,10 +58,9 @@ const typebotContext = createContext< publishTypebot: () => void } & BlocksActions & StepsActions & - ChoiceItemsActions & + ItemsActions & VariablesActions & - EdgesActions & - WebhooksAction + EdgesActions // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore >({}) @@ -72,7 +70,7 @@ export const TypebotContext = ({ typebotId, }: { children: ReactNode - typebotId?: string + typebotId: string }) => { const router = useRouter() const toast = useToast({ @@ -237,10 +235,9 @@ export const TypebotContext = ({ updateTypebot: updateLocalTypebot, ...blocksActions(localTypebot as Typebot, setLocalTypebot), ...stepsAction(localTypebot as Typebot, setLocalTypebot), - ...choiceItemsAction(localTypebot as Typebot, setLocalTypebot), ...variablesAction(localTypebot as Typebot, setLocalTypebot), ...edgesAction(localTypebot as Typebot, setLocalTypebot), - ...webhooksAction(localTypebot as Typebot, setLocalTypebot), + ...itemsAction(localTypebot as Typebot, setLocalTypebot), }} > {children} @@ -254,13 +251,13 @@ export const useFetchedTypebot = ({ typebotId, onError, }: { - typebotId?: string + typebotId: string onError: (error: Error) => void }) => { const { data, error, mutate } = useSWR< { typebot: Typebot; publishedTypebot?: PublicTypebot }, Error - >(typebotId ? `/api/typebots/${typebotId}` : null, fetcher) + >(`/api/typebots/${typebotId}`, fetcher) if (error) onError(error) return { typebot: data?.typebot, diff --git a/apps/builder/contexts/TypebotContext/actions/blocks.ts b/apps/builder/contexts/TypebotContext/actions/blocks.ts index dae171905..cb999edcd 100644 --- a/apps/builder/contexts/TypebotContext/actions/blocks.ts +++ b/apps/builder/contexts/TypebotContext/actions/blocks.ts @@ -1,96 +1,86 @@ import { Coordinates } from 'contexts/GraphContext' import { produce } from 'immer' import { WritableDraft } from 'immer/dist/internal' -import { Block, DraggableStep, DraggableStepType, Typebot } from 'models' +import { + Block, + DraggableStep, + DraggableStepType, + StepIndices, + Typebot, +} from 'models' import { SetTypebot } from '../TypebotContext' -import { deleteEdgeDraft } from './edges' -import { createStepDraft, deleteStepDraft } from './steps' +import { cleanUpEdgeDraft } from './edges' +import { createStepDraft } from './steps' export type BlocksActions = { createBlock: ( props: Coordinates & { id: string step: DraggableStep | DraggableStepType + indices: StepIndices } ) => void - updateBlock: (blockId: string, updates: Partial>) => void - deleteBlock: (blockId: string) => void + updateBlock: (blockIndex: number, updates: Partial>) => void + deleteBlock: (blockIndex: number) => void } -export const blocksActions = ( +const blocksActions = ( typebot: Typebot, setTypebot: SetTypebot ): BlocksActions => ({ createBlock: ({ id, step, + indices, ...graphCoordinates }: Coordinates & { id: string step: DraggableStep | DraggableStepType + indices: StepIndices }) => { setTypebot( produce(typebot, (typebot) => { const newBlock: Block = { id, graphCoordinates, - title: `Block ${typebot.blocks.allIds.length}`, - stepIds: [], + title: `Block #${typebot.blocks.length}`, + steps: [], } - typebot.blocks.byId[newBlock.id] = newBlock - typebot.blocks.allIds.push(newBlock.id) - createStepDraft(typebot, step, newBlock.id) - removeEmptyBlocks(typebot) + typebot.blocks.push(newBlock) + createStepDraft(typebot, step, newBlock.id, indices) }) ) }, - updateBlock: (blockId: string, updates: Partial>) => + updateBlock: (blockIndex: number, updates: Partial>) => setTypebot( produce(typebot, (typebot) => { - typebot.blocks.byId[blockId] = { - ...typebot.blocks.byId[blockId], - ...updates, - } + const block = typebot.blocks[blockIndex] + typebot.blocks[blockIndex] = { ...block, ...updates } }) ), - deleteBlock: (blockId: string) => + deleteBlock: (blockIndex: number) => setTypebot( produce(typebot, (typebot) => { - deleteStepsInsideBlock(typebot, blockId) - deleteAssociatedEdges(typebot, blockId) - deleteBlockDraft(typebot)(blockId) + deleteBlockDraft(typebot)(blockIndex) }) ), }) -export const removeEmptyBlocks = (typebot: WritableDraft) => { - const emptyBlockIds = typebot.blocks.allIds.filter( - (blockId) => typebot.blocks.byId[blockId].stepIds.length === 0 - ) - emptyBlockIds.forEach(deleteBlockDraft(typebot)) -} - -const deleteAssociatedEdges = ( - typebot: WritableDraft, - blockId: string -) => { - typebot.edges.allIds.forEach((edgeId) => { - if (typebot.edges.byId[edgeId].to.blockId === blockId) - deleteEdgeDraft(typebot, edgeId) - }) -} - -const deleteStepsInsideBlock = ( - typebot: WritableDraft, - blockId: string -) => { - const block = typebot.blocks.byId[blockId] - block.stepIds.forEach((stepId) => deleteStepDraft(stepId)(typebot)) -} - -export const deleteBlockDraft = - (typebot: WritableDraft) => (blockId: string) => { - delete typebot.blocks.byId[blockId] - const index = typebot.blocks.allIds.indexOf(blockId) - if (index !== -1) typebot.blocks.allIds.splice(index, 1) +const deleteBlockDraft = + (typebot: WritableDraft) => (blockIndex: number) => { + cleanUpEdgeDraft(typebot, typebot.blocks[blockIndex].id) + typebot.blocks.splice(blockIndex, 1) } + +const removeEmptyBlocks = (typebot: WritableDraft) => { + const emptyBlocksIndices = typebot.blocks.reduce( + (arr, block, idx) => { + block.steps.length === 0 && arr.push(idx) + return arr + }, + [] + ) + emptyBlocksIndices.forEach(deleteBlockDraft(typebot)) +} + +export { blocksActions, removeEmptyBlocks } diff --git a/apps/builder/contexts/TypebotContext/actions/choiceItems.ts b/apps/builder/contexts/TypebotContext/actions/choiceItems.ts deleted file mode 100644 index 50c11833e..000000000 --- a/apps/builder/contexts/TypebotContext/actions/choiceItems.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { ChoiceItem, InputStepType, Typebot } from 'models' -import { generate } from 'short-uuid' -import assert from 'assert' -import { SetTypebot } from '../TypebotContext' -import { produce } from 'immer' -import { WritableDraft } from 'immer/dist/internal' - -export type ChoiceItemsActions = { - createChoiceItem: ( - item: ChoiceItem | Pick, - index?: number - ) => void - updateChoiceItem: ( - itemId: string, - updates: Partial> - ) => void - deleteChoiceItem: (itemId: string) => void -} - -export const choiceItemsAction = ( - typebot: Typebot, - setTypebot: SetTypebot -): ChoiceItemsActions => ({ - createChoiceItem: ( - item: ChoiceItem | Pick, - index?: number - ) => { - setTypebot( - produce(typebot, (typebot) => { - createChoiceItemDraft(typebot, item, index) - }) - ) - }, - updateChoiceItem: ( - itemId: string, - updates: Partial> - ) => - setTypebot( - produce(typebot, (typebot) => { - typebot.choiceItems.byId[itemId] = { - ...typebot.choiceItems.byId[itemId], - ...updates, - } - }) - ), - deleteChoiceItem: (itemId: string) => { - setTypebot( - produce(typebot, (typebot) => { - removeChoiceItemFromStep(typebot, itemId) - deleteChoiceItemDraft(typebot, itemId) - }) - ) - }, -}) - -const removeChoiceItemFromStep = ( - typebot: WritableDraft, - itemId: string -) => { - const containerStepId = typebot.choiceItems.byId[itemId].stepId - const step = typebot.steps.byId[containerStepId] - assert(step.type === InputStepType.CHOICE) - step.options?.itemIds.splice(step.options.itemIds.indexOf(itemId), 1) -} - -export const deleteChoiceItemDraft = (typebot: Typebot, itemId: string) => { - delete typebot.choiceItems.byId[itemId] - const index = typebot.choiceItems.allIds.indexOf(itemId) - if (index !== -1) typebot.choiceItems.allIds.splice(index, 1) -} - -export const createChoiceItemDraft = ( - typebot: Typebot, - item: ChoiceItem | Pick, - index?: number -) => { - const step = typebot.steps.byId[item.stepId] - assert(step.type === InputStepType.CHOICE) - const newItem: ChoiceItem = - 'id' in item ? { ...item } : { id: generate(), stepId: item.stepId } - typebot.choiceItems.byId[newItem.id] = newItem - typebot.choiceItems.allIds.push(newItem.id) - if (step.options.itemIds.indexOf(newItem.id) !== -1) return - step.options.itemIds.splice(index ?? 0, 0, newItem.id) -} diff --git a/apps/builder/contexts/TypebotContext/actions/edges.ts b/apps/builder/contexts/TypebotContext/actions/edges.ts index a8b101100..303120d90 100644 --- a/apps/builder/contexts/TypebotContext/actions/edges.ts +++ b/apps/builder/contexts/TypebotContext/actions/edges.ts @@ -1,12 +1,13 @@ -import { Typebot, Edge, ConditionStep } from 'models' +import { Typebot, Edge, StepWithItems, StepIndices, ItemIndices } from 'models' import { WritableDraft } from 'immer/dist/types/types-external' import { generate } from 'short-uuid' import { SetTypebot } from '../TypebotContext' import { produce } from 'immer' +import { byId, isDefined, isNotDefined } from 'utils' export type EdgesActions = { createEdge: (edge: Omit) => void - updateEdge: (edgeId: string, updates: Partial>) => void + updateEdge: (edgeIndex: number, updates: Partial>) => void deleteEdge: (edgeId: string) => void } @@ -21,40 +22,37 @@ export const edgesAction = ( ...edge, id: generate(), } - if (edge.from.buttonId) { - deleteEdgeDraft( - typebot, - typebot.choiceItems.byId[edge.from.buttonId].edgeId - ) - typebot.choiceItems.byId[edge.from.buttonId].edgeId = newEdge.id - } else if (edge.from.conditionType === 'true') { - deleteEdgeDraft( - typebot, - (typebot.steps.byId[edge.from.stepId] as ConditionStep).trueEdgeId - ) - ;(typebot.steps.byId[edge.from.stepId] as ConditionStep).trueEdgeId = - newEdge.id - } else if (edge.from.conditionType === 'false') { - deleteEdgeDraft( - typebot, - (typebot.steps.byId[edge.from.stepId] as ConditionStep).falseEdgeId - ) - ;(typebot.steps.byId[edge.from.stepId] as ConditionStep).falseEdgeId = - newEdge.id - } else { - deleteEdgeDraft(typebot, typebot.steps.byId[edge.from.stepId].edgeId) - typebot.steps.byId[edge.from.stepId].edgeId = newEdge.id - } - typebot.edges.byId[newEdge.id] = newEdge - typebot.edges.allIds.push(newEdge.id) + removeExistingEdge(typebot, edge) + typebot.edges.push(newEdge) + const blockIndex = typebot.blocks.findIndex(byId(edge.from.blockId)) + const stepIndex = typebot.blocks[blockIndex].steps.findIndex( + byId(edge.from.stepId) + ) + const itemIndex = edge.from.itemId + ? ( + typebot.blocks[blockIndex].steps[stepIndex] as StepWithItems + ).items.findIndex(byId(edge.from.itemId)) + : null + + isDefined(itemIndex) + ? addEdgeIdToItem(typebot, newEdge.id, { + blockIndex, + stepIndex, + itemIndex, + }) + : addEdgeIdToStep(typebot, newEdge.id, { + blockIndex, + stepIndex, + }) }) ) }, - updateEdge: (edgeId: string, updates: Partial>) => + updateEdge: (edgeIndex: number, updates: Partial>) => setTypebot( produce(typebot, (typebot) => { - typebot.edges.byId[edgeId] = { - ...typebot.edges.byId[edgeId], + const currentEdge = typebot.edges[edgeIndex] + typebot.edges[edgeIndex] = { + ...currentEdge, ...updates, } }) @@ -68,12 +66,55 @@ export const edgesAction = ( }, }) +const addEdgeIdToStep = ( + typebot: WritableDraft, + edgeId: string, + { blockIndex, stepIndex }: StepIndices +) => { + typebot.blocks[blockIndex].steps[stepIndex].outgoingEdgeId = edgeId +} + +const addEdgeIdToItem = ( + typebot: WritableDraft, + edgeId: string, + { blockIndex, stepIndex, itemIndex }: ItemIndices +) => { + ;(typebot.blocks[blockIndex].steps[stepIndex] as StepWithItems).items[ + itemIndex + ].outgoingEdgeId = edgeId +} + export const deleteEdgeDraft = ( typebot: WritableDraft, - edgeId?: string + edgeId: string ) => { - if (!edgeId) return - delete typebot.edges.byId[edgeId] - const index = typebot.edges.allIds.indexOf(edgeId) - if (index !== -1) typebot.edges.allIds.splice(index, 1) + const edgeIndex = typebot.edges.findIndex(byId(edgeId)) + typebot.edges.splice(edgeIndex, 1) +} + +export const cleanUpEdgeDraft = ( + typebot: WritableDraft, + deletedNodeId: string +) => { + typebot.edges = typebot.edges.filter( + (edge) => + ![ + edge.from.blockId, + edge.from.stepId, + edge.from.itemId, + edge.to.blockId, + edge.to.stepId, + ].includes(deletedNodeId) + ) +} + +const removeExistingEdge = ( + typebot: WritableDraft, + edge: Omit +) => { + typebot.edges = typebot.edges.filter((e) => + edge.from.itemId + ? e.from.itemId !== edge.from.itemId + : isDefined(e.from.itemId) || e.from.stepId !== edge.from.stepId + ) } diff --git a/apps/builder/contexts/TypebotContext/actions/items.ts b/apps/builder/contexts/TypebotContext/actions/items.ts new file mode 100644 index 000000000..667765930 --- /dev/null +++ b/apps/builder/contexts/TypebotContext/actions/items.ts @@ -0,0 +1,71 @@ +import { + Typebot, + ItemIndices, + Item, + InputStepType, + StepWithItems, + ButtonItem, +} from 'models' +import { SetTypebot } from '../TypebotContext' +import produce from 'immer' +import { cleanUpEdgeDraft } from './edges' +import { stepHasItems } from 'utils' +import { generate } from 'short-uuid' + +export type ItemsActions = { + createItem: (item: Omit, indices: ItemIndices) => void + updateItem: (indices: ItemIndices, updates: Partial>) => void + deleteItem: (indices: ItemIndices) => void +} + +const itemsAction = ( + typebot: Typebot, + setTypebot: SetTypebot +): ItemsActions => ({ + createItem: ( + item: Omit, + { blockIndex, stepIndex, itemIndex }: ItemIndices + ) => { + setTypebot( + produce(typebot, (typebot) => { + const step = typebot.blocks[blockIndex].steps[stepIndex] + if (step.type !== InputStepType.CHOICE) return + step.items.splice(itemIndex, 0, { + ...item, + stepId: step.id, + id: generate(), + }) + }) + ) + }, + updateItem: ( + { blockIndex, stepIndex, itemIndex }: ItemIndices, + updates: Partial> + ) => + setTypebot( + produce(typebot, (typebot) => { + const step = typebot.blocks[blockIndex].steps[stepIndex] + if (!stepHasItems(step)) return + ;(typebot.blocks[blockIndex].steps[stepIndex] as StepWithItems).items[ + itemIndex + ] = { + ...step.items[itemIndex], + ...updates, + } as Item + }) + ), + deleteItem: ({ blockIndex, stepIndex, itemIndex }: ItemIndices) => { + setTypebot( + produce(typebot, (typebot) => { + const step = typebot.blocks[blockIndex].steps[ + stepIndex + ] as StepWithItems + const removingItem = step.items[itemIndex] + step.items.splice(itemIndex, 1) + cleanUpEdgeDraft(typebot, removingItem.id) + }) + ) + }, +}) + +export { itemsAction } diff --git a/apps/builder/contexts/TypebotContext/actions/steps.ts b/apps/builder/contexts/TypebotContext/actions/steps.ts index f4eb2cbe5..84df705ce 100644 --- a/apps/builder/contexts/TypebotContext/actions/steps.ts +++ b/apps/builder/contexts/TypebotContext/actions/steps.ts @@ -1,142 +1,108 @@ import { - ChoiceInputStep, Step, Typebot, DraggableStep, DraggableStepType, - defaultWebhookAttributes, + StepIndices, } from 'models' import { parseNewStep } from 'services/typebots' import { removeEmptyBlocks } from './blocks' import { WritableDraft } from 'immer/dist/types/types-external' -import { createChoiceItemDraft, deleteChoiceItemDraft } from './choiceItems' -import { isChoiceInput, isWebhookStep } from 'utils' -import { deleteEdgeDraft } from './edges' -import { createWebhookDraft, deleteWebhookDraft } from './webhooks' import { SetTypebot } from '../TypebotContext' import produce from 'immer' +import { cleanUpEdgeDraft } from './edges' export type StepsActions = { createStep: ( blockId: string, - step?: DraggableStep | DraggableStepType, - index?: number + step: DraggableStep | DraggableStepType, + indices: StepIndices ) => void updateStep: ( - stepId: string, + indices: StepIndices, updates: Partial> ) => void - detachStepFromBlock: (stepId: string) => void - deleteStep: (stepId: string) => void + detachStepFromBlock: (indices: StepIndices) => void + deleteStep: (indices: StepIndices) => void } -export const stepsAction = ( +const stepsAction = ( typebot: Typebot, setTypebot: SetTypebot ): StepsActions => ({ createStep: ( blockId: string, - step?: DraggableStep | DraggableStepType, - index?: number + step: DraggableStep | DraggableStepType, + indices: StepIndices ) => { - if (!step) return setTypebot( produce(typebot, (typebot) => { - createStepDraft(typebot, step, blockId, index) + createStepDraft(typebot, step, blockId, indices) + }) + ) + }, + updateStep: ( + { blockIndex, stepIndex }: StepIndices, + updates: Partial> + ) => + setTypebot( + produce(typebot, (typebot) => { + const step = typebot.blocks[blockIndex].steps[stepIndex] + typebot.blocks[blockIndex].steps[stepIndex] = { ...step, ...updates } + }) + ), + detachStepFromBlock: (indices: StepIndices) => { + setTypebot(produce(typebot, removeStepFromBlock(indices))) + }, + deleteStep: (indices: StepIndices) => { + setTypebot( + produce(typebot, (typebot) => { + removeStepFromBlock(indices)(typebot) removeEmptyBlocks(typebot) }) ) }, - updateStep: (stepId: string, updates: Partial>) => - setTypebot( - produce(typebot, (typebot) => { - typebot.steps.byId[stepId] = { - ...typebot.steps.byId[stepId], - ...updates, - } - }) - ), - detachStepFromBlock: (stepId: string) => { - setTypebot( - produce(typebot, (typebot) => { - removeStepIdFromBlock(typebot, stepId) - }) - ) - }, - deleteStep: (stepId: string) => { - setTypebot(produce(typebot, deleteStepDraft(stepId))) - }, }) -const removeStepIdFromBlock = ( - typebot: WritableDraft, - stepId: string -) => { - const containerBlock = typebot.blocks.byId[typebot.steps.byId[stepId].blockId] - containerBlock.stepIds.splice(containerBlock.stepIds.indexOf(stepId), 1) -} - -export const deleteStepDraft = - (stepId: string) => (typebot: WritableDraft) => { - const step = typebot.steps.byId[stepId] - if (isChoiceInput(step)) deleteChoiceItemsInsideStep(typebot, step) - if (isWebhookStep(step)) - deleteWebhookDraft(step.options?.webhookId)(typebot) - deleteEdgeDraft(typebot, step.edgeId) - removeStepIdFromBlock(typebot, stepId) - delete typebot.steps.byId[stepId] - const index = typebot.steps.allIds.indexOf(stepId) - if (index !== -1) typebot.steps.allIds.splice(index, 1) - removeEmptyBlocks(typebot) +const removeStepFromBlock = + ({ blockIndex, stepIndex }: StepIndices) => + (typebot: WritableDraft) => { + const removingStep = typebot.blocks[blockIndex].steps[stepIndex] + cleanUpEdgeDraft(typebot, removingStep.id) + typebot.blocks[blockIndex].steps.splice(stepIndex, 1) } -export const createStepDraft = ( +const createStepDraft = ( typebot: WritableDraft, step: DraggableStep | DraggableStepType, blockId: string, - index?: number -) => + indices: StepIndices +) => { typeof step === 'string' - ? createNewStep(typebot, step, blockId, index) - : moveStepToBlock(typebot, step, blockId, index) + ? createNewStep(typebot, step, blockId, indices) + : moveStepToBlock(typebot, step, blockId, indices) + removeEmptyBlocks(typebot) +} const createNewStep = ( typebot: WritableDraft, type: DraggableStepType, blockId: string, - index?: number + { blockIndex, stepIndex }: StepIndices ) => { const newStep = parseNewStep(type, blockId) - typebot.steps.byId[newStep.id] = newStep - if (isChoiceInput(newStep)) { - createChoiceItemDraft(typebot, { - id: newStep.options.itemIds[0], - stepId: newStep.id, - }) - } else if (isWebhookStep(newStep)) { - createWebhookDraft({ - id: newStep.options.webhookId, - ...defaultWebhookAttributes, - })(typebot) - } - typebot.steps.allIds.push(newStep.id) - typebot.blocks.byId[blockId].stepIds.splice(index ?? 0, 0, newStep.id) + typebot.blocks[blockIndex].steps.splice(stepIndex ?? 0, 0, newStep) } const moveStepToBlock = ( typebot: WritableDraft, step: DraggableStep, blockId: string, - index?: number -) => { - typebot.steps.byId[step.id].blockId = blockId - typebot.blocks.byId[blockId].stepIds.splice(index ?? 0, 0, step.id) -} - -const deleteChoiceItemsInsideStep = ( - typebot: WritableDraft, - step: ChoiceInputStep + { blockIndex, stepIndex }: StepIndices ) => - step.options?.itemIds.forEach((itemId) => - deleteChoiceItemDraft(typebot, itemId) - ) + typebot.blocks[blockIndex].steps.splice(stepIndex ?? 0, 0, { + ...step, + blockId, + }) + +export { stepsAction, createStepDraft } diff --git a/apps/builder/contexts/TypebotContext/actions/variables.ts b/apps/builder/contexts/TypebotContext/actions/variables.ts index 6e9f065be..2f477e3cb 100644 --- a/apps/builder/contexts/TypebotContext/actions/variables.ts +++ b/apps/builder/contexts/TypebotContext/actions/variables.ts @@ -19,8 +19,7 @@ export const variablesAction = ( createVariable: (newVariable: Variable) => { setTypebot( produce(typebot, (typebot) => { - typebot.variables.byId[newVariable.id] = newVariable - typebot.variables.allIds.push(newVariable.id) + typebot.variables.push(newVariable) }) ) }, @@ -30,10 +29,9 @@ export const variablesAction = ( ) => setTypebot( produce(typebot, (typebot) => { - typebot.variables.byId[variableId] = { - ...typebot.variables.byId[variableId], - ...updates, - } + typebot.variables.map((v) => + v.id === variableId ? { ...v, ...updates } : v + ) }) ), deleteVariable: (itemId: string) => { @@ -49,7 +47,6 @@ export const deleteVariableDraft = ( typebot: WritableDraft, variableId: string ) => { - delete typebot.variables.byId[variableId] - const index = typebot.variables.allIds.indexOf(variableId) - if (index !== -1) typebot.variables.allIds.splice(index, 1) + const index = typebot.variables.findIndex((v) => v.id === variableId) + typebot.variables.splice(index, 1) } diff --git a/apps/builder/contexts/TypebotContext/actions/webhooks.ts b/apps/builder/contexts/TypebotContext/actions/webhooks.ts deleted file mode 100644 index d22b1757e..000000000 --- a/apps/builder/contexts/TypebotContext/actions/webhooks.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Typebot, Webhook } from 'models' -import { WritableDraft } from 'immer/dist/internal' -import { SetTypebot } from '../TypebotContext' -import { produce } from 'immer' - -export type WebhooksAction = { - createWebhook: (webook: Webhook) => void - updateWebhook: ( - webhookId: string, - updates: Partial> - ) => void - deleteWebhook: (variableId: string) => void -} - -export const webhooksAction = ( - typebot: Typebot, - setTypebot: SetTypebot -): WebhooksAction => ({ - createWebhook: (newWebhook: Webhook) => { - setTypebot(produce(typebot, createWebhookDraft(newWebhook))) - }, - updateWebhook: (webhookId: string, updates: Partial>) => - setTypebot( - produce(typebot, (typebot) => { - typebot.webhooks.byId[webhookId] = { - ...typebot.webhooks.byId[webhookId], - ...updates, - } - }) - ), - deleteWebhook: (webhookId: string) => { - setTypebot(produce(typebot, deleteWebhookDraft(webhookId))) - }, -}) - -export const createWebhookDraft = - (newWebhook: Webhook) => (typebot: WritableDraft) => { - typebot.webhooks.byId[newWebhook.id] = newWebhook - typebot.webhooks.allIds.push(newWebhook.id) - } - -export const deleteWebhookDraft = - (webhookId?: string) => (typebot: WritableDraft) => { - if (!webhookId) return - delete typebot.webhooks.byId[webhookId] - const index = typebot.webhooks.allIds.indexOf(webhookId) - if (index !== -1) typebot.webhooks.allIds.splice(index, 1) - } diff --git a/apps/builder/layouts/editor/Board.tsx b/apps/builder/layouts/editor/Board.tsx index 21c409209..f52a10cc2 100644 --- a/apps/builder/layouts/editor/Board.tsx +++ b/apps/builder/layouts/editor/Board.tsx @@ -1,6 +1,6 @@ import { Flex } from '@chakra-ui/react' import React from 'react' -import { StepDndContext } from 'contexts/StepDndContext' +import { GraphDndContext } from 'contexts/GraphDndContext' import { StepsSideBar } from '../../components/editor/StepsSideBar' import { PreviewDrawer } from '../../components/editor/preview/PreviewDrawer' import { RightPanel, useEditor } from 'contexts/EditorContext' @@ -15,14 +15,14 @@ export const Board = () => { return ( - + - + {rightPanel === RightPanel.PREVIEW && } - + ) } diff --git a/apps/builder/layouts/theme/ThemeContent.tsx b/apps/builder/layouts/theme/ThemeContent.tsx index ddb32207c..eb4677625 100644 --- a/apps/builder/layouts/theme/ThemeContent.tsx +++ b/apps/builder/layouts/theme/ThemeContent.tsx @@ -1,17 +1,13 @@ import { Flex } from '@chakra-ui/react' import { TypebotViewer } from 'bot-engine' import { useTypebot } from 'contexts/TypebotContext/TypebotContext' -import React, { useMemo } from 'react' +import React from 'react' import { parseTypebotToPublicTypebot } from 'services/publicTypebot' import { ThemeSideMenu } from '../../components/theme/ThemeSideMenu' export const ThemeContent = () => { const { typebot } = useTypebot() - const publicTypebot = useMemo( - () => (typebot ? parseTypebotToPublicTypebot(typebot) : undefined), - // eslint-disable-next-line react-hooks/exhaustive-deps - [typebot?.theme] - ) + const publicTypebot = typebot && parseTypebotToPublicTypebot(typebot) return ( diff --git a/apps/builder/pages/_app.tsx b/apps/builder/pages/_app.tsx index 18c8607c0..0e2a1fa66 100644 --- a/apps/builder/pages/_app.tsx +++ b/apps/builder/pages/_app.tsx @@ -20,14 +20,19 @@ const App = ({ Component, pageProps }: AppProps) => { useRouterProgressBar() const { query } = useRouter() + const typebotId = query.typebotId?.toString() return ( - + {typebotId ? ( + + + + ) : ( - + )} diff --git a/apps/builder/pages/api/typebots/[typebotId]/webhooks/[id]/execute.ts b/apps/builder/pages/api/typebots/[typebotId]/blocks/[blockIndex]/steps/[stepIndex]/executeWebhook.ts similarity index 78% rename from apps/builder/pages/api/typebots/[typebotId]/webhooks/[id]/execute.ts rename to apps/builder/pages/api/typebots/[typebotId]/blocks/[blockIndex]/steps/[stepIndex]/executeWebhook.ts index 1353d66d0..4c22c04c7 100644 --- a/apps/builder/pages/api/typebots/[typebotId]/webhooks/[id]/execute.ts +++ b/apps/builder/pages/api/typebots/[typebotId]/blocks/[blockIndex]/steps/[stepIndex]/executeWebhook.ts @@ -1,12 +1,5 @@ import prisma from 'libs/prisma' -import { - KeyValue, - Table, - Typebot, - Variable, - Webhook, - WebhookResponse, -} from 'models' +import { KeyValue, Typebot, Variable, Webhook, WebhookResponse } from 'models' import { parseVariables } from 'bot-engine' import { NextApiRequest, NextApiResponse } from 'next' import got, { Method, Headers, HTTPError } from 'got' @@ -16,13 +9,21 @@ import { stringify } from 'qs' const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'POST') { const typebotId = req.query.typebotId.toString() - const webhookId = req.query.id.toString() - const variables = JSON.parse(req.body).variables as Table + const blockIndex = Number(req.query.blockIndex) + const stepIndex = Number(req.query.stepIndex) + const variables = JSON.parse(req.body).variables as Variable[] const typebot = await prisma.typebot.findUnique({ where: { id: typebotId }, }) - const webhook = (typebot as Typebot).webhooks.byId[webhookId] - const result = await executeWebhook(webhook, variables) + const step = (typebot as unknown as Typebot).blocks[blockIndex].steps[ + stepIndex + ] + if (!('webhook' in step)) + return { + statusCode: 400, + data: { message: `Couldn't find webhook` }, + } + const result = await executeWebhook(step.webhook, variables) return res.status(200).send(result) } return methodNotAllowed(res) @@ -30,7 +31,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const executeWebhook = async ( webhook: Webhook, - variables: Table + variables: Variable[] ): Promise => { if (!webhook.url || !webhook.method) return { @@ -87,12 +88,11 @@ const parseBody = (body: string) => { } const convertKeyValueTableToObject = ( - keyValues: Table | undefined, - variables: Table + keyValues: KeyValue[] | undefined, + variables: Variable[] ) => { if (!keyValues) return - return keyValues.allIds.reduce((object, id) => { - const item = keyValues.byId[id] + return keyValues.reduce((object, item) => { if (!item.key) return {} return { ...object, diff --git a/apps/builder/pages/api/typebots/[typebotId]/results/answers/count.ts b/apps/builder/pages/api/typebots/[typebotId]/results/answers/count.ts index 93784d3f4..ada2e72f3 100644 --- a/apps/builder/pages/api/typebots/[typebotId]/results/answers/count.ts +++ b/apps/builder/pages/api/typebots/[typebotId]/results/answers/count.ts @@ -24,14 +24,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const answersCounts: { blockId: string; totalAnswers: number }[] = await Promise.all( - ( - typebot.publishedTypebot as unknown as PublicTypebot - ).blocks.allIds.map(async (blockId) => { - const totalAnswers = await prisma.answer.count({ - where: { blockId }, - }) - return { blockId, totalAnswers } - }) + (typebot.publishedTypebot as unknown as PublicTypebot).blocks.map( + async (block) => { + const totalAnswers = await prisma.answer.count({ + where: { blockId: block.id }, + }) + return { blockId: block.id, totalAnswers } + } + ) ) return res.status(200).send({ answersCounts }) } diff --git a/apps/builder/playwright.config.ts b/apps/builder/playwright.config.ts index c038a910e..9b3126666 100644 --- a/apps/builder/playwright.config.ts +++ b/apps/builder/playwright.config.ts @@ -8,8 +8,8 @@ const config: PlaywrightTestConfig = { expect: { timeout: 5000, }, - retries: 2, - workers: process.env.CI ? 1 : undefined, + retries: process.env.NO_RETRIES ? 0 : 2, + workers: process.env.CI ? 1 : 3, reporter: 'html', maxFailures: process.env.CI ? 10 : undefined, use: { diff --git a/apps/builder/playwright/fixtures/typebots/editor/buttonsDnd.json b/apps/builder/playwright/fixtures/typebots/editor/buttonsDnd.json new file mode 100644 index 000000000..c793c8a9d --- /dev/null +++ b/apps/builder/playwright/fixtures/typebots/editor/buttonsDnd.json @@ -0,0 +1,185 @@ +{ + "id": "ckz84wbbj2095no1ali9kzfz4", + "createdAt": "2022-02-04T08:16:59.215Z", + "updatedAt": "2022-02-04T08:16:59.215Z", + "name": "My typebot", + "ownerId": "ckz6t9iep0006k31a22j05fwq", + "publishedTypebotId": null, + "folderId": null, + "blocks": [ + { + "id": "de8iZbvNxMxyhvLrnPBpt8", + "steps": [ + { + "id": "hqgG9FuPDWrkrdHXFnSy9G", + "type": "start", + "label": "Start", + "blockId": "de8iZbvNxMxyhvLrnPBpt8", + "outgoingEdgeId": "41aa19ih9WQQQEurwdjmVJ" + } + ], + "title": "Start", + "graphCoordinates": { "x": 0, "y": 0 } + }, + { + "id": "vmDTsAC7aLeqanVVtJ9yQx", + "graphCoordinates": { "x": 85, "y": 220 }, + "title": "Block #1", + "steps": [ + { + "id": "sqUp2x8SXx8JBC8a9XuKGL9", + "blockId": "vmDTsAC7aLeqanVVtJ9yQx", + "type": "text", + "content": { + "html": "
Hello!
", + "richText": [{ "type": "p", "children": [{ "text": "Hello!" }] }], + "plainText": "Hello!" + } + }, + { + "id": "suRXuWyuJ7kpsdLUYKA6VqM", + "blockId": "vmDTsAC7aLeqanVVtJ9yQx", + "type": "text", + "content": { + "html": "
How are you?
", + "richText": [ + { "type": "p", "children": [{ "text": "How are you?" }] } + ], + "plainText": "How are you?" + } + }, + { + "id": "ssxDdzVUkgZYPPoPnQK4dCo", + "blockId": "vmDTsAC7aLeqanVVtJ9yQx", + "type": "choice input", + "options": { "buttonLabel": "Send", "isMultipleChoice": false }, + "items": [ + { + "id": "e7dy3bH2py8fFcak2jUJjr", + "stepId": "ssxDdzVUkgZYPPoPnQK4dCo", + "type": 0, + "content": "Item 1", + "outgoingEdgeId": "8Ty7noiTJAP3jtaWXLsNwy" + }, + { + "stepId": "ssxDdzVUkgZYPPoPnQK4dCo", + "type": 0, + "id": "mBJkTavGHAygmPTjiLMQyC", + "content": "Item 2" + }, + { + "stepId": "ssxDdzVUkgZYPPoPnQK4dCo", + "type": 0, + "id": "vVfToFyNLyGgRYyB8jYLkn", + "content": "Item 3", + "outgoingEdgeId": "tprSzPvt6A5kTFf7iUNaeR" + } + ] + } + ] + }, + { + "id": "hB4p8rwA1dUSq9A5ctTLqh", + "graphCoordinates": { "x": 513, "y": 152 }, + "title": "Block #2", + "steps": [ + { + "id": "suHw7fjcMD9KjDRpbB413jn", + "blockId": "hB4p8rwA1dUSq9A5ctTLqh", + "type": "Condition", + "items": [ + { + "id": "jvGN6sfftqJgfYYVRUYMuJ", + "stepId": "suHw7fjcMD9KjDRpbB413jn", + "type": 1, + "content": { "comparisons": [], "logicalOperator": "AND" } + } + ] + } + ] + }, + { + "id": "t7g44CwVvCg6mN16KHVAWv", + "graphCoordinates": { "x": 509, "y": 489 }, + "title": "Block #3", + "steps": [ + { + "id": "suHztCMVss4kTAtgShANxjU", + "blockId": "t7g44CwVvCg6mN16KHVAWv", + "type": "choice input", + "options": { "buttonLabel": "Send", "isMultipleChoice": false }, + "items": [ + { + "id": "buQjAL2M3cBUVK2ofnxKW3", + "stepId": "suHztCMVss4kTAtgShANxjU", + "type": 0, + "content": "Item 2-1" + }, + { + "stepId": "suHztCMVss4kTAtgShANxjU", + "type": 0, + "id": "4gQe9XK1vyQUHXVzFErW4t", + "content": "Item 2-2" + }, + { + "stepId": "suHztCMVss4kTAtgShANxjU", + "type": 0, + "id": "uttagH8w5XWzibkKsW23oi", + "content": "Item 2-3" + } + ] + } + ] + } + ], + "variables": [], + "edges": [ + { + "from": { + "blockId": "de8iZbvNxMxyhvLrnPBpt8", + "stepId": "hqgG9FuPDWrkrdHXFnSy9G" + }, + "to": { "blockId": "vmDTsAC7aLeqanVVtJ9yQx" }, + "id": "41aa19ih9WQQQEurwdjmVJ" + }, + { + "from": { + "blockId": "vmDTsAC7aLeqanVVtJ9yQx", + "stepId": "ssxDdzVUkgZYPPoPnQK4dCo", + "itemId": "e7dy3bH2py8fFcak2jUJjr" + }, + "to": { "blockId": "hB4p8rwA1dUSq9A5ctTLqh" }, + "id": "8Ty7noiTJAP3jtaWXLsNwy" + }, + { + "from": { + "blockId": "vmDTsAC7aLeqanVVtJ9yQx", + "stepId": "ssxDdzVUkgZYPPoPnQK4dCo", + "itemId": "vVfToFyNLyGgRYyB8jYLkn" + }, + "to": { "blockId": "t7g44CwVvCg6mN16KHVAWv" }, + "id": "tprSzPvt6A5kTFf7iUNaeR" + } + ], + "theme": { + "chat": { + "inputs": { + "color": "#303235", + "backgroundColor": "#FFFFFF", + "placeholderColor": "#9095A0" + }, + "buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" }, + "hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" }, + "guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" } + }, + "general": { "font": "Open Sans", "background": { "type": "None" } } + }, + "settings": { + "general": { "isBrandingEnabled": true }, + "metadata": { + "description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form." + }, + "typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 } + }, + "publicId": null +} diff --git a/apps/builder/playwright/fixtures/typebots/integrations/googleSheets.json b/apps/builder/playwright/fixtures/typebots/integrations/googleSheets.json index ee6a36063..4c8989e23 100644 --- a/apps/builder/playwright/fixtures/typebots/integrations/googleSheets.json +++ b/apps/builder/playwright/fixtures/typebots/integrations/googleSheets.json @@ -1,98 +1,76 @@ { - "id": "ckylszb9z0354z31a623dg7ji", - "createdAt": "2022-01-19T17:12:27.863Z", - "updatedAt": "2022-01-19T17:12:27.863Z", + "id": "ckz8gg4n39573no1aa5rsfyp1", + "createdAt": "2022-02-04T13:40:19.455Z", + "updatedAt": "2022-02-04T13:40:19.455Z", "name": "My typebot", - "ownerId": "ckylsz8yy0335z31amvq0jwtt", + "ownerId": "ckz6t9iep0006k31a22j05fwq", "publishedTypebotId": null, "folderId": null, - "webhooks": { "byId": {}, "allIds": [] }, - "blocks": { - "byId": { - "j24wz82YG3rjXMgrmCiTLy": { - "id": "j24wz82YG3rjXMgrmCiTLy", - "title": "Start", - "stepIds": ["1NdXPCiRicqDA8k4JfnXfi"], - "graphCoordinates": { "x": 0, "y": 0 } - }, - "bmaKTUXkT2cc3wtKfK7ra71": { - "id": "bmaKTUXkT2cc3wtKfK7ra71", - "title": "Block #2", - "graphCoordinates": { "x": 175, "y": 197 }, - "stepIds": ["spHxPWbSqkVZW9gqH86ovC5"] - }, - "bnt8fM5Wgc8gBg2iSmUcfJu": { - "id": "bnt8fM5Wgc8gBg2iSmUcfJu", - "title": "Block #3", - "graphCoordinates": { "x": 504, "y": 347 }, - "stepIds": ["siPoEE9H27hVHqykth3a7Kj"] - } + "blocks": [ + { + "id": "bSHn2HQZ1sKji5pd9Nmejf", + "steps": [ + { + "id": "qYmbSFBxCvGCgTvQTg9HeH", + "type": "start", + "label": "Start", + "blockId": "bSHn2HQZ1sKji5pd9Nmejf", + "outgoingEdgeId": "jdQnqJK4b559rTJzHHhjcz" + } + ], + "title": "Start", + "graphCoordinates": { "x": 0, "y": 0 } }, - "allIds": [ - "j24wz82YG3rjXMgrmCiTLy", - "bmaKTUXkT2cc3wtKfK7ra71", - "bnt8fM5Wgc8gBg2iSmUcfJu" - ] - }, - "steps": { - "byId": { - "1NdXPCiRicqDA8k4JfnXfi": { - "id": "1NdXPCiRicqDA8k4JfnXfi", - "type": "start", - "label": "Start", - "blockId": "j24wz82YG3rjXMgrmCiTLy", - "edgeId": "benDCcLMUWNvi6Fg6CXE9H" - }, - "spHxPWbSqkVZW9gqH86ovC5": { - "id": "spHxPWbSqkVZW9gqH86ovC5", - "blockId": "bmaKTUXkT2cc3wtKfK7ra71", - "type": "email input", - "options": { "variableId": "oexLr4sJQNVdSnYCGgGRB3" }, - "edgeId": "6Tax9rw7L8kmRn9JRD2Mrg" - }, - "siPoEE9H27hVHqykth3a7Kj": { - "id": "siPoEE9H27hVHqykth3a7Kj", - "blockId": "bnt8fM5Wgc8gBg2iSmUcfJu", - "type": "Google Sheets" - } + { + "id": "mSvUFogQH16bQDN1iGWF66", + "graphCoordinates": { "x": 324, "y": 209 }, + "title": "Block #1", + "steps": [ + { + "id": "spDLmDCZfNJu4DrZ1MUg84c", + "blockId": "mSvUFogQH16bQDN1iGWF66", + "type": "email input", + "options": { + "labels": { "button": "Send", "placeholder": "Type your email..." }, + "variableId": "qyLW6xD1AyLeedso2tHmhw" + }, + "outgoingEdgeId": "4yg9V76fdDntpDEw6H3tvU" + } + ] }, - "allIds": [ - "1NdXPCiRicqDA8k4JfnXfi", - "spHxPWbSqkVZW9gqH86ovC5", - "siPoEE9H27hVHqykth3a7Kj" - ] - }, - "choiceItems": { "byId": {}, "allIds": [] }, - "variables": { - "byId": { - "oexLr4sJQNVdSnYCGgGRB3": { - "id": "oexLr4sJQNVdSnYCGgGRB3", - "name": "Email" - } - }, - "allIds": ["oexLr4sJQNVdSnYCGgGRB3"] - }, - "edges": { - "byId": { - "benDCcLMUWNvi6Fg6CXE9H": { - "from": { - "blockId": "j24wz82YG3rjXMgrmCiTLy", - "stepId": "1NdXPCiRicqDA8k4JfnXfi" - }, - "to": { "blockId": "bmaKTUXkT2cc3wtKfK7ra71" }, - "id": "benDCcLMUWNvi6Fg6CXE9H" + { + "id": "jd4S6BQsUQ2RuKieHXYKs9", + "graphCoordinates": { "x": 655, "y": 363 }, + "title": "Block #2", + "steps": [ + { + "id": "s1ZvUqWxV6b8UgFGYWh39pV", + "blockId": "jd4S6BQsUQ2RuKieHXYKs9", + "type": "Google Sheets", + "options": {} + } + ] + } + ], + "variables": [{ "id": "qyLW6xD1AyLeedso2tHmhw", "name": "Email" }], + "edges": [ + { + "from": { + "blockId": "bSHn2HQZ1sKji5pd9Nmejf", + "stepId": "qYmbSFBxCvGCgTvQTg9HeH" }, - "6Tax9rw7L8kmRn9JRD2Mrg": { - "from": { - "blockId": "bmaKTUXkT2cc3wtKfK7ra71", - "stepId": "spHxPWbSqkVZW9gqH86ovC5" - }, - "to": { "blockId": "bnt8fM5Wgc8gBg2iSmUcfJu" }, - "id": "6Tax9rw7L8kmRn9JRD2Mrg" - } + "to": { "blockId": "mSvUFogQH16bQDN1iGWF66" }, + "id": "jdQnqJK4b559rTJzHHhjcz" }, - "allIds": ["benDCcLMUWNvi6Fg6CXE9H", "6Tax9rw7L8kmRn9JRD2Mrg"] - }, + { + "from": { + "blockId": "mSvUFogQH16bQDN1iGWF66", + "stepId": "spDLmDCZfNJu4DrZ1MUg84c" + }, + "to": { "blockId": "jd4S6BQsUQ2RuKieHXYKs9" }, + "id": "4yg9V76fdDntpDEw6H3tvU" + } + ], "theme": { "chat": { "inputs": { diff --git a/apps/builder/playwright/fixtures/typebots/integrations/googleSheetsGet.json b/apps/builder/playwright/fixtures/typebots/integrations/googleSheetsGet.json index c0fc51813..6f395df6a 100644 --- a/apps/builder/playwright/fixtures/typebots/integrations/googleSheetsGet.json +++ b/apps/builder/playwright/fixtures/typebots/integrations/googleSheetsGet.json @@ -1,136 +1,109 @@ { - "id": "ckyltevlb0559z31an8cmkyrp", - "createdAt": "2022-01-19T17:24:34.031Z", - "updatedAt": "2022-01-19T17:24:34.031Z", + "id": "ckz8gg4n39573no1aa5rsfyp1", + "createdAt": "2022-02-04T13:40:19.455Z", + "updatedAt": "2022-02-04T13:40:19.455Z", "name": "My typebot", - "ownerId": "ckyltekzq0533z31ad8opmacz", + "ownerId": "ckz6t9iep0006k31a22j05fwq", "publishedTypebotId": null, "folderId": null, - "webhooks": { "byId": {}, "allIds": [] }, - "blocks": { - "byId": { - "kPupUcEn7TcBGKHUpgK2Q5": { - "id": "kPupUcEn7TcBGKHUpgK2Q5", - "title": "Start", - "stepIds": ["nP5oWm7PxigMupyWpPLq24"], - "graphCoordinates": { "x": 0, "y": 0 } - }, - "bi4J5fv9DFn1zPSqGf8eRht": { - "id": "bi4J5fv9DFn1zPSqGf8eRht", - "title": "Block #2", - "graphCoordinates": { "x": 104, "y": 201 }, - "stepIds": ["scH9qdXwFfAScoavj6UzQNT"] - }, - "bwqGhUsa2SKaxXSrKtapVc8": { - "id": "bwqGhUsa2SKaxXSrKtapVc8", - "title": "Block #3", - "graphCoordinates": { "x": 458, "y": 292 }, - "stepIds": ["shZdc8Qw48domEbS7vLW5eN"] - }, - "bqmgS9hLUu2RA2oxVv7hMka": { - "id": "bqmgS9hLUu2RA2oxVv7hMka", - "title": "Block #4", - "graphCoordinates": { "x": 102, "y": 432 }, - "stepIds": ["s4z6G3MGAyhXChU9jakQWer"] - } - }, - "allIds": [ - "kPupUcEn7TcBGKHUpgK2Q5", - "bi4J5fv9DFn1zPSqGf8eRht", - "bwqGhUsa2SKaxXSrKtapVc8", - "bqmgS9hLUu2RA2oxVv7hMka" - ] - }, - "steps": { - "byId": { - "nP5oWm7PxigMupyWpPLq24": { - "id": "nP5oWm7PxigMupyWpPLq24", - "type": "start", - "label": "Start", - "blockId": "kPupUcEn7TcBGKHUpgK2Q5", - "edgeId": "kCLXGLpiM2F6pn4wFYc1f5" - }, - "scH9qdXwFfAScoavj6UzQNT": { - "id": "scH9qdXwFfAScoavj6UzQNT", - "blockId": "bi4J5fv9DFn1zPSqGf8eRht", - "type": "email input", - "options": { "variableId": "ifXp66N1meAtoUDcbqWxuD" }, - "edgeId": "7Czn5hJFUfpkRGtwGnKxtt" - }, - "shZdc8Qw48domEbS7vLW5eN": { - "id": "shZdc8Qw48domEbS7vLW5eN", - "blockId": "bwqGhUsa2SKaxXSrKtapVc8", - "type": "Google Sheets", - "edgeId": "eMhGokwHMDRDrynSvpiRje" - }, - "s4z6G3MGAyhXChU9jakQWer": { - "id": "s4z6G3MGAyhXChU9jakQWer", - "blockId": "bqmgS9hLUu2RA2oxVv7hMka", - "type": "text", - "content": { - "html": "
Your name is: {{First name}} {{Last name}}
", - "richText": [ - { - "type": "p", - "children": [ - { "text": "Your name is: {{First name}} {{Last name}}" } - ] - } - ], - "plainText": "Your name is: {{First name}} {{Last name}}" + "blocks": [ + { + "id": "bSHn2HQZ1sKji5pd9Nmejf", + "steps": [ + { + "id": "qYmbSFBxCvGCgTvQTg9HeH", + "type": "start", + "label": "Start", + "blockId": "bSHn2HQZ1sKji5pd9Nmejf", + "outgoingEdgeId": "jdQnqJK4b559rTJzHHhjcz" } - } + ], + "title": "Start", + "graphCoordinates": { "x": 0, "y": 0 } }, - "allIds": [ - "nP5oWm7PxigMupyWpPLq24", - "scH9qdXwFfAScoavj6UzQNT", - "shZdc8Qw48domEbS7vLW5eN", - "s4z6G3MGAyhXChU9jakQWer" - ] - }, - "choiceItems": { "byId": {}, "allIds": [] }, - "variables": { - "byId": { - "ifXp66N1meAtoUDcbqWxuD": { - "id": "ifXp66N1meAtoUDcbqWxuD", - "name": "Email" - } + { + "id": "mSvUFogQH16bQDN1iGWF66", + "graphCoordinates": { "x": 324, "y": 209 }, + "title": "Block #1", + "steps": [ + { + "id": "spDLmDCZfNJu4DrZ1MUg84c", + "blockId": "mSvUFogQH16bQDN1iGWF66", + "type": "email input", + "options": { + "labels": { "button": "Send", "placeholder": "Type your email..." }, + "variableId": "qyLW6xD1AyLeedso2tHmhw" + }, + "outgoingEdgeId": "4yg9V76fdDntpDEw6H3tvU" + } + ] }, - "allIds": ["ifXp66N1meAtoUDcbqWxuD"] - }, - "edges": { - "byId": { - "kCLXGLpiM2F6pn4wFYc1f5": { - "from": { - "blockId": "kPupUcEn7TcBGKHUpgK2Q5", - "stepId": "nP5oWm7PxigMupyWpPLq24" - }, - "to": { "blockId": "bi4J5fv9DFn1zPSqGf8eRht" }, - "id": "kCLXGLpiM2F6pn4wFYc1f5" + { + "id": "jd4S6BQsUQ2RuKieHXYKs9", + "graphCoordinates": { "x": 655, "y": 363 }, + "title": "Block #2", + "steps": [ + { + "id": "s1ZvUqWxV6b8UgFGYWh39pV", + "blockId": "jd4S6BQsUQ2RuKieHXYKs9", + "type": "Google Sheets", + "options": {}, + "outgoingEdgeId": "tBsPNYzMW1mMSvFMHZpmx8" + } + ] + }, + { + "id": "pd3PECJqHB9xHMfc52SbrZ", + "graphCoordinates": { "x": 292, "y": 509 }, + "title": "Block #3", + "steps": [ + { + "id": "sdECvSYszxBaZHH5TuWm11h", + "blockId": "pd3PECJqHB9xHMfc52SbrZ", + "type": "text", + "content": { + "html": "
Your name is: {{First name}} {{Last name}}
", + "richText": [ + { + "type": "p", + "children": [ + { "text": "Your name is: {{First name}} {{Last name}}" } + ] + } + ], + "plainText": "Your name is: {{First name}} {{Last name}}" + } + } + ] + } + ], + "variables": [{ "id": "qyLW6xD1AyLeedso2tHmhw", "name": "Email" }], + "edges": [ + { + "from": { + "blockId": "bSHn2HQZ1sKji5pd9Nmejf", + "stepId": "qYmbSFBxCvGCgTvQTg9HeH" }, - "7Czn5hJFUfpkRGtwGnKxtt": { - "from": { - "blockId": "bi4J5fv9DFn1zPSqGf8eRht", - "stepId": "scH9qdXwFfAScoavj6UzQNT" - }, - "to": { "blockId": "bwqGhUsa2SKaxXSrKtapVc8" }, - "id": "7Czn5hJFUfpkRGtwGnKxtt" - }, - "eMhGokwHMDRDrynSvpiRje": { - "from": { - "blockId": "bwqGhUsa2SKaxXSrKtapVc8", - "stepId": "shZdc8Qw48domEbS7vLW5eN" - }, - "to": { "blockId": "bqmgS9hLUu2RA2oxVv7hMka" }, - "id": "eMhGokwHMDRDrynSvpiRje" - } + "to": { "blockId": "mSvUFogQH16bQDN1iGWF66" }, + "id": "jdQnqJK4b559rTJzHHhjcz" }, - "allIds": [ - "kCLXGLpiM2F6pn4wFYc1f5", - "7Czn5hJFUfpkRGtwGnKxtt", - "eMhGokwHMDRDrynSvpiRje" - ] - }, + { + "from": { + "blockId": "mSvUFogQH16bQDN1iGWF66", + "stepId": "spDLmDCZfNJu4DrZ1MUg84c" + }, + "to": { "blockId": "jd4S6BQsUQ2RuKieHXYKs9" }, + "id": "4yg9V76fdDntpDEw6H3tvU" + }, + { + "from": { + "blockId": "jd4S6BQsUQ2RuKieHXYKs9", + "stepId": "s1ZvUqWxV6b8UgFGYWh39pV" + }, + "to": { "blockId": "pd3PECJqHB9xHMfc52SbrZ" }, + "id": "tBsPNYzMW1mMSvFMHZpmx8" + } + ], "theme": { "chat": { "inputs": { diff --git a/apps/builder/playwright/fixtures/typebots/integrations/webhook.json b/apps/builder/playwright/fixtures/typebots/integrations/webhook.json index 4c5606360..12b7facf2 100644 --- a/apps/builder/playwright/fixtures/typebots/integrations/webhook.json +++ b/apps/builder/playwright/fixtures/typebots/integrations/webhook.json @@ -1,155 +1,103 @@ { - "id": "ckz478ggv1144eo1a5euf9twl", - "createdAt": "2022-02-01T14:11:20.287Z", - "updatedAt": "2022-02-01T14:11:20.286Z", - "name": "Webhook", - "ownerId": "ckz478eaj1091eo1amyo1me1z", + "id": "ckz8gli9e9842no1afuppdn0z", + "createdAt": "2022-02-04T13:44:30.386Z", + "updatedAt": "2022-02-04T13:44:30.386Z", + "name": "My typebot", + "ownerId": "ckz6t9iep0006k31a22j05fwq", "publishedTypebotId": null, "folderId": null, - "blocks": { - "byId": { - "q7gjzJu7wBFycca5dNvZek": { - "id": "q7gjzJu7wBFycca5dNvZek", - "title": "Start", - "stepIds": ["da1KErxMzczHwaM25vQtFP"], - "graphCoordinates": { "x": 0, "y": 0 } - }, - "bUUqjxyAFZkKzjByqEaEzV": { - "id": "bUUqjxyAFZkKzjByqEaEzV", - "graphCoordinates": { "x": 248, "y": 247 }, - "title": "Block 1", - "stepIds": ["siAj9x5LZ8W5cviqznX82T3", "s5GToHCtqZhwpygDuTb3tu4"] - }, - "ifpYvoBnYU2X3B3RgwfeNJ": { - "id": "ifpYvoBnYU2X3B3RgwfeNJ", - "graphCoordinates": { "x": 690, "y": 504 }, - "title": "Block 2", - "stepIds": ["sjDhaBWVLd2Ep7N3WryJGQJ"] - } - }, - "allIds": [ - "q7gjzJu7wBFycca5dNvZek", - "bUUqjxyAFZkKzjByqEaEzV", - "ifpYvoBnYU2X3B3RgwfeNJ" - ] - }, - "steps": { - "byId": { - "da1KErxMzczHwaM25vQtFP": { - "id": "da1KErxMzczHwaM25vQtFP", - "type": "start", - "label": "Start", - "blockId": "q7gjzJu7wBFycca5dNvZek", - "edgeId": "mcxdssnDkbvJBZ6d51XDey" - }, - "s5GToHCtqZhwpygDuTb3tu4": { - "id": "s5GToHCtqZhwpygDuTb3tu4", - "blockId": "bUUqjxyAFZkKzjByqEaEzV", - "type": "choice input", - "options": { - "buttonLabel": "Send", - "isMultipleChoice": false, - "itemIds": ["ddSjZkft27gQnZAEeXtQny"] + "blocks": [ + { + "id": "p6GeeRXHgwiJeoJRBkKaMJ", + "steps": [ + { + "id": "iDS7jFemUsQ7Sp3eu3xg3w", + "type": "start", + "label": "Start", + "blockId": "p6GeeRXHgwiJeoJRBkKaMJ", + "outgoingEdgeId": "cyEJPaLU7AchnBSaeWoyiS" } - }, - "siAj9x5LZ8W5cviqznX82T3": { - "id": "siAj9x5LZ8W5cviqznX82T3", - "blockId": "bUUqjxyAFZkKzjByqEaEzV", - "type": "text", - "content": { - "html": "
Ready?
", - "richText": [{ "type": "p", "children": [{ "text": "Ready?" }] }], - "plainText": "Ready?" - } - }, - "sjDhaBWVLd2Ep7N3WryJGQJ": { - "id": "sjDhaBWVLd2Ep7N3WryJGQJ", - "blockId": "ifpYvoBnYU2X3B3RgwfeNJ", - "type": "Webhook", - "options": { - "responseVariableMapping": { "byId": {}, "allIds": [] }, - "variablesForTest": { "byId": {}, "allIds": [] }, - "webhookId": "3nxQGoMMXpA6K5iuhGFW5S" - } - } + ], + "title": "Start", + "graphCoordinates": { "x": 0, "y": 0 } }, - "allIds": [ - "da1KErxMzczHwaM25vQtFP", - "s5GToHCtqZhwpygDuTb3tu4", - "siAj9x5LZ8W5cviqznX82T3", - "sjDhaBWVLd2Ep7N3WryJGQJ" - ] - }, - "choiceItems": { - "byId": { - "ddSjZkft27gQnZAEeXtQny": { - "id": "ddSjZkft27gQnZAEeXtQny", - "stepId": "s5GToHCtqZhwpygDuTb3tu4", - "content": "Go", - "edgeId": "x6cbRGrLAVYy4ymAg5tfp9" - } - }, - "allIds": ["ddSjZkft27gQnZAEeXtQny"] - }, - "variables": { - "byId": { - "oASkBtoLqkYNqeakcjZH4L": { - "id": "oASkBtoLqkYNqeakcjZH4L", - "name": "secret 1" - }, - "4tvkRmf32wiTsXrYoqyhfr": { - "id": "4tvkRmf32wiTsXrYoqyhfr", - "name": "secret 2" - }, - "jEg1FvkCU5S5owNAxXFsHL": { - "id": "jEg1FvkCU5S5owNAxXFsHL", - "name": "secret 3" - }, - "rEoE1ehHzgx8X3d3UPGDHg": { - "id": "rEoE1ehHzgx8X3d3UPGDHg", - "name": "secret 4" - } - }, - "allIds": [ - "oASkBtoLqkYNqeakcjZH4L", - "4tvkRmf32wiTsXrYoqyhfr", - "jEg1FvkCU5S5owNAxXFsHL", - "rEoE1ehHzgx8X3d3UPGDHg" - ] - }, - "webhooks": { - "byId": { - "3nxQGoMMXpA6K5iuhGFW5S": { - "id": "3nxQGoMMXpA6K5iuhGFW5S", - "method": "GET", - "headers": { "byId": {}, "allIds": [] }, - "queryParams": { "byId": {}, "allIds": [] } - } - }, - "allIds": ["3nxQGoMMXpA6K5iuhGFW5S"] - }, - "edges": { - "byId": { - "mcxdssnDkbvJBZ6d51XDey": { - "from": { - "blockId": "q7gjzJu7wBFycca5dNvZek", - "stepId": "da1KErxMzczHwaM25vQtFP" + { + "id": "kBneEpKdMYrF65XxUQ5GS7", + "graphCoordinates": { "x": 260, "y": 186 }, + "title": "Block #1", + "steps": [ + { + "id": "skSkZ4PNP7m1gYvu9Ew6ngM", + "blockId": "kBneEpKdMYrF65XxUQ5GS7", + "type": "text", + "content": { + "html": "
Ready?
", + "richText": [{ "type": "p", "children": [{ "text": "Ready?" }] }], + "plainText": "Ready?" + } }, - "to": { "blockId": "bUUqjxyAFZkKzjByqEaEzV" }, - "id": "mcxdssnDkbvJBZ6d51XDey" - }, - "x6cbRGrLAVYy4ymAg5tfp9": { - "from": { - "blockId": "bUUqjxyAFZkKzjByqEaEzV", - "stepId": "s5GToHCtqZhwpygDuTb3tu4", - "buttonId": "ddSjZkft27gQnZAEeXtQny" - }, - "to": { "blockId": "ifpYvoBnYU2X3B3RgwfeNJ" }, - "id": "x6cbRGrLAVYy4ymAg5tfp9" - } + { + "id": "sh6ZVRA3o72y6BEiNKVcoma", + "blockId": "kBneEpKdMYrF65XxUQ5GS7", + "type": "choice input", + "options": { "buttonLabel": "Send", "isMultipleChoice": false }, + "items": [ + { + "id": "rr5mKKBPq73ZrfXZ3uuupz", + "stepId": "sh6ZVRA3o72y6BEiNKVcoma", + "type": 0, + "content": "Go", + "outgoingEdgeId": "1sLicz8gq2QxytFTwBd8ac" + } + ] + } + ] }, - "allIds": ["mcxdssnDkbvJBZ6d51XDey", "x6cbRGrLAVYy4ymAg5tfp9"] - }, + { + "id": "8XnDM1QsqPms4LQHh8q3Jo", + "graphCoordinates": { "x": 646, "y": 511 }, + "title": "Block #2", + "steps": [ + { + "id": "soSmiE7zyb3WF77GxFxAjYX", + "blockId": "8XnDM1QsqPms4LQHh8q3Jo", + "type": "Webhook", + "options": { "responseVariableMapping": [], "variablesForTest": [] }, + "webhook": { + "id": "2L9mPYsLAXdXwcnGVK6pv9", + "method": "GET", + "headers": [], + "queryParams": [] + } + } + ] + } + ], + "variables": [ + { "id": "var1", "name": "secret 1" }, + { "id": "var2", "name": "secret 2" }, + { "id": "var3", "name": "secret 3" }, + { "id": "var4", "name": "secret 4" } + ], + "edges": [ + { + "from": { + "blockId": "p6GeeRXHgwiJeoJRBkKaMJ", + "stepId": "iDS7jFemUsQ7Sp3eu3xg3w" + }, + "to": { "blockId": "kBneEpKdMYrF65XxUQ5GS7" }, + "id": "cyEJPaLU7AchnBSaeWoyiS" + }, + { + "from": { + "blockId": "kBneEpKdMYrF65XxUQ5GS7", + "stepId": "sh6ZVRA3o72y6BEiNKVcoma", + "itemId": "rr5mKKBPq73ZrfXZ3uuupz" + }, + "to": { "blockId": "8XnDM1QsqPms4LQHh8q3Jo" }, + "id": "1sLicz8gq2QxytFTwBd8ac" + } + ], "theme": { "chat": { "inputs": { diff --git a/apps/builder/playwright/fixtures/typebots/integrations/webhook.png b/apps/builder/playwright/fixtures/typebots/integrations/webhook.png new file mode 100644 index 000000000..4077ad2a6 Binary files /dev/null and b/apps/builder/playwright/fixtures/typebots/integrations/webhook.png differ diff --git a/apps/builder/playwright/fixtures/typebots/integrations/webhookPreview.json b/apps/builder/playwright/fixtures/typebots/integrations/webhookPreview.json deleted file mode 100644 index 70ab65ed8..000000000 --- a/apps/builder/playwright/fixtures/typebots/integrations/webhookPreview.json +++ /dev/null @@ -1,275 +0,0 @@ -{ - "id": "bdFW2HHjMoEFmqHtFre9Xi8", - "createdAt": "2022-01-21T07:55:14.727Z", - "updatedAt": "2022-01-21T07:55:14.727Z", - "name": "My typebot", - "ownerId": "user2", - "publishedTypebotId": null, - "folderId": null, - "blocks": { - "byId": { - "3kH2sUjVThQDWmqdoKnGk5": { - "id": "3kH2sUjVThQDWmqdoKnGk5", - "title": "Start", - "stepIds": ["oxTsU2C1RX5QHuyY8qjHAM"], - "graphCoordinates": { "x": 42, "y": 13 } - }, - "b9mSgu7RKmK4xuiTVQP5Me8": { - "id": "b9mSgu7RKmK4xuiTVQP5Me8", - "title": "Block #3", - "stepIds": ["ssLd2wjExS9qWRur4tZDU1Z"], - "graphCoordinates": { "x": 300, "y": 550 } - }, - "bdFW2HHjMoEFmqHtFre9Xi8": { - "id": "bdFW2HHjMoEFmqHtFre9Xi8", - "title": "Block #2", - "stepIds": ["sgkADMK25y9P9V3vjwjBaac", "ssEiEECKSFkA44dGDceHxKw"], - "graphCoordinates": { "x": 121, "y": 227 } - }, - "bmz4rc8r19H2C6b7soxzby4": { - "id": "bmz4rc8r19H2C6b7soxzby4", - "title": "Block #4", - "graphCoordinates": { "x": 632, "y": 279 }, - "stepIds": ["sgTWsRM1qF2YoYLuGo3Z3pU"] - } - }, - "allIds": [ - "3kH2sUjVThQDWmqdoKnGk5", - "bdFW2HHjMoEFmqHtFre9Xi8", - "b9mSgu7RKmK4xuiTVQP5Me8", - "bmz4rc8r19H2C6b7soxzby4" - ] - }, - "steps": { - "byId": { - "oxTsU2C1RX5QHuyY8qjHAM": { - "id": "oxTsU2C1RX5QHuyY8qjHAM", - "type": "start", - "label": "Start", - "edgeId": "25yX9DnQgdafpdAjfAu5Fp", - "blockId": "3kH2sUjVThQDWmqdoKnGk5" - }, - "sgkADMK25y9P9V3vjwjBaac": { - "id": "sgkADMK25y9P9V3vjwjBaac", - "type": "text", - "blockId": "bdFW2HHjMoEFmqHtFre9Xi8", - "content": { - "html": "
Ready?
", - "richText": [{ "type": "p", "children": [{ "text": "Ready?" }] }], - "plainText": "Ready?" - } - }, - "ssEiEECKSFkA44dGDceHxKw": { - "id": "ssEiEECKSFkA44dGDceHxKw", - "type": "choice input", - "edgeId": "oxEEtym3NfDf34NCipzjRQ", - "blockId": "bdFW2HHjMoEFmqHtFre9Xi8", - "options": { "itemIds": ["q69Ex7LacPrH9QUMeosRnB"] } - }, - "ssLd2wjExS9qWRur4tZDU1Z": { - "id": "ssLd2wjExS9qWRur4tZDU1Z", - "type": "Webhook", - "blockId": "b9mSgu7RKmK4xuiTVQP5Me8", - "options": { - "webhookId": "4h4Kk3Q1qGy7gFzpZtWVpU", - "variablesForTest": { - "byId": { - "6pMn1xm1y3xWVSdJetMAJH": { - "id": "6pMn1xm1y3xWVSdJetMAJH", - "variableId": "oASkBtoLqkYNqeakcjZH4L", - "value": "secret1" - }, - "ettAiB75uoFWnJyPS7gn5k": { - "id": "ettAiB75uoFWnJyPS7gn5k", - "variableId": "4tvkRmf32wiTsXrYoqyhfr", - "value": "secret2" - }, - "kKpD3Q4YvFQ7CGWiZxJF4s": { - "id": "kKpD3Q4YvFQ7CGWiZxJF4s", - "variableId": "jEg1FvkCU5S5owNAxXFsHL", - "value": "secret3" - }, - "xjUC5Q3msXCw9fwqpNdoSx": { - "id": "xjUC5Q3msXCw9fwqpNdoSx", - "variableId": "rEoE1ehHzgx8X3d3UPGDHg", - "value": "secret4" - } - }, - "allIds": [ - "6pMn1xm1y3xWVSdJetMAJH", - "ettAiB75uoFWnJyPS7gn5k", - "kKpD3Q4YvFQ7CGWiZxJF4s", - "xjUC5Q3msXCw9fwqpNdoSx" - ] - }, - "responseVariableMapping": { - "byId": { - "o53h6M1sgHJfDTY5C3YEaT": { - "id": "o53h6M1sgHJfDTY5C3YEaT", - "bodyPath": "data[0].name", - "variableId": "4kVx5uf8W1XP6WsfJEvt8v" - } - }, - "allIds": ["o53h6M1sgHJfDTY5C3YEaT"] - } - }, - "edgeId": "81SjKnxuUgrPmXvvJJihHM" - }, - "sgTWsRM1qF2YoYLuGo3Z3pU": { - "id": "sgTWsRM1qF2YoYLuGo3Z3pU", - "blockId": "bmz4rc8r19H2C6b7soxzby4", - "type": "text", - "content": { - "html": "
His name is {{Name}}
", - "richText": [ - { "type": "p", "children": [{ "text": "His name is {{Name}}" }] } - ], - "plainText": "His name is {{Name}}" - } - } - }, - "allIds": [ - "oxTsU2C1RX5QHuyY8qjHAM", - "sgkADMK25y9P9V3vjwjBaac", - "ssEiEECKSFkA44dGDceHxKw", - "ssLd2wjExS9qWRur4tZDU1Z", - "sgTWsRM1qF2YoYLuGo3Z3pU" - ] - }, - "choiceItems": { - "byId": { - "q69Ex7LacPrH9QUMeosRnB": { - "id": "q69Ex7LacPrH9QUMeosRnB", - "stepId": "ssEiEECKSFkA44dGDceHxKw", - "content": "Go" - } - }, - "allIds": ["q69Ex7LacPrH9QUMeosRnB"] - }, - "webhooks": { "byId": {}, "allIds": [] }, - "variables": { - "byId": { - "4tvkRmf32wiTsXrYoqyhfr": { - "id": "4tvkRmf32wiTsXrYoqyhfr", - "name": "secret 2", - "value": "secret2" - }, - "jEg1FvkCU5S5owNAxXFsHL": { - "id": "jEg1FvkCU5S5owNAxXFsHL", - "name": "secret 3", - "value": "secret3" - }, - "oASkBtoLqkYNqeakcjZH4L": { - "id": "oASkBtoLqkYNqeakcjZH4L", - "name": "secret 1", - "value": "secret1" - }, - "rEoE1ehHzgx8X3d3UPGDHg": { - "id": "rEoE1ehHzgx8X3d3UPGDHg", - "name": "secret 4", - "value": "secret4" - }, - "4kVx5uf8W1XP6WsfJEvt8v": { - "id": "4kVx5uf8W1XP6WsfJEvt8v", - "name": "Name" - } - }, - "allIds": [ - "oASkBtoLqkYNqeakcjZH4L", - "4tvkRmf32wiTsXrYoqyhfr", - "jEg1FvkCU5S5owNAxXFsHL", - "rEoE1ehHzgx8X3d3UPGDHg", - "4kVx5uf8W1XP6WsfJEvt8v" - ] - }, - "webhooks": { - "byId": { - "4h4Kk3Q1qGy7gFzpZtWVpU": { - "id": "4h4Kk3Q1qGy7gFzpZtWVpU", - "url": "http://localhost:3000/api/mock/webhook", - "queryParams": { - "byId": { - "hwGB11cA7RaYnaqH7gYyuQ": { - "id": "hwGB11cA7RaYnaqH7gYyuQ", - "key": "firstParam", - "value": "{{secret 1}}" - }, - "6ux2FZjhNc4vfqNUDuCkxn": { - "id": "6ux2FZjhNc4vfqNUDuCkxn", - "key": "secondParam", - "value": "{{secret 2}}" - } - }, - "allIds": ["hwGB11cA7RaYnaqH7gYyuQ", "6ux2FZjhNc4vfqNUDuCkxn"] - }, - "headers": { - "byId": { - "ayTB2cFRKMo6oH9t9KS8SA": { - "id": "ayTB2cFRKMo6oH9t9KS8SA", - "key": "Custom-Typebot", - "value": "{{secret 3}}" - } - }, - "allIds": ["ayTB2cFRKMo6oH9t9KS8SA"] - }, - "method": "POST", - "body": "{ \"customField\": \"{{secret 4}}\" }" - } - }, - "allIds": ["4h4Kk3Q1qGy7gFzpZtWVpU"] - }, - "edges": { - "byId": { - "25yX9DnQgdafpdAjfAu5Fp": { - "id": "25yX9DnQgdafpdAjfAu5Fp", - "to": { "blockId": "bdFW2HHjMoEFmqHtFre9Xi8" }, - "from": { - "stepId": "oxTsU2C1RX5QHuyY8qjHAM", - "blockId": "3kH2sUjVThQDWmqdoKnGk5" - } - }, - "oxEEtym3NfDf34NCipzjRQ": { - "id": "oxEEtym3NfDf34NCipzjRQ", - "to": { "blockId": "b9mSgu7RKmK4xuiTVQP5Me8" }, - "from": { - "stepId": "ssEiEECKSFkA44dGDceHxKw", - "blockId": "bdFW2HHjMoEFmqHtFre9Xi8" - } - }, - "81SjKnxuUgrPmXvvJJihHM": { - "from": { - "blockId": "b9mSgu7RKmK4xuiTVQP5Me8", - "stepId": "ssLd2wjExS9qWRur4tZDU1Z" - }, - "to": { "blockId": "bmz4rc8r19H2C6b7soxzby4" }, - "id": "81SjKnxuUgrPmXvvJJihHM" - } - }, - "allIds": [ - "25yX9DnQgdafpdAjfAu5Fp", - "oxEEtym3NfDf34NCipzjRQ", - "81SjKnxuUgrPmXvvJJihHM" - ] - }, - "theme": { - "chat": { - "inputs": { - "color": "#303235", - "backgroundColor": "#FFFFFF", - "placeholderColor": "#9095A0" - }, - "buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" }, - "hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" }, - "guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" } - }, - "general": { "font": "Open Sans", "background": { "type": "None" } } - }, - "settings": { - "general": { "isBrandingEnabled": true }, - "metadata": { - "description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form." - }, - "typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 } - }, - "publicId": null -} diff --git a/apps/builder/playwright/fixtures/typebots/integrations/webhookPreview.png b/apps/builder/playwright/fixtures/typebots/integrations/webhookPreview.png deleted file mode 100644 index 8b98e2c9e..000000000 Binary files a/apps/builder/playwright/fixtures/typebots/integrations/webhookPreview.png and /dev/null differ diff --git a/apps/builder/playwright/fixtures/typebots/logic/condition.json b/apps/builder/playwright/fixtures/typebots/logic/condition.json index 8fbfe4265..9bd7c9ff5 100644 --- a/apps/builder/playwright/fixtures/typebots/logic/condition.json +++ b/apps/builder/playwright/fixtures/typebots/logic/condition.json @@ -1,228 +1,217 @@ { - "id": "ckylsd52p0114z31aobllswmu", - "createdAt": "2022-01-19T16:55:13.393Z", - "updatedAt": "2022-01-19T16:55:13.393Z", + "id": "ckz8gpmgr10008no1a1mq4q1l2", + "createdAt": "2022-02-04T13:47:42.459Z", + "updatedAt": "2022-02-04T13:47:42.459Z", "name": "My typebot", - "ownerId": "ckylsbdf60088z31ayqytest6", + "ownerId": "ckz6t9iep0006k31a22j05fwq", "publishedTypebotId": null, "folderId": null, - "webhooks": { "byId": {}, "allIds": [] }, - "blocks": { - "byId": { - "2x83WHtEBkiv7pk7KgqJwZ": { - "id": "2x83WHtEBkiv7pk7KgqJwZ", - "title": "Start", - "stepIds": ["1A76iZBgXG7hvkG2koCxe4"], - "graphCoordinates": { "x": 0, "y": 0 } - }, - "bwga7RwqQWbowdHph27DM1N": { - "id": "bwga7RwqQWbowdHph27DM1N", - "title": "Block #2", - "graphCoordinates": { "x": 78, "y": 224 }, - "stepIds": ["srwUKaUFFmehppJ2ZDqp4xG", "sxvzuo48GHi3AcAfmiFyYC1"] - }, - "bu8whx817bJBG37FQrtD5dD": { - "id": "bu8whx817bJBG37FQrtD5dD", - "title": "Block #3", - "graphCoordinates": { "x": 430, "y": 287 }, - "stepIds": ["ituVWW1AvQeVdFHTwsiVao", "5SLc4whZooZVUfr1bmTNSC"] - }, - "b59jwmEdwZUvJszV394x44u": { - "id": "b59jwmEdwZUvJszV394x44u", - "title": "Block #4", - "graphCoordinates": { "x": 844, "y": 185 }, - "stepIds": ["sm1YcKTL9cQMCGywzo1wyBB"] - }, - "baVF9HqhuSnLDZqY9eRPpcp": { - "id": "baVF9HqhuSnLDZqY9eRPpcp", - "title": "Block #5", - "graphCoordinates": { "x": 841, "y": 356 }, - "stepIds": ["sb3o6J8Fybg6u8KuayKviJq"] - }, - "b9aEH46RHuZWTdQwZJ6KBWR": { - "id": "b9aEH46RHuZWTdQwZJ6KBWR", - "title": "Block #6", - "graphCoordinates": { "x": 839, "y": 523 }, - "stepIds": ["scKogEJSTq4kPeHRhwTTjit"] - } - }, - "allIds": [ - "2x83WHtEBkiv7pk7KgqJwZ", - "bwga7RwqQWbowdHph27DM1N", - "bu8whx817bJBG37FQrtD5dD", - "b59jwmEdwZUvJszV394x44u", - "baVF9HqhuSnLDZqY9eRPpcp", - "b9aEH46RHuZWTdQwZJ6KBWR" - ] - }, - "steps": { - "byId": { - "1A76iZBgXG7hvkG2koCxe4": { - "id": "1A76iZBgXG7hvkG2koCxe4", - "type": "start", - "label": "Start", - "blockId": "2x83WHtEBkiv7pk7KgqJwZ", - "edgeId": "jjNy2hYgrQgPS9EBMKA7MH" - }, - "srwUKaUFFmehppJ2ZDqp4xG": { - "id": "srwUKaUFFmehppJ2ZDqp4xG", - "blockId": "bwga7RwqQWbowdHph27DM1N", - "type": "text", - "content": { - "html": "
How old are you?
", - "richText": [ - { "type": "p", "children": [{ "text": "How old are you?" }] } - ], - "plainText": "How old are you?" + "blocks": [ + { + "id": "cN46uqNAR3ohjrS8jHJ6xT", + "steps": [ + { + "id": "nzijwLtLTAZfNNCN7kEpn5", + "type": "start", + "label": "Start", + "blockId": "cN46uqNAR3ohjrS8jHJ6xT", + "outgoingEdgeId": "7wxB76VK81JsXMX9jU9dbQ" } - }, - "sxvzuo48GHi3AcAfmiFyYC1": { - "id": "sxvzuo48GHi3AcAfmiFyYC1", - "blockId": "bwga7RwqQWbowdHph27DM1N", - "type": "number input", - "options": { "variableId": "dEz689uVm8AxUM8TrbQd2t" }, - "edgeId": "7mcWaWohM9zGtLX8ZSnqFy" - }, - "ituVWW1AvQeVdFHTwsiVao": { - "id": "ituVWW1AvQeVdFHTwsiVao", - "blockId": "bu8whx817bJBG37FQrtD5dD", - "type": "Condition", - "options": { - "comparisons": { - "byId": {}, - "allIds": [] + ], + "title": "Start", + "graphCoordinates": { "x": 0, "y": 0 } + }, + { + "id": "eh2ohNATnGg6RTdjG9h5kb", + "steps": [ + { + "id": "sb6xdkJRr6P6BxtTM3ku5JD", + "type": "text", + "blockId": "eh2ohNATnGg6RTdjG9h5kb", + "content": { + "html": "
How old are you?
", + "richText": [ + { "type": "p", "children": [{ "text": "How old are you?" }] } + ], + "plainText": "How old are you?" + } + }, + { + "id": "ssyBKZve7bihSxUASYTruZA", + "type": "number input", + "blockId": "eh2ohNATnGg6RTdjG9h5kb", + "options": { + "labels": { "button": "Send", "placeholder": "Type a number..." }, + "variableId": "iDJzzyzAY2jrapm3NwhGMz" }, - "logicalOperator": "AND" - }, - "trueEdgeId": "iBPsFyBsPv6Rbdfo2QdJyi" - }, - "5SLc4whZooZVUfr1bmTNSC": { - "id": "5SLc4whZooZVUfr1bmTNSC", - "blockId": "bu8whx817bJBG37FQrtD5dD", - "type": "Condition", - "options": { - "comparisons": { - "byId": {}, - "allIds": [] - }, - "logicalOperator": "AND" - }, - "trueEdgeId": "354PJ2jD5U3J2APqLsPJrp", - "falseEdgeId": "94bmeCLigEUUpWYw2xsAVB" - }, - "sm1YcKTL9cQMCGywzo1wyBB": { - "id": "sm1YcKTL9cQMCGywzo1wyBB", - "blockId": "b59jwmEdwZUvJszV394x44u", - "type": "text", - "content": { - "html": "
You are older than 80
", - "richText": [ - { "type": "p", "children": [{ "text": "You are older than 80" }] } - ], - "plainText": "You are older than 80" + "outgoingEdgeId": "r8LX7iuEXxjF5SW5dbS6qT" } - }, - "sb3o6J8Fybg6u8KuayKviJq": { - "id": "sb3o6J8Fybg6u8KuayKviJq", - "blockId": "baVF9HqhuSnLDZqY9eRPpcp", - "type": "text", - "content": { - "html": "
You are older than 20
", - "richText": [ - { "type": "p", "children": [{ "text": "You are older than 20" }] } + ], + "title": "Block #1", + "graphCoordinates": { "x": 159, "y": 224 } + }, + { + "id": "eMk84KvFM53sBxchTeackR", + "steps": [ + { + "id": "s5hz7HQki66cwELvk2738MJ", + "blockId": "eMk84KvFM53sBxchTeackR", + "type": "Condition", + "items": [ + { + "id": "56s4R9THRKeoJtBdKzEGLn", + "stepId": "s5hz7HQki66cwELvk2738MJ", + "type": 1, + "content": { + "comparisons": [ + { + "id": "wxWqL7JuZhy9uwCZNPDJjM" + } + ], + "logicalOperator": "AND" + }, + "outgoingEdgeId": "nDjMjM11xPQF7c9Be6ukdY" + } + ] + }, + { + "id": "sv8uvEXgYWQNMfZWcdbfyCs", + "type": "Condition", + "items": [ + { + "id": "ijYfW38tGhCMRrCtmR3bcr", + "type": 1, + "stepId": "sv8uvEXgYWQNMfZWcdbfyCs", + "content": { + "comparisons": [ + { + "id": "3di6P3ypWf3XusgBLLHYBE" + } + ], + "logicalOperator": "AND" + }, + "outgoingEdgeId": "s1uVbzP1zMMJjsCguAfhGV" + } ], - "plainText": "You are older than 20" + "blockId": "eMk84KvFM53sBxchTeackR", + "outgoingEdgeId": "hVScFHNzToiEsMfYVv5AWZ" } - }, - "scKogEJSTq4kPeHRhwTTjit": { - "id": "scKogEJSTq4kPeHRhwTTjit", - "blockId": "b9aEH46RHuZWTdQwZJ6KBWR", - "type": "text", - "content": { - "html": "
You are younger than 20
", - "richText": [ - { "type": "p", "children": [{ "text": "You are younger than 20" }] } - ], - "plainText": "You are younger than 20" + ], + "title": "Block #2", + "graphCoordinates": { "x": 561, "y": 295 } + }, + { + "id": "fGrzjahWecA8hoNMRrLSwn", + "steps": [ + { + "id": "soZqPdPUjYAht9nHmVpba1Q", + "type": "text", + "blockId": "fGrzjahWecA8hoNMRrLSwn", + "content": { + "html": "
You are older than 80
", + "richText": [ + { + "type": "p", + "children": [{ "text": "You are older than 80" }] + } + ], + "plainText": "You are older than 80" + } } - } + ], + "title": "Block #3", + "graphCoordinates": { "x": 936, "y": 63 } }, - "allIds": [ - "1A76iZBgXG7hvkG2koCxe4", - "srwUKaUFFmehppJ2ZDqp4xG", - "sxvzuo48GHi3AcAfmiFyYC1", - "ituVWW1AvQeVdFHTwsiVao", - "5SLc4whZooZVUfr1bmTNSC", - "sxvzuo48GHi3AcAfmiFyYC1", - "sm1YcKTL9cQMCGywzo1wyBB", - "sb3o6J8Fybg6u8KuayKviJq", - "scKogEJSTq4kPeHRhwTTjit" - ] - }, - "choiceItems": { "byId": {}, "allIds": [] }, - "variables": { - "byId": { - "dEz689uVm8AxUM8TrbQd2t": { - "id": "dEz689uVm8AxUM8TrbQd2t", - "name": "Age" - } + { + "id": "49Jv45UJi9R3U4FuWS8R2c", + "steps": [ + { + "id": "svipUacs1sDk9KDxzaGhnsG", + "type": "text", + "blockId": "49Jv45UJi9R3U4FuWS8R2c", + "content": { + "html": "
You are older than 20
", + "richText": [ + { + "type": "p", + "children": [{ "text": "You are older than 20" }] + } + ], + "plainText": "You are older than 20" + } + } + ], + "title": "Block #4", + "graphCoordinates": { "x": 950, "y": 298 } }, - "allIds": ["dEz689uVm8AxUM8TrbQd2t"] - }, - "edges": { - "byId": { - "jjNy2hYgrQgPS9EBMKA7MH": { - "from": { - "blockId": "2x83WHtEBkiv7pk7KgqJwZ", - "stepId": "1A76iZBgXG7hvkG2koCxe4" - }, - "to": { "blockId": "bwga7RwqQWbowdHph27DM1N" }, - "id": "jjNy2hYgrQgPS9EBMKA7MH" + { + "id": "fD28kefdySKK7XA7SyTozC", + "steps": [ + { + "id": "spHJ7v9bDdVrFfuF2zg3YNR", + "type": "text", + "blockId": "fD28kefdySKK7XA7SyTozC", + "content": { + "html": "
You are younger than 20
", + "richText": [ + { + "type": "p", + "children": [{ "text": "You are younger than 20" }] + } + ], + "plainText": "You are younger than 20" + } + } + ], + "title": "Block #5", + "graphCoordinates": { "x": 985, "y": 537 } + } + ], + "variables": [{ "id": "iDJzzyzAY2jrapm3NwhGMz", "name": "Age" }], + "edges": [ + { + "from": { + "blockId": "eMk84KvFM53sBxchTeackR", + "stepId": "s5hz7HQki66cwELvk2738MJ", + "itemId": "56s4R9THRKeoJtBdKzEGLn" }, - "iBPsFyBsPv6Rbdfo2QdJyi": { - "from": { - "blockId": "bu8whx817bJBG37FQrtD5dD", - "stepId": "ituVWW1AvQeVdFHTwsiVao", - "conditionType": "true" - }, - "to": { "blockId": "b59jwmEdwZUvJszV394x44u" }, - "id": "iBPsFyBsPv6Rbdfo2QdJyi" - }, - "354PJ2jD5U3J2APqLsPJrp": { - "from": { - "blockId": "bu8whx817bJBG37FQrtD5dD", - "stepId": "5SLc4whZooZVUfr1bmTNSC", - "conditionType": "true" - }, - "to": { "blockId": "baVF9HqhuSnLDZqY9eRPpcp" }, - "id": "354PJ2jD5U3J2APqLsPJrp" - }, - "94bmeCLigEUUpWYw2xsAVB": { - "from": { - "blockId": "bu8whx817bJBG37FQrtD5dD", - "stepId": "5SLc4whZooZVUfr1bmTNSC", - "conditionType": "false" - }, - "to": { "blockId": "b9aEH46RHuZWTdQwZJ6KBWR" }, - "id": "94bmeCLigEUUpWYw2xsAVB" - }, - "7mcWaWohM9zGtLX8ZSnqFy": { - "from": { - "blockId": "bwga7RwqQWbowdHph27DM1N", - "stepId": "sxvzuo48GHi3AcAfmiFyYC1" - }, - "to": { "blockId": "bu8whx817bJBG37FQrtD5dD" }, - "id": "7mcWaWohM9zGtLX8ZSnqFy" - } + "to": { "blockId": "fGrzjahWecA8hoNMRrLSwn" }, + "id": "nDjMjM11xPQF7c9Be6ukdY" }, - "allIds": [ - "jjNy2hYgrQgPS9EBMKA7MH", - "iBPsFyBsPv6Rbdfo2QdJyi", - "354PJ2jD5U3J2APqLsPJrp", - "94bmeCLigEUUpWYw2xsAVB", - "7mcWaWohM9zGtLX8ZSnqFy" - ] - }, + { + "from": { + "blockId": "eMk84KvFM53sBxchTeackR", + "stepId": "sv8uvEXgYWQNMfZWcdbfyCs", + "itemId": "ijYfW38tGhCMRrCtmR3bcr" + }, + "to": { "blockId": "49Jv45UJi9R3U4FuWS8R2c" }, + "id": "s1uVbzP1zMMJjsCguAfhGV" + }, + { + "from": { + "blockId": "eMk84KvFM53sBxchTeackR", + "stepId": "sv8uvEXgYWQNMfZWcdbfyCs" + }, + "to": { "blockId": "fD28kefdySKK7XA7SyTozC" }, + "id": "hVScFHNzToiEsMfYVv5AWZ" + }, + { + "from": { + "blockId": "eh2ohNATnGg6RTdjG9h5kb", + "stepId": "ssyBKZve7bihSxUASYTruZA" + }, + "to": { "blockId": "eMk84KvFM53sBxchTeackR" }, + "id": "r8LX7iuEXxjF5SW5dbS6qT" + }, + { + "from": { + "blockId": "cN46uqNAR3ohjrS8jHJ6xT", + "stepId": "nzijwLtLTAZfNNCN7kEpn5" + }, + "to": { "blockId": "eh2ohNATnGg6RTdjG9h5kb" }, + "id": "7wxB76VK81JsXMX9jU9dbQ" + } + ], "theme": { "chat": { "inputs": { diff --git a/apps/builder/playwright/fixtures/typebots/logic/condition.png b/apps/builder/playwright/fixtures/typebots/logic/condition.png index 22fe2da91..d668cbfde 100644 Binary files a/apps/builder/playwright/fixtures/typebots/logic/condition.png and b/apps/builder/playwright/fixtures/typebots/logic/condition.png differ diff --git a/apps/builder/playwright/fixtures/typebots/logic/redirect.json b/apps/builder/playwright/fixtures/typebots/logic/redirect.json index 3ec1cb2e9..d48cdd006 100644 --- a/apps/builder/playwright/fixtures/typebots/logic/redirect.json +++ b/apps/builder/playwright/fixtures/typebots/logic/redirect.json @@ -1,101 +1,82 @@ { - "id": "ckymkfh1e00562z1a3fjoua3e", - "createdAt": "2022-01-20T06:00:51.458Z", - "updatedAt": "2022-01-20T06:00:51.458Z", + "id": "ckz8hnw7m10833no1ar12eov20", + "createdAt": "2022-02-04T14:14:21.394Z", + "updatedAt": "2022-02-04T14:14:21.394Z", "name": "My typebot", - "ownerId": "ckymkff1100362z1a85juyoa8", + "ownerId": "ckz6t9iep0006k31a22j05fwq", "publishedTypebotId": null, "folderId": null, - "webhooks": { "byId": {}, "allIds": [] }, - "blocks": { - "byId": { - "bsVJfEW7EZrUnAi9s5ev17": { - "id": "bsVJfEW7EZrUnAi9s5ev17", - "title": "Start", - "stepIds": ["9Ck2yveNjZNHhjyc4HCJAL"], - "graphCoordinates": { "x": 0, "y": 0 } - }, - "bmdnpyvzopZ8YVfqsJY7Q8K": { - "id": "bmdnpyvzopZ8YVfqsJY7Q8K", - "title": "Block #2", - "graphCoordinates": { "x": 68, "y": 229 }, - "stepIds": ["sas16Qqf4TmZEXSexmYpmSd"] - }, - "bnsxmer7DD2R9DogoXTsvHJ": { - "id": "bnsxmer7DD2R9DogoXTsvHJ", - "title": "Block #3", - "graphCoordinates": { "x": 491, "y": 239 }, - "stepIds": ["sqNGop2aYkXRvJqb9nGtFbD"] - } + "blocks": [ + { + "id": "tdN9VXcdBWpuh6Gpaz3w4u", + "steps": [ + { + "id": "cVRL5EuVruTK31SAaVCvNE", + "type": "start", + "label": "Start", + "blockId": "tdN9VXcdBWpuh6Gpaz3w4u", + "outgoingEdgeId": "jqZYCYGxaL8svJbM2h1QAn" + } + ], + "title": "Start", + "graphCoordinates": { "x": 0, "y": 0 } }, - "allIds": [ - "bsVJfEW7EZrUnAi9s5ev17", - "bmdnpyvzopZ8YVfqsJY7Q8K", - "bnsxmer7DD2R9DogoXTsvHJ" - ] - }, - "steps": { - "byId": { - "9Ck2yveNjZNHhjyc4HCJAL": { - "id": "9Ck2yveNjZNHhjyc4HCJAL", - "type": "start", - "label": "Start", - "blockId": "bsVJfEW7EZrUnAi9s5ev17", - "edgeId": "totLsWG6AQfcFT39CsZwDy" - }, - "sas16Qqf4TmZEXSexmYpmSd": { - "id": "sas16Qqf4TmZEXSexmYpmSd", - "blockId": "bmdnpyvzopZ8YVfqsJY7Q8K", - "type": "choice input", - "options": { "itemIds": ["mAgynXh3zmkmWzNyPGVAcf"] } - }, - "sqNGop2aYkXRvJqb9nGtFbD": { - "id": "sqNGop2aYkXRvJqb9nGtFbD", - "blockId": "bnsxmer7DD2R9DogoXTsvHJ", - "type": "Redirect", - "options": { "isNewTab": false } - } + { + "id": "vymPUjL9AcWpkg9PkUXovk", + "graphCoordinates": { "x": 685, "y": 194 }, + "title": "Block #1", + "steps": [ + { + "id": "sa8WhnrMyMjYCBMeozfYRoi", + "blockId": "vymPUjL9AcWpkg9PkUXovk", + "type": "Redirect", + "options": { "isNewTab": false } + } + ] }, - "allIds": [ - "9Ck2yveNjZNHhjyc4HCJAL", - "sas16Qqf4TmZEXSexmYpmSd", - "sqNGop2aYkXRvJqb9nGtFbD" - ] - }, - "choiceItems": { - "byId": { - "mAgynXh3zmkmWzNyPGVAcf": { - "id": "mAgynXh3zmkmWzNyPGVAcf", - "stepId": "sas16Qqf4TmZEXSexmYpmSd", - "content": "Go to URL", - "edgeId": "7KgqWB88ufzhDwzvwHuEbN" - } - }, - "allIds": ["mAgynXh3zmkmWzNyPGVAcf"] - }, - "variables": { "byId": {}, "allIds": [] }, - "edges": { - "byId": { - "totLsWG6AQfcFT39CsZwDy": { - "from": { - "blockId": "bsVJfEW7EZrUnAi9s5ev17", - "stepId": "9Ck2yveNjZNHhjyc4HCJAL" - }, - "to": { "blockId": "bmdnpyvzopZ8YVfqsJY7Q8K" }, - "id": "totLsWG6AQfcFT39CsZwDy" + { + "id": "rEJ3PhFQc7diJ23jdoF6w7", + "graphCoordinates": { "x": 294, "y": 201 }, + "title": "Block #2", + "steps": [ + { + "id": "s7QRApVZmVFZgS53CNruBRz", + "blockId": "rEJ3PhFQc7diJ23jdoF6w7", + "type": "choice input", + "options": { "buttonLabel": "Send", "isMultipleChoice": false }, + "items": [ + { + "id": "5rWR3enRg6jZyFhtmgbPYo", + "stepId": "s7QRApVZmVFZgS53CNruBRz", + "type": 0, + "content": "Go to URL", + "outgoingEdgeId": "6aVDkPMEsadze2vf4mLiYt" + } + ] + } + ] + } + ], + "variables": [], + "edges": [ + { + "from": { + "blockId": "tdN9VXcdBWpuh6Gpaz3w4u", + "stepId": "cVRL5EuVruTK31SAaVCvNE" }, - "7KgqWB88ufzhDwzvwHuEbN": { - "from": { - "blockId": "bmdnpyvzopZ8YVfqsJY7Q8K", - "stepId": "sas16Qqf4TmZEXSexmYpmSd", - "nodeId": "mAgynXh3zmkmWzNyPGVAcf" - }, - "to": { "blockId": "bnsxmer7DD2R9DogoXTsvHJ" }, - "id": "7KgqWB88ufzhDwzvwHuEbN" - } + "to": { "blockId": "rEJ3PhFQc7diJ23jdoF6w7" }, + "id": "jqZYCYGxaL8svJbM2h1QAn" }, - "allIds": ["totLsWG6AQfcFT39CsZwDy", "7KgqWB88ufzhDwzvwHuEbN"] - }, + { + "from": { + "blockId": "rEJ3PhFQc7diJ23jdoF6w7", + "stepId": "s7QRApVZmVFZgS53CNruBRz", + "itemId": "5rWR3enRg6jZyFhtmgbPYo" + }, + "to": { "blockId": "vymPUjL9AcWpkg9PkUXovk" }, + "id": "6aVDkPMEsadze2vf4mLiYt" + } + ], "theme": { "chat": { "inputs": { diff --git a/apps/builder/playwright/fixtures/typebots/logic/setVariable.json b/apps/builder/playwright/fixtures/typebots/logic/setVariable.json index 9574ffb98..a63607894 100644 --- a/apps/builder/playwright/fixtures/typebots/logic/setVariable.json +++ b/apps/builder/playwright/fixtures/typebots/logic/setVariable.json @@ -1,152 +1,125 @@ { - "id": "ckylrr3qh0030fn1a3nszzxiu", - "createdAt": "2022-01-19T16:38:05.225Z", - "updatedAt": "2022-01-19T16:38:05.225Z", + "id": "ckz8hovd511021no1apuuyjv7b", + "createdAt": "2022-02-04T14:15:06.953Z", + "updatedAt": "2022-02-04T14:15:06.953Z", "name": "My typebot", - "ownerId": "ckylrpsmt0006fn1ah956d0z1", + "ownerId": "ckz6t9iep0006k31a22j05fwq", "publishedTypebotId": null, "folderId": null, - "webhooks": { "byId": {}, "allIds": [] }, - "blocks": { - "byId": { - "kmUzhRFzSKjkaipYNcku9S": { - "id": "kmUzhRFzSKjkaipYNcku9S", - "title": "Start", - "stepIds": ["6XgP3JoCh7Y4M8GCX9DKym"], - "graphCoordinates": { "x": 0, "y": 0 } - }, - "bwWRAaX5m6NZyZ9jjpXmWSb": { - "id": "bwWRAaX5m6NZyZ9jjpXmWSb", - "title": "Block #2", - "graphCoordinates": { "x": -21, "y": 221 }, - "stepIds": ["sqMVMXeRYp4inLcRqej2Wac", "s8n3nJajsBaYqrFeRYVvcf6"] - }, - "baUyUnNBxZzPe1z5PqE4NkD": { - "id": "baUyUnNBxZzPe1z5PqE4NkD", - "title": "Block #3", - "graphCoordinates": { "x": 375, "y": 280 }, - "stepIds": ["shfL5ueQDuj2RPcJPWZGArT", "sugJ6xN3jFys1CjWfsxGhiJ"] - }, - "bwkKNpJmAFCCLbZSnPnnLnR": { - "id": "bwkKNpJmAFCCLbZSnPnnLnR", - "title": "Block #4", - "graphCoordinates": { "x": 421, "y": 42 }, - "stepIds": ["shR7ae3iNEvB6arCSu7wVFF"] - } + "blocks": [ + { + "id": "jvbBpKifJ3ssvKQxPqhBiD", + "steps": [ + { + "id": "rqecLJCZT7gP2JgBhRpb3a", + "type": "start", + "label": "Start", + "blockId": "jvbBpKifJ3ssvKQxPqhBiD", + "outgoingEdgeId": "qnD38SqA7sYEh7efPZgDoR" + } + ], + "title": "Start", + "graphCoordinates": { "x": 0, "y": 0 } }, - "allIds": [ - "kmUzhRFzSKjkaipYNcku9S", - "bwWRAaX5m6NZyZ9jjpXmWSb", - "baUyUnNBxZzPe1z5PqE4NkD", - "bwkKNpJmAFCCLbZSnPnnLnR" - ] - }, - "steps": { - "byId": { - "6XgP3JoCh7Y4M8GCX9DKym": { - "id": "6XgP3JoCh7Y4M8GCX9DKym", - "type": "start", - "label": "Start", - "blockId": "kmUzhRFzSKjkaipYNcku9S", - "edgeId": "ahfJ4fUuvxX2dcBMk876tf" - }, - "s8n3nJajsBaYqrFeRYVvcf6": { - "id": "s8n3nJajsBaYqrFeRYVvcf6", - "blockId": "bwWRAaX5m6NZyZ9jjpXmWSb", - "type": "number input", - "edgeId": "dcJedLC7qsLtsmm1wbiFFc", - "options": { - "labels": { - "placeholder": "Type a number..." + { + "id": "roD9feCwx6jTDuVCThgzM2", + "graphCoordinates": { "x": 200, "y": 171 }, + "title": "Block #1", + "steps": [ + { + "id": "souEkLukHsYU9jrN2rAP7YT", + "blockId": "roD9feCwx6jTDuVCThgzM2", + "type": "text", + "content": { + "html": "
How old are you?
", + "richText": [ + { "type": "p", "children": [{ "text": "How old are you?" }] } + ], + "plainText": "How old are you?" + } + }, + { + "id": "skfn5McXVrTNpi2e62RtEEY", + "blockId": "roD9feCwx6jTDuVCThgzM2", + "type": "number input", + "options": { + "labels": { "button": "Send", "placeholder": "Type a number..." } + }, + "outgoingEdgeId": "5ZYDLyR1CUF6B8ESHrFXwK" + } + ] + }, + { + "id": "tFFeBrrWxY4tvr11C8rjTw", + "graphCoordinates": { "x": 526, "y": 283 }, + "title": "Block #2", + "steps": [ + { + "id": "spgqbyvunY91Ct5kVgeLLkz", + "blockId": "tFFeBrrWxY4tvr11C8rjTw", + "type": "Set variable", + "options": {} + }, + { + "id": "skeKC71L8C8wpfeuV4TTLCD", + "blockId": "tFFeBrrWxY4tvr11C8rjTw", + "type": "Set variable", + "options": {}, + "outgoingEdgeId": "7A4BD2vJT87grt3xFw86bn" + } + ] + }, + { + "id": "k6jFuKuSwy29LVwKxMWasv", + "graphCoordinates": { "x": 691, "y": 35 }, + "title": "Block #3", + "steps": [ + { + "id": "svpmd4uNoAXpoKyfYuuXTQe", + "blockId": "k6jFuKuSwy29LVwKxMWasv", + "type": "text", + "content": { + "html": "
Total: {{Total}}
Custom var: {{Custom var}}
", + "richText": [ + { "type": "p", "children": [{ "text": "Total: {{Total}}" }] }, + { + "type": "p", + "children": [{ "text": "Custom var: {{Custom var}}" }] + } + ], + "plainText": "Total: {{Total}}Custom var: {{Custom var}}" } } + ] + } + ], + "variables": [], + "edges": [ + { + "from": { + "blockId": "jvbBpKifJ3ssvKQxPqhBiD", + "stepId": "rqecLJCZT7gP2JgBhRpb3a" }, - "sqMVMXeRYp4inLcRqej2Wac": { - "id": "sqMVMXeRYp4inLcRqej2Wac", - "blockId": "bwWRAaX5m6NZyZ9jjpXmWSb", - "type": "text", - "content": { - "html": "
How old are you?
", - "richText": [ - { "type": "p", "children": [{ "text": "How old are you?" }] } - ], - "plainText": "How old are you?" - } - }, - "shfL5ueQDuj2RPcJPWZGArT": { - "id": "shfL5ueQDuj2RPcJPWZGArT", - "blockId": "baUyUnNBxZzPe1z5PqE4NkD", - "type": "Set variable", - "options": {} - }, - "sugJ6xN3jFys1CjWfsxGhiJ": { - "id": "sugJ6xN3jFys1CjWfsxGhiJ", - "blockId": "baUyUnNBxZzPe1z5PqE4NkD", - "type": "Set variable", - "edgeId": "sA5gvCVVBVYdGsdeSGF5ei", - "options": {} - }, - "shR7ae3iNEvB6arCSu7wVFF": { - "id": "shR7ae3iNEvB6arCSu7wVFF", - "blockId": "bwkKNpJmAFCCLbZSnPnnLnR", - "type": "text", - "content": { - "html": "
Total: {{Total}}
Custom var: {{Custom var}}
", - "richText": [ - { "type": "p", "children": [{ "text": "Total: {{Total}}" }] }, - { - "type": "p", - "children": [{ "text": "Custom var: {{Custom var}}" }] - } - ], - "plainText": "Total: {{Total}}Custom var: {{Custom var}}" - } - } + "to": { "blockId": "roD9feCwx6jTDuVCThgzM2" }, + "id": "qnD38SqA7sYEh7efPZgDoR" }, - "allIds": [ - "6XgP3JoCh7Y4M8GCX9DKym", - "s8n3nJajsBaYqrFeRYVvcf6", - "sqMVMXeRYp4inLcRqej2Wac", - "shfL5ueQDuj2RPcJPWZGArT", - "sugJ6xN3jFys1CjWfsxGhiJ", - "shR7ae3iNEvB6arCSu7wVFF" - ] - }, - "choiceItems": { "byId": {}, "allIds": [] }, - "variables": { "byId": {}, "allIds": [] }, - "edges": { - "byId": { - "ahfJ4fUuvxX2dcBMk876tf": { - "from": { - "blockId": "kmUzhRFzSKjkaipYNcku9S", - "stepId": "6XgP3JoCh7Y4M8GCX9DKym" - }, - "to": { "blockId": "bwWRAaX5m6NZyZ9jjpXmWSb" }, - "id": "ahfJ4fUuvxX2dcBMk876tf" + { + "from": { + "blockId": "roD9feCwx6jTDuVCThgzM2", + "stepId": "skfn5McXVrTNpi2e62RtEEY" }, - "dcJedLC7qsLtsmm1wbiFFc": { - "from": { - "blockId": "bwWRAaX5m6NZyZ9jjpXmWSb", - "stepId": "s8n3nJajsBaYqrFeRYVvcf6" - }, - "to": { "blockId": "baUyUnNBxZzPe1z5PqE4NkD" }, - "id": "dcJedLC7qsLtsmm1wbiFFc" - }, - "sA5gvCVVBVYdGsdeSGF5ei": { - "from": { - "blockId": "baUyUnNBxZzPe1z5PqE4NkD", - "stepId": "sugJ6xN3jFys1CjWfsxGhiJ" - }, - "to": { "blockId": "bwkKNpJmAFCCLbZSnPnnLnR" }, - "id": "sA5gvCVVBVYdGsdeSGF5ei" - } + "to": { "blockId": "tFFeBrrWxY4tvr11C8rjTw" }, + "id": "5ZYDLyR1CUF6B8ESHrFXwK" }, - "allIds": [ - "ahfJ4fUuvxX2dcBMk876tf", - "dcJedLC7qsLtsmm1wbiFFc", - "sA5gvCVVBVYdGsdeSGF5ei" - ] - }, + { + "from": { + "blockId": "tFFeBrrWxY4tvr11C8rjTw", + "stepId": "skeKC71L8C8wpfeuV4TTLCD" + }, + "to": { "blockId": "k6jFuKuSwy29LVwKxMWasv" }, + "id": "7A4BD2vJT87grt3xFw86bn" + } + ], "theme": { "chat": { "inputs": { diff --git a/apps/builder/playwright/fixtures/typebots/singleChoiceTarget.json b/apps/builder/playwright/fixtures/typebots/singleChoiceTarget.json index 53753b8a2..aac9f1067 100644 --- a/apps/builder/playwright/fixtures/typebots/singleChoiceTarget.json +++ b/apps/builder/playwright/fixtures/typebots/singleChoiceTarget.json @@ -1,158 +1,126 @@ { - "id": "ckylsr69q0240z31afjhedyxo", - "createdAt": "2022-01-19T17:06:08.126Z", - "updatedAt": "2022-01-19T17:06:08.126Z", + "id": "ckz8hrq1i11165no1artywpjvb", + "createdAt": "2022-02-04T14:17:20.022Z", + "updatedAt": "2022-02-04T14:17:20.022Z", "name": "My typebot", - "ownerId": "ckylsr4fi0220z31apbinpy9d", + "ownerId": "ckz6t9iep0006k31a22j05fwq", "publishedTypebotId": null, "folderId": null, - "webhooks": { "byId": {}, "allIds": [] }, - "blocks": { - "byId": { - "weeBMMXxNKwEonMfDX8Z5k": { - "id": "weeBMMXxNKwEonMfDX8Z5k", - "title": "Start", - "stepIds": ["nEXiHesKXRQJhQbaWfbDVH"], - "graphCoordinates": { "x": 0, "y": 0 } - }, - "bg2MBdkf6y7g6WsbqAP3eAT": { - "id": "bg2MBdkf6y7g6WsbqAP3eAT", - "title": "Block #2", - "graphCoordinates": { "x": 120, "y": 221 }, - "stepIds": ["sqzMjp1Ba4jTL3A6iJehC6C"] - }, - "bj5BE1yKPzFFhvRk6cMnmsQ": { - "id": "bj5BE1yKPzFFhvRk6cMnmsQ", - "title": "Block #3", - "graphCoordinates": { "x": 529, "y": 130 }, - "stepIds": ["s8zPdEj96z8EoJG2zBqgoE8"] - }, - "bdET8zLFQbwpTaAmi4wmezE": { - "id": "bdET8zLFQbwpTaAmi4wmezE", - "title": "Block #4", - "graphCoordinates": { "x": 538, "y": 386 }, - "stepIds": ["sjZ28izS5e3VjNynFKT2F7E"] - } + "blocks": [ + { + "id": "3EgW9xiicKuiCNycEY2huP", + "steps": [ + { + "id": "bHS7nGbziYUwD27tKANQY6", + "type": "start", + "label": "Start", + "blockId": "3EgW9xiicKuiCNycEY2huP", + "outgoingEdgeId": "9LViRZxY8G6iswJrE4YLsq" + } + ], + "title": "Start", + "graphCoordinates": { "x": 0, "y": 0 } }, - "allIds": [ - "weeBMMXxNKwEonMfDX8Z5k", - "bg2MBdkf6y7g6WsbqAP3eAT", - "bj5BE1yKPzFFhvRk6cMnmsQ", - "bdET8zLFQbwpTaAmi4wmezE" - ] - }, - "steps": { - "byId": { - "nEXiHesKXRQJhQbaWfbDVH": { - "id": "nEXiHesKXRQJhQbaWfbDVH", - "type": "start", - "label": "Start", - "blockId": "weeBMMXxNKwEonMfDX8Z5k", - "edgeId": "uh95dDpiiZdYxpPFsUqZEg" - }, - "sqzMjp1Ba4jTL3A6iJehC6C": { - "id": "sqzMjp1Ba4jTL3A6iJehC6C", - "blockId": "bg2MBdkf6y7g6WsbqAP3eAT", - "type": "choice input", - "options": { - "itemIds": [ - "bWrsg18ucP9cdtFKhzgHbF", - "p7Z57shv7p79KiwAtdi8Y3", - "wjMRa2GBBnME9bEiNi6XgP" - ] - }, - "edgeId": "asT5shwJqDQ67qPuydR4gy" - }, - "s8zPdEj96z8EoJG2zBqgoE8": { - "id": "s8zPdEj96z8EoJG2zBqgoE8", - "blockId": "bj5BE1yKPzFFhvRk6cMnmsQ", - "type": "text", - "content": { - "html": "
I love burgers!
", - "richText": [ - { "type": "p", "children": [{ "text": "I love burgers!" }] } + { + "id": "bs9JCJfixRTv8W2imPAoyX", + "graphCoordinates": { "x": 392, "y": 180 }, + "title": "Block #1", + "steps": [ + { + "id": "sqcez9cUVbPvaFL4rQsUwUk", + "blockId": "bs9JCJfixRTv8W2imPAoyX", + "type": "choice input", + "options": { "buttonLabel": "Send", "isMultipleChoice": false }, + "items": [ + { + "id": "2jCerpszvvbmhUS8FXkKG9", + "stepId": "sqcez9cUVbPvaFL4rQsUwUk", + "type": 0, + "content": "Burgers", + "outgoingEdgeId": "aTS7nwxhRdFN8NwAXE2oSq" + }, + { + "stepId": "sqcez9cUVbPvaFL4rQsUwUk", + "type": 0, + "id": "vP4HWCYkeRL6egk3yjCCmg", + "content": "Hot dogs" + }, + { + "stepId": "sqcez9cUVbPvaFL4rQsUwUk", + "type": 0, + "id": "hhveB5JSxJ8X9N66LFaUfe", + "content": "Carpaccio" + } ], - "plainText": "I love burgers!" + "outgoingEdgeId": "8FaF38WfM7PiLJLS5z6vQe" } - }, - "sjZ28izS5e3VjNynFKT2F7E": { - "id": "sjZ28izS5e3VjNynFKT2F7E", - "blockId": "bdET8zLFQbwpTaAmi4wmezE", - "type": "text", - "content": { - "html": "
Cool!
", - "richText": [{ "type": "p", "children": [{ "text": "Cool!" }] }], - "plainText": "Cool!" + ] + }, + { + "id": "ih574JsgYCSSt3t77DH9gp", + "graphCoordinates": { "x": 770, "y": 105 }, + "title": "Block #2", + "steps": [ + { + "id": "s9hdG689cjRzmTdb5hMN83q", + "blockId": "ih574JsgYCSSt3t77DH9gp", + "type": "text", + "content": { + "html": "
I love burgers!
", + "richText": [ + { "type": "p", "children": [{ "text": "I love burgers!" }] } + ], + "plainText": "I love burgers!" + } } - } + ] }, - "allIds": [ - "nEXiHesKXRQJhQbaWfbDVH", - "sqzMjp1Ba4jTL3A6iJehC6C", - "s8zPdEj96z8EoJG2zBqgoE8", - "sjZ28izS5e3VjNynFKT2F7E" - ] - }, - "choiceItems": { - "byId": { - "bWrsg18ucP9cdtFKhzgHbF": { - "id": "bWrsg18ucP9cdtFKhzgHbF", - "stepId": "sqzMjp1Ba4jTL3A6iJehC6C", - "content": "Burgers", - "edgeId": "jfR6AUWt9b4dhjnUHXB179" + { + "id": "5bMwu6Wv79avgdz3TKjVXr", + "graphCoordinates": { "x": 766, "y": 311 }, + "title": "Block #3", + "steps": [ + { + "id": "s3Zwr8m3Nm2BwGxNNCu4n7N", + "blockId": "5bMwu6Wv79avgdz3TKjVXr", + "type": "text", + "content": { + "html": "
Cool!
", + "richText": [{ "type": "p", "children": [{ "text": "Cool!" }] }], + "plainText": "Cool!" + } + } + ] + } + ], + "variables": [], + "edges": [ + { + "from": { + "blockId": "3EgW9xiicKuiCNycEY2huP", + "stepId": "bHS7nGbziYUwD27tKANQY6" }, - "p7Z57shv7p79KiwAtdi8Y3": { - "id": "p7Z57shv7p79KiwAtdi8Y3", - "stepId": "sqzMjp1Ba4jTL3A6iJehC6C", - "content": "Hot dogs" - }, - "wjMRa2GBBnME9bEiNi6XgP": { - "id": "wjMRa2GBBnME9bEiNi6XgP", - "stepId": "sqzMjp1Ba4jTL3A6iJehC6C", - "content": "Carpaccio" - } + "to": { "blockId": "bs9JCJfixRTv8W2imPAoyX" }, + "id": "9LViRZxY8G6iswJrE4YLsq" }, - "allIds": [ - "bWrsg18ucP9cdtFKhzgHbF", - "p7Z57shv7p79KiwAtdi8Y3", - "wjMRa2GBBnME9bEiNi6XgP" - ] - }, - "variables": { "byId": {}, "allIds": [] }, - "edges": { - "byId": { - "uh95dDpiiZdYxpPFsUqZEg": { - "from": { - "blockId": "weeBMMXxNKwEonMfDX8Z5k", - "stepId": "nEXiHesKXRQJhQbaWfbDVH" - }, - "to": { "blockId": "bg2MBdkf6y7g6WsbqAP3eAT" }, - "id": "uh95dDpiiZdYxpPFsUqZEg" + { + "from": { + "blockId": "bs9JCJfixRTv8W2imPAoyX", + "stepId": "sqcez9cUVbPvaFL4rQsUwUk", + "itemId": "2jCerpszvvbmhUS8FXkKG9" }, - "jfR6AUWt9b4dhjnUHXB179": { - "from": { - "blockId": "bg2MBdkf6y7g6WsbqAP3eAT", - "stepId": "sqzMjp1Ba4jTL3A6iJehC6C", - "nodeId": "bWrsg18ucP9cdtFKhzgHbF" - }, - "to": { "blockId": "bj5BE1yKPzFFhvRk6cMnmsQ" }, - "id": "jfR6AUWt9b4dhjnUHXB179" - }, - "asT5shwJqDQ67qPuydR4gy": { - "from": { - "blockId": "bg2MBdkf6y7g6WsbqAP3eAT", - "stepId": "sqzMjp1Ba4jTL3A6iJehC6C" - }, - "to": { "blockId": "bdET8zLFQbwpTaAmi4wmezE" }, - "id": "asT5shwJqDQ67qPuydR4gy" - } + "to": { "blockId": "ih574JsgYCSSt3t77DH9gp" }, + "id": "aTS7nwxhRdFN8NwAXE2oSq" }, - "allIds": [ - "uh95dDpiiZdYxpPFsUqZEg", - "jfR6AUWt9b4dhjnUHXB179", - "asT5shwJqDQ67qPuydR4gy" - ] - }, + { + "from": { + "blockId": "bs9JCJfixRTv8W2imPAoyX", + "stepId": "sqcez9cUVbPvaFL4rQsUwUk" + }, + "to": { "blockId": "5bMwu6Wv79avgdz3TKjVXr" }, + "id": "8FaF38WfM7PiLJLS5z6vQe" + } + ], "theme": { "chat": { "inputs": { diff --git a/apps/builder/playwright/fixtures/typebots/theme.json b/apps/builder/playwright/fixtures/typebots/theme.json index 56b0a3e72..c4782702d 100644 --- a/apps/builder/playwright/fixtures/typebots/theme.json +++ b/apps/builder/playwright/fixtures/typebots/theme.json @@ -1,140 +1,94 @@ { - "id": "bdFW2HHjMoEFmqHtFre9Xi8", - "createdAt": "2022-01-21T07:55:14.727Z", - "updatedAt": "2022-01-21T07:55:14.727Z", + "id": "ckz8huhvo11297no1a7b4zf3ce", + "createdAt": "2022-02-04T14:19:29.412Z", + "updatedAt": "2022-02-04T14:19:29.412Z", "name": "My typebot", - "ownerId": "user2", + "ownerId": "ckz6t9iep0006k31a22j05fwq", "publishedTypebotId": null, "folderId": null, - "blocks": { - "byId": { - "3kH2sUjVThQDWmqdoKnGk5": { - "id": "3kH2sUjVThQDWmqdoKnGk5", - "title": "Start", - "stepIds": ["oxTsU2C1RX5QHuyY8qjHAM"], - "graphCoordinates": { "x": 42, "y": 13 } - }, - "bdFW2HHjMoEFmqHtFre9Xi8": { - "id": "bdFW2HHjMoEFmqHtFre9Xi8", - "title": "Block #2", - "stepIds": ["sgkADMK25y9P9V3vjwjBaac", "ssEiEECKSFkA44dGDceHxKw"], - "graphCoordinates": { "x": 121, "y": 227 } - }, - "bhKHKi1SQb5woZEy1y4fNsJ": { - "id": "bhKHKi1SQb5woZEy1y4fNsJ", - "title": "Block #3", - "graphCoordinates": { "x": 605, "y": 454 }, - "stepIds": ["sseUQEWCMdiZquk8EbxHYtk"] - } - }, - "allIds": [ - "3kH2sUjVThQDWmqdoKnGk5", - "bdFW2HHjMoEFmqHtFre9Xi8", - "bhKHKi1SQb5woZEy1y4fNsJ" - ] - }, - "steps": { - "byId": { - "oxTsU2C1RX5QHuyY8qjHAM": { - "id": "oxTsU2C1RX5QHuyY8qjHAM", - "type": "start", - "label": "Start", - "edgeId": "25yX9DnQgdafpdAjfAu5Fp", - "blockId": "3kH2sUjVThQDWmqdoKnGk5" - }, - "sgkADMK25y9P9V3vjwjBaac": { - "id": "sgkADMK25y9P9V3vjwjBaac", - "type": "text", - "blockId": "bdFW2HHjMoEFmqHtFre9Xi8", - "content": { - "html": "
Ready?
", - "richText": [{ "type": "p", "children": [{ "text": "Ready?" }] }], - "plainText": "Ready?" + "blocks": [ + { + "id": "teepNancm8TLj1qYhaTYAf", + "steps": [ + { + "id": "8fG3wDsExSSkq5ekUMzWVY", + "type": "start", + "label": "Start", + "blockId": "teepNancm8TLj1qYhaTYAf", + "outgoingEdgeId": "pj6fgTAjarwBq2jVgMgYoK" } - }, - "ssEiEECKSFkA44dGDceHxKw": { - "id": "ssEiEECKSFkA44dGDceHxKw", - "type": "choice input", - "edgeId": "6e4Sbp8pGTvBQYtCk2qXbN", - "blockId": "bdFW2HHjMoEFmqHtFre9Xi8", - "options": { "itemIds": ["q69Ex7LacPrH9QUMeosRnB"] } - }, - "sseUQEWCMdiZquk8EbxHYtk": { - "id": "sseUQEWCMdiZquk8EbxHYtk", - "blockId": "bhKHKi1SQb5woZEy1y4fNsJ", - "type": "text input" - } + ], + "title": "Start", + "graphCoordinates": { "x": 0, "y": 0 } }, - "allIds": [ - "oxTsU2C1RX5QHuyY8qjHAM", - "sgkADMK25y9P9V3vjwjBaac", - "ssEiEECKSFkA44dGDceHxKw", - "sseUQEWCMdiZquk8EbxHYtk" - ] - }, - "choiceItems": { - "byId": { - "q69Ex7LacPrH9QUMeosRnB": { - "id": "q69Ex7LacPrH9QUMeosRnB", - "stepId": "ssEiEECKSFkA44dGDceHxKw", - "content": "Go" - } - }, - "allIds": ["q69Ex7LacPrH9QUMeosRnB"] - }, - "variables": { - "byId": { - "4tvkRmf32wiTsXrYoqyhfr": { - "id": "4tvkRmf32wiTsXrYoqyhfr", - "name": "secret 2" - }, - "jEg1FvkCU5S5owNAxXFsHL": { - "id": "jEg1FvkCU5S5owNAxXFsHL", - "name": "secret 3" - }, - "oASkBtoLqkYNqeakcjZH4L": { - "id": "oASkBtoLqkYNqeakcjZH4L", - "name": "secret 1" - }, - "rEoE1ehHzgx8X3d3UPGDHg": { - "id": "rEoE1ehHzgx8X3d3UPGDHg", - "name": "secret 4" - } - }, - "allIds": [ - "oASkBtoLqkYNqeakcjZH4L", - "4tvkRmf32wiTsXrYoqyhfr", - "jEg1FvkCU5S5owNAxXFsHL", - "rEoE1ehHzgx8X3d3UPGDHg" - ] - }, - "webhooks": { - "byId": { - "4h4Kk3Q1qGy7gFzpZtWVpU": { "id": "4h4Kk3Q1qGy7gFzpZtWVpU", "url": "" } - }, - "allIds": ["4h4Kk3Q1qGy7gFzpZtWVpU"] - }, - "edges": { - "byId": { - "25yX9DnQgdafpdAjfAu5Fp": { - "id": "25yX9DnQgdafpdAjfAu5Fp", - "to": { "blockId": "bdFW2HHjMoEFmqHtFre9Xi8" }, - "from": { - "stepId": "oxTsU2C1RX5QHuyY8qjHAM", - "blockId": "3kH2sUjVThQDWmqdoKnGk5" - } - }, - "6e4Sbp8pGTvBQYtCk2qXbN": { - "from": { - "blockId": "bdFW2HHjMoEFmqHtFre9Xi8", - "stepId": "ssEiEECKSFkA44dGDceHxKw" + { + "id": "6Dj1i7LeM3qXg5SKMhMyo1", + "graphCoordinates": { "x": 315, "y": 137 }, + "title": "Block #1", + "steps": [ + { + "id": "swUB2pSmvcv3NC7ySzskRpL", + "blockId": "6Dj1i7LeM3qXg5SKMhMyo1", + "type": "text", + "content": { + "html": "
Ready?
", + "richText": [{ "type": "p", "children": [{ "text": "Ready?" }] }], + "plainText": "Ready?" + } }, - "to": { "blockId": "bhKHKi1SQb5woZEy1y4fNsJ" }, - "id": "6e4Sbp8pGTvBQYtCk2qXbN" - } + { + "id": "sc7ZYFtHVegJUA8c5K3gghi", + "blockId": "6Dj1i7LeM3qXg5SKMhMyo1", + "type": "choice input", + "options": { "buttonLabel": "Send", "isMultipleChoice": false }, + "items": [ + { + "id": "nTjur4kxyL473XTbAb4Fak", + "stepId": "sc7ZYFtHVegJUA8c5K3gghi", + "type": 0, + "content": "Go" + } + ], + "outgoingEdgeId": "uAsACqSmud99zmyCABWDwr" + } + ] }, - "allIds": ["25yX9DnQgdafpdAjfAu5Fp", "6e4Sbp8pGTvBQYtCk2qXbN"] - }, + { + "id": "2TR5xAQobKAg8hbArfh5br", + "graphCoordinates": { "x": 760, "y": 299 }, + "title": "Block #2", + "steps": [ + { + "id": "s4xokHybra1jmZsWGVmza1K", + "blockId": "2TR5xAQobKAg8hbArfh5br", + "type": "text input", + "options": { + "isLong": false, + "labels": { "button": "Send", "placeholder": "Type your answer..." } + } + } + ] + } + ], + "variables": [], + "edges": [ + { + "from": { + "blockId": "teepNancm8TLj1qYhaTYAf", + "stepId": "8fG3wDsExSSkq5ekUMzWVY" + }, + "to": { "blockId": "6Dj1i7LeM3qXg5SKMhMyo1" }, + "id": "pj6fgTAjarwBq2jVgMgYoK" + }, + { + "from": { + "blockId": "6Dj1i7LeM3qXg5SKMhMyo1", + "stepId": "sc7ZYFtHVegJUA8c5K3gghi" + }, + "to": { "blockId": "2TR5xAQobKAg8hbArfh5br" }, + "id": "uAsACqSmud99zmyCABWDwr" + } + ], "theme": { "chat": { "inputs": { diff --git a/apps/builder/playwright/global-setup.ts b/apps/builder/playwright/global-setup.ts index 8f6673bdf..e8c4ee34b 100644 --- a/apps/builder/playwright/global-setup.ts +++ b/apps/builder/playwright/global-setup.ts @@ -1,6 +1,10 @@ import { chromium, FullConfig, Page } from '@playwright/test' import { existsSync } from 'fs' -import { setupDatabase, teardownDatabase } from './services/database' +import { + getSignedInUser, + setupDatabase, + teardownDatabase, +} from './services/database' // eslint-disable-next-line @typescript-eslint/no-var-requires require('dotenv').config({ path: '.env' }) @@ -8,10 +12,15 @@ require('dotenv').config({ path: '.env' }) async function globalSetup(config: FullConfig) { const { baseURL } = config.projects[0].use if (!baseURL) throw new Error('baseURL is missing') + if (!process.env.GITHUB_EMAIL || !process.env.GITHUB_PASSWORD) + throw new Error( + 'GITHUB_EMAIL or GITHUB_PASSWORD are missing in the environment. They are required to log in.' + ) await teardownDatabase() - if (!existsSync('./playwright/authenticatedState.json')) { + const signedInUser = await getSignedInUser(process.env.GITHUB_EMAIL as string) + if (!signedInUser || !existsSync('./playwright/authenticatedState.json')) { const browser = await chromium.launch() const page = await browser.newPage() await signIn(page) @@ -24,14 +33,13 @@ async function globalSetup(config: FullConfig) { } const signIn = async (page: Page) => { - if (!process.env.GITHUB_EMAIL || !process.env.GITHUB_PASSWORD) - throw new Error( - 'GITHUB_EMAIL or GITHUB_PASSWORD are missing in the environment. They are required to log in.' - ) await page.goto(`${process.env.PLAYWRIGHT_BUILDER_TEST_BASE_URL}/signin`) await page.click('text=Continue with GitHub') - await page.fill('input[name="login"]', process.env.GITHUB_EMAIL) - await page.fill('input[name="password"]', process.env.GITHUB_PASSWORD) + await page.fill('input[name="login"]', process.env.GITHUB_EMAIL as string) + await page.fill( + 'input[name="password"]', + process.env.GITHUB_PASSWORD as string + ) await page.press('input[name="password"]', 'Enter') try { await page.locator('text=Authorize baptisteArno').click({ timeout: 3000 }) diff --git a/apps/builder/playwright/services/database.ts b/apps/builder/playwright/services/database.ts index 797483861..92f89e886 100644 --- a/apps/builder/playwright/services/database.ts +++ b/apps/builder/playwright/services/database.ts @@ -1,6 +1,8 @@ import { + Block, defaultSettings, defaultTheme, + PublicBlock, PublicTypebot, Step, Typebot, @@ -23,7 +25,7 @@ export const setupDatabase = async (userEmail: string) => { return createCredentials() } -const getSignedInUser = (email: string) => +export const getSignedInUser = (email: string) => prisma.user.findFirst({ where: { email } }) export const createTypebots = async (partialTypebots: Partial[]) => { @@ -108,18 +110,24 @@ const parseTypebotToPublicTypebot = ( typebot: Typebot ): PublicTypebot => ({ id, - blocks: typebot.blocks, - steps: typebot.steps, name: typebot.name, + blocks: parseBlocksToPublicBlocks(typebot.blocks), typebotId: typebot.id, theme: typebot.theme, settings: typebot.settings, publicId: typebot.publicId, - choiceItems: typebot.choiceItems, variables: typebot.variables, edges: typebot.edges, }) +const parseBlocksToPublicBlocks = (blocks: Block[]): PublicBlock[] => + blocks.map((b) => ({ + ...b, + steps: b.steps.map((s) => + 'webhook' in s ? { ...s, webhook: s.webhook.id } : s + ), + })) + const parseTestTypebot = (partialTypebot: Partial): Typebot => ({ id: partialTypebot.id ?? 'typebot', folderId: null, @@ -128,82 +136,54 @@ const parseTestTypebot = (partialTypebot: Partial): Typebot => ({ theme: defaultTheme, settings: defaultSettings, createdAt: new Date(), - choiceItems: partialTypebot.choiceItems ?? { - byId: { - choice1: { - id: 'choice1', - stepId: 'step1', - }, - }, - allIds: ['choice1'], - }, publicId: null, publishedTypebotId: null, updatedAt: new Date(), - variables: { byId: {}, allIds: [] }, - webhooks: { byId: {}, allIds: [] }, - edges: { - byId: { - edge1: { - id: 'edge1', - from: { blockId: 'block0', stepId: 'step0' }, - to: { blockId: 'block1' }, - }, - }, - allIds: ['edge1'], - }, + variables: [], ...partialTypebot, - blocks: { - byId: { - block0: { - id: 'block0', - title: 'Block #0', - stepIds: ['step0'], - graphCoordinates: { x: 0, y: 0 }, - }, - ...partialTypebot.blocks?.byId, + edges: [ + { + id: 'edge1', + from: { blockId: 'block0', stepId: 'step0' }, + to: { blockId: 'block1' }, }, - allIds: ['block0', ...(partialTypebot.blocks?.allIds ?? [])], - }, - steps: { - byId: { - step0: { - id: 'step0', - type: 'start', - blockId: 'block0', - label: 'Start', - edgeId: 'edge1', - }, - ...partialTypebot.steps?.byId, + ], + blocks: [ + { + id: 'block0', + title: 'Block #0', + steps: [ + { + id: 'step0', + type: 'start', + blockId: 'block0', + label: 'Start', + outgoingEdgeId: 'edge1', + }, + ], + graphCoordinates: { x: 0, y: 0 }, }, - allIds: ['step0', ...(partialTypebot.steps?.allIds ?? [])], - }, + ...(partialTypebot.blocks ?? []), + ], }) export const parseDefaultBlockWithStep = ( step: Partial -): Pick => ({ - blocks: { - byId: { - block1: { - graphCoordinates: { x: 200, y: 200 }, - id: 'block1', - stepIds: ['step1'], - title: 'Block #1', - }, +): Pick => ({ + blocks: [ + { + graphCoordinates: { x: 200, y: 200 }, + id: 'block1', + steps: [ + { + id: 'step1', + blockId: 'block1', + ...step, + } as Step, + ], + title: 'Block #1', }, - allIds: ['block1'], - }, - steps: { - byId: { - step1: { - id: 'step1', - blockId: 'block1', - ...step, - } as Step, - }, - allIds: ['step1'], - }, + ], }) export const importTypebotInDatabase = ( diff --git a/apps/builder/playwright/tests/account.spec.ts b/apps/builder/playwright/tests/account.spec.ts index 760221051..55424a7c2 100644 --- a/apps/builder/playwright/tests/account.spec.ts +++ b/apps/builder/playwright/tests/account.spec.ts @@ -6,7 +6,11 @@ import { updateUser } from '../services/database' test.describe('Account page', () => { test('should edit user info properly', async ({ page }) => { - await updateUser({ name: 'Default Name' }) + await updateUser({ + name: 'Default Name', + image: + 'https://images.unsplash.com/photo-1521119989659-a83eee488004?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1323&q=80', + }) await page.goto('/account') const saveButton = page.locator('button:has-text("Save")') await expect(saveButton).toBeHidden() diff --git a/apps/builder/playwright/tests/bubbles/text.spec.ts b/apps/builder/playwright/tests/bubbles/text.spec.ts index bd5a11da7..4f530afb9 100644 --- a/apps/builder/playwright/tests/bubbles/text.spec.ts +++ b/apps/builder/playwright/tests/bubbles/text.spec.ts @@ -24,22 +24,22 @@ test.describe('Text bubble step', () => { await page.click('[data-testid="bold-button"]') await page.type('div[role="textbox"]', 'Bold text') - await page.press('div[role="textbox"]', 'Enter') + await page.press('div[role="textbox"]', 'Shift+Enter') await page.click('[data-testid="bold-button"]') await page.click('[data-testid="italic-button"]') await page.type('div[role="textbox"]', 'Italic text') - await page.press('div[role="textbox"]', 'Enter') + await page.press('div[role="textbox"]', 'Shift+Enter') await page.click('[data-testid="underline-button"]') await page.click('[data-testid="italic-button"]') await page.type('div[role="textbox"]', 'Underlined text') - await page.press('div[role="textbox"]', 'Enter') + await page.press('div[role="textbox"]', 'Shift+Enter') await page.click('[data-testid="bold-button"]') await page.click('[data-testid="italic-button"]') await page.type('div[role="textbox"]', 'Everything text') - await page.press('div[role="textbox"]', 'Enter') + await page.press('div[role="textbox"]', 'Shift+Enter') await page.click('text=Preview') await expect( diff --git a/apps/builder/playwright/tests/editor.spec.ts b/apps/builder/playwright/tests/editor.spec.ts index b1b552877..2c1752a67 100644 --- a/apps/builder/playwright/tests/editor.spec.ts +++ b/apps/builder/playwright/tests/editor.spec.ts @@ -1,9 +1,97 @@ -import test, { expect } from '@playwright/test' -import { createTypebots, parseDefaultBlockWithStep } from '../services/database' +import test, { expect, Page } from '@playwright/test' +import { + createTypebots, + importTypebotInDatabase, + parseDefaultBlockWithStep, +} from '../services/database' import { defaultTextInputOptions, InputStepType } from 'models' import { generate } from 'short-uuid' +import path from 'path' -test.describe('Editor', () => { +test.describe.parallel('Editor', () => { + test('Edges connection should work', async ({ page }) => { + const typebotId = generate() + await createTypebots([ + { + id: typebotId, + }, + ]) + await page.goto(`/typebots/${typebotId}/edit`) + await page.dragAndDrop('text=Button', '#editor-container', { + targetPosition: { x: 800, y: 400 }, + }) + await page.dragAndDrop( + 'text=Text >> nth=0', + '[data-testid="block"] >> nth=1', + { + targetPosition: { x: 100, y: 50 }, + } + ) + await page.dragAndDrop( + '[data-testid="endpoint"]', + '[data-testid="block"] >> nth=1', + { targetPosition: { x: 100, y: 10 } } + ) + await expect(page.locator('[data-testid="edge"]')).toBeVisible() + await page.dragAndDrop( + '[data-testid="endpoint"]', + '[data-testid="step"] >> nth=1' + ) + await expect(page.locator('[data-testid="edge"]')).toBeVisible() + await page.dragAndDrop('text=Date', '#editor-container', { + targetPosition: { x: 1000, y: 800 }, + }) + await page.dragAndDrop( + '[data-testid="endpoint"] >> nth=2', + '[data-testid="block"] >> nth=2', + { + targetPosition: { x: 100, y: 10 }, + } + ) + await expect(page.locator('[data-testid="edge"] >> nth=0')).toBeVisible() + await expect(page.locator('[data-testid="edge"] >> nth=1')).toBeVisible() + }) + test('Drag and drop steps and items should work', async ({ page }) => { + const typebotId = generate() + await importTypebotInDatabase( + path.join(__dirname, '../fixtures/typebots/editor/buttonsDnd.json'), + { + id: typebotId, + } + ) + + // Steps dnd + await page.goto(`/typebots/${typebotId}/edit`) + await expect(page.locator('[data-testid="step"] >> nth=1')).toHaveText( + 'Hello!' + ) + await page.dragAndDrop('text=Hello', 'text=Item 1') + await expect(page.locator('[data-testid="step"] >> nth=2')).toHaveText( + 'Hello!' + ) + await page.dragAndDrop('text=Hello', '[data-testid="step"] >> text=Start') + await expect(page.locator('text=Block #4')).toBeVisible() + await page.dragAndDrop('text=Hello', 'text=Block #2') + await expect(page.locator('[data-testid="step"] >> nth=3')).toHaveText( + 'Hello!' + ) + + // Items dnd + await expect(page.locator('[data-testid="item"] >> nth=0')).toHaveText( + 'Item 1' + ) + await page.dragAndDrop('text=Item 1', 'text=Item 3') + await expect(page.locator('[data-testid="item"] >> nth=2')).toHaveText( + 'Item 1' + ) + await expect(page.locator('[data-testid="item"] >> nth=1')).toHaveText( + 'Item 3' + ) + await page.dragAndDrop('text=Item 3', 'text=Item 2-3') + await expect(page.locator('[data-testid="item"] >> nth=6')).toHaveText( + 'Item 3' + ) + }) test('Undo / Redo buttons should work', async ({ page }) => { const typebotId = generate() await createTypebots([ diff --git a/apps/builder/playwright/tests/inputs/buttons.spec.ts b/apps/builder/playwright/tests/inputs/buttons.spec.ts index 4f3e97ffb..df95ed19d 100644 --- a/apps/builder/playwright/tests/inputs/buttons.spec.ts +++ b/apps/builder/playwright/tests/inputs/buttons.spec.ts @@ -3,7 +3,7 @@ import { createTypebots, parseDefaultBlockWithStep, } from '../../services/database' -import { defaultChoiceInputOptions, InputStepType } from 'models' +import { defaultChoiceInputOptions, InputStepType, ItemType } from 'models' import { typebotViewer } from '../../services/selectorUtils' import { generate } from 'short-uuid' @@ -15,7 +15,14 @@ test.describe.parallel('Buttons input step', () => { id: typebotId, ...parseDefaultBlockWithStep({ type: InputStepType.CHOICE, - options: { ...defaultChoiceInputOptions, itemIds: ['choice1'] }, + items: [ + { + id: 'choice1', + stepId: 'step1', + type: ItemType.BUTTON, + }, + ], + options: { ...defaultChoiceInputOptions }, }), }, ]) @@ -23,14 +30,11 @@ test.describe.parallel('Buttons input step', () => { await page.goto(`/typebots/${typebotId}/edit`) await page.fill('input[value="Click to edit"]', 'Item 1') await page.press('input[value="Item 1"]', 'Enter') - await page.locator('text=Item 1').hover() - await page.click('[aria-label="Add item"]') await page.fill('input[value="Click to edit"]', 'Item 2') await page.press('input[value="Item 2"]', 'Enter') - await page.locator('text=Item 2').hover() - await page.click('[aria-label="Add item"]') await page.fill('input[value="Click to edit"]', 'Item 3') await page.press('input[value="Item 3"]', 'Enter') + await page.press('input[value="Click to edit"]', 'Escape') await page.click('text=Item 2', { button: 'right' }) await page.click('text=Delete') await expect(page.locator('text=Item 2')).toBeHidden() diff --git a/apps/builder/playwright/tests/integrations/googleSheets.spec.ts b/apps/builder/playwright/tests/integrations/googleSheets.spec.ts index d103806c1..b3cb9f178 100644 --- a/apps/builder/playwright/tests/integrations/googleSheets.spec.ts +++ b/apps/builder/playwright/tests/integrations/googleSheets.spec.ts @@ -145,7 +145,7 @@ test.describe.parallel('Google sheets integration', () => { .press('Enter') await expect( typebotViewer(page).locator('text=Your name is: John Smith') - ).toBeVisible() + ).toBeVisible({ timeout: 30000 }) }) }) diff --git a/apps/builder/playwright/tests/logic/condition.spec.ts b/apps/builder/playwright/tests/logic/condition.spec.ts index 2968c1e19..be3172f27 100644 --- a/apps/builder/playwright/tests/logic/condition.spec.ts +++ b/apps/builder/playwright/tests/logic/condition.spec.ts @@ -16,8 +16,7 @@ test.describe('Condition step', () => { ) await page.goto(`/typebots/${typebotId}/edit`) - await page.click('text=Configure...') - await page.click('button:has-text("Add a comparison")') + await page.click('text=Configure... >> nth=0', { force: true }) await page.fill( 'input[placeholder="Search for a variable"] >> nth=-1', 'Age' @@ -41,8 +40,7 @@ test.describe('Condition step', () => { '100' ) - await page.click('text=Configure...') - await page.click('button:has-text("Add a comparison")') + await page.click('text=Configure...', { force: true }) await page.fill( 'input[placeholder="Search for a variable"] >> nth=-1', 'Age' @@ -54,7 +52,7 @@ test.describe('Condition step', () => { await page.click('text=Preview') await typebotViewer(page) - .locator('input[placeholder="Type your answer..."]') + .locator('input[placeholder="Type a number..."]') .fill('15') await typebotViewer(page).locator('text=Send').click() await expect( @@ -63,7 +61,7 @@ test.describe('Condition step', () => { await page.click('text=Restart') await typebotViewer(page) - .locator('input[placeholder="Type your answer..."]') + .locator('input[placeholder="Type a number..."]') .fill('45') await typebotViewer(page).locator('text=Send').click() await expect( @@ -72,7 +70,7 @@ test.describe('Condition step', () => { await page.click('text=Restart') await typebotViewer(page) - .locator('input[placeholder="Type your answer..."]') + .locator('input[placeholder="Type a number..."]') .fill('90') await typebotViewer(page).locator('text=Send').click() await expect( diff --git a/apps/builder/playwright/tests/theme.spec.ts b/apps/builder/playwright/tests/theme.spec.ts index 7be72baa2..6c48e65e5 100644 --- a/apps/builder/playwright/tests/theme.spec.ts +++ b/apps/builder/playwright/tests/theme.spec.ts @@ -31,7 +31,7 @@ test.describe.parallel('Theme page', () => { ) await page.click('text=Color') await page.click('[aria-label="Pick a color"]') - await page.fill('[aria-label="Color value"]', '#2a9d8f') + await page.fill('[aria-label="Color value"] >> nth=-1', '#2a9d8f') await expect(chatContainer).toHaveCSS( 'background-color', 'rgb(42, 157, 143)' @@ -56,10 +56,14 @@ test.describe.parallel('Theme page', () => { await page.waitForTimeout(300) // Host bubbles - await page.click(':nth-match([aria-label="Pick a color"], 1)') - await page.fill('[aria-label="Color value"]', '#2a9d8f') - await page.click(':nth-match([aria-label="Pick a color"], 2)') - await page.fill('[aria-label="Color value"]', '#ffffff') + await page.click( + '[data-testid="host-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=0' + ) + await page.fill('input[value="#F7F8FF"]', '#2a9d8f') + await page.click( + '[data-testid="host-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=1' + ) + await page.fill('input[value="#303235"]', '#ffffff') const hostBubble = typebotViewer(page).locator( '[data-testid="host-bubble"]' ) @@ -70,19 +74,27 @@ test.describe.parallel('Theme page', () => { await expect(hostBubble).toHaveCSS('color', 'rgb(255, 255, 255)') // Buttons - await page.click(':nth-match([aria-label="Pick a color"], 5)') - await page.fill('[aria-label="Color value"]', '#7209b7') - await page.click(':nth-match([aria-label="Pick a color"], 6)') - await page.fill('[aria-label="Color value"]', '#e9c46a') + await page.click( + '[data-testid="buttons-theme"] >> [aria-label="Pick a color"] >> nth=0' + ) + await page.fill('input[value="#0042DA"]', '#7209b7') + await page.click( + '[data-testid="buttons-theme"] >> [aria-label="Pick a color"] >> nth=1' + ) + await page.fill('input[value="#FFFFFF"]', '#e9c46a') const button = typebotViewer(page).locator('[data-testid="button"]') await expect(button).toHaveCSS('background-color', 'rgb(114, 9, 183)') await expect(button).toHaveCSS('color', 'rgb(233, 196, 106)') // Guest bubbles - await page.click(':nth-match([aria-label="Pick a color"], 3)') - await page.fill('[aria-label="Color value"]', '#d8f3dc') - await page.click(':nth-match([aria-label="Pick a color"], 4)') - await page.fill('[aria-label="Color value"]', '#264653') + await page.click( + '[data-testid="guest-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=0' + ) + await page.fill('input[value="#FF8E21"]', '#d8f3dc') + await page.click( + '[data-testid="guest-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=1' + ) + await page.fill('input[value="#FFFFFF"]', '#264653') await typebotViewer(page).locator('text=Go').click() const guestBubble = typebotViewer(page).locator( '[data-testid="guest-bubble"]' @@ -94,10 +106,14 @@ test.describe.parallel('Theme page', () => { await expect(guestBubble).toHaveCSS('color', 'rgb(38, 70, 83)') // Input - await page.click(':nth-match([aria-label="Pick a color"], 7)') - await page.fill('[aria-label="Color value"]', '#ffe8d6') - await page.click(':nth-match([aria-label="Pick a color"], 8)') - await page.fill('[aria-label="Color value"]', '#023e8a') + await page.click( + '[data-testid="inputs-theme"] >> [aria-label="Pick a color"] >> nth=0' + ) + await page.fill('input[value="#FFFFFF"]', '#ffe8d6') + await page.click( + '[data-testid="inputs-theme"] >> [aria-label="Pick a color"] >> nth=1' + ) + await page.fill('input[value="#303235"]', '#023e8a') await typebotViewer(page).locator('text=Go').click() const input = typebotViewer(page).locator('.typebot-input') await expect(input).toHaveCSS('background-color', 'rgb(255, 232, 214)') diff --git a/apps/builder/services/graph.ts b/apps/builder/services/graph.ts index 1016730b8..c2b9afd52 100644 --- a/apps/builder/services/graph.ts +++ b/apps/builder/services/graph.ts @@ -1,13 +1,11 @@ -import { Edge, Table, Target } from 'models' +import { Edge, IdMap } from 'models' import { AnchorsPositionProps } from 'components/shared/Graph/Edges/Edge' import { stubLength, blockWidth, blockAnchorsOffset, - ConnectingIds, Endpoint, Coordinates, - BlocksCoordinates, } from 'contexts/GraphContext' import { roundCorners } from 'svg-round-corners' import { headerHeight } from 'components/shared/TypebotHeader' @@ -230,20 +228,11 @@ export const computeEdgePath = ({ } export const computeConnectingEdgePath = ({ - connectingIds, + sourceBlockCoordinates, + targetBlockCoordinates, sourceTop, targetTop, - blocksCoordinates, -}: { - connectingIds: Omit & { target: Target } - sourceTop: number - targetTop?: number - blocksCoordinates: BlocksCoordinates -}) => { - const sourceBlockCoordinates = - blocksCoordinates.byId[connectingIds.source.blockId] - const targetBlockCoordinates = - blocksCoordinates.byId[connectingIds.target.blockId] +}: GetAnchorsPositionParams) => { const anchorsPosition = getAnchorsPosition({ sourceBlockCoordinates, targetBlockCoordinates, @@ -254,23 +243,25 @@ export const computeConnectingEdgePath = ({ } export const computeEdgePathToMouse = ({ - blockPosition, + sourceBlockCoordinates, mousePosition, sourceTop, }: { - blockPosition: Coordinates + sourceBlockCoordinates: Coordinates mousePosition: Coordinates sourceTop: number }): string => { const sourcePosition = { x: - mousePosition.x - blockPosition.x > blockWidth / 2 - ? blockPosition.x + blockWidth - 40 - : blockPosition.x + 40, + mousePosition.x - sourceBlockCoordinates.x > blockWidth / 2 + ? sourceBlockCoordinates.x + blockWidth - 40 + : sourceBlockCoordinates.x + 40, y: sourceTop, } const sourceType = - mousePosition.x - blockPosition.x > blockWidth / 2 ? 'right' : 'left' + mousePosition.x - sourceBlockCoordinates.x > blockWidth / 2 + ? 'right' + : 'left' const segments = computeThreeSegments( sourcePosition, mousePosition, @@ -284,11 +275,11 @@ export const computeEdgePathToMouse = ({ export const getEndpointTopOffset = ( graphPosition: Coordinates, - endpoints: Table, + endpoints: IdMap, endpointId?: string ): number | undefined => { if (!endpointId) return - const endpointRef = endpoints.byId[endpointId]?.ref + const endpointRef = endpoints[endpointId]?.ref if (!endpointRef) return return ( 8 + @@ -299,4 +290,4 @@ export const getEndpointTopOffset = ( } export const getSourceEndpointId = (edge?: Edge) => - edge?.from.buttonId ?? edge?.from.stepId + `${edge?.from.conditionType ?? ''}` + edge?.from.itemId ?? edge?.from.stepId diff --git a/apps/builder/services/integrations.ts b/apps/builder/services/integrations.ts index 6028e58f0..20fbb5f22 100644 --- a/apps/builder/services/integrations.ts +++ b/apps/builder/services/integrations.ts @@ -2,7 +2,7 @@ import { sendRequest } from 'utils' import { stringify } from 'qs' import useSWR from 'swr' import { fetcher } from './utils' -import { Table, Variable, VariableForTest, WebhookResponse } from 'models' +import { StepIndices, Variable, VariableForTest, WebhookResponse } from 'models' export const getGoogleSheetsConsentScreenUrl = ( redirectUrl: string, @@ -69,10 +69,11 @@ export const useSheets = ({ export const executeWebhook = ( typebotId: string, webhookId: string, - variables: Table + variables: Variable[], + { blockIndex, stepIndex }: StepIndices ) => sendRequest({ - url: `/api/typebots/${typebotId}/webhooks/${webhookId}/execute`, + url: `/api/typebots/${typebotId}/blocks/${blockIndex}/steps/${stepIndex}/executeWebhook`, method: 'POST', body: { variables, @@ -80,28 +81,21 @@ export const executeWebhook = ( }) export const convertVariableForTestToVariables = ( - variablesForTest: Table | undefined, - variables: Table -): Table => { - if (!variablesForTest) return { byId: {}, allIds: [] } - return { - byId: { - ...variables.byId, - ...variablesForTest.allIds.reduce((obj, id) => { - const variableForTest = variablesForTest.byId[id] - if (!variableForTest.variableId) return {} - const variable = variables.byId[variableForTest.variableId ?? ''] - return { - ...obj, - [variableForTest.variableId]: { - ...variable, - value: variableForTest.value, - }, - } + variablesForTest: VariableForTest[], + variables: Variable[] +): Variable[] => { + if (!variablesForTest) return [] + return [ + ...variables, + ...variablesForTest + .filter((v) => v.variableId) + .map((variableForTest) => { + const variable = variables.find( + (v) => v.id === variableForTest.variableId + ) as Variable + return { ...variable, value: variableForTest.value } }, {}), - }, - allIds: variables.allIds, - } + ] } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/apps/builder/services/publicTypebot.tsx b/apps/builder/services/publicTypebot.tsx index 0f1b434aa..5faf1473d 100644 --- a/apps/builder/services/publicTypebot.tsx +++ b/apps/builder/services/publicTypebot.tsx @@ -1,26 +1,39 @@ -import { PublicTypebot, Typebot } from 'models' +import { + Block, + InputStep, + PublicBlock, + PublicStep, + PublicTypebot, + Step, + Typebot, +} from 'models' import shortId from 'short-uuid' import { HStack, Text } from '@chakra-ui/react' import { CalendarIcon } from 'assets/icons' import { StepIcon } from 'components/editor/StepsSideBar/StepIcon' import { isInputStep, sendRequest } from 'utils' +import { isDefined } from '@udecode/plate-common' export const parseTypebotToPublicTypebot = ( typebot: Typebot ): PublicTypebot => ({ + ...typebot, id: shortId.generate(), - blocks: typebot.blocks, - steps: typebot.steps, - name: typebot.name, typebotId: typebot.id, - theme: typebot.theme, - settings: typebot.settings, - publicId: typebot.publicId, - choiceItems: typebot.choiceItems, - variables: typebot.variables, - edges: typebot.edges, + blocks: parseBlocksToPublicBlocks(typebot.blocks), }) +const parseBlocksToPublicBlocks = (blocks: Block[]): PublicBlock[] => + blocks.map((b) => ({ + ...b, + steps: b.steps.map( + (s) => + ('webhook' in s && isDefined(s.webhook) + ? { ...s, webhook: s.webhook.id } + : s) as PublicStep + ), + })) + export const createPublishedTypebot = async ( typebot: Omit ) => @@ -41,12 +54,11 @@ export const updatePublishedTypebot = async ( }) export const parseSubmissionsColumns = ( - typebot?: PublicTypebot + typebot: PublicTypebot ): { Header: JSX.Element accessor: string }[] => { - if (!typebot) return [] return [ { Header: ( @@ -57,14 +69,14 @@ export const parseSubmissionsColumns = ( ), accessor: 'createdAt', }, - ...typebot.blocks.allIds - .filter((blockId) => typebot && blockContainsInput(typebot, blockId)) - .map((blockId) => { - const block = typebot.blocks.byId[blockId] - const inputStepId = block.stepIds.find((stepId) => - isInputStep(typebot.steps.byId[stepId]) - ) - const inputStep = typebot.steps.byId[inputStepId as string] + ...typebot.blocks + .filter( + (block) => typebot && block.steps.some((step) => isInputStep(step)) + ) + .map((block) => { + const inputStep = block.steps.find((step) => + isInputStep(step) + ) as InputStep return { Header: ( @@ -72,16 +84,8 @@ export const parseSubmissionsColumns = ( {block.title} ), - accessor: blockId, + accessor: block.id, } }), ] } - -const blockContainsInput = ( - typebot: PublicTypebot | Typebot, - blockId: string -) => - typebot.blocks.byId[blockId].stepIds.some((stepId) => - isInputStep(typebot.steps.byId[stepId]) - ) diff --git a/apps/builder/services/typebots.ts b/apps/builder/services/typebots.ts index b376265b3..a3903d242 100644 --- a/apps/builder/services/typebots.ts +++ b/apps/builder/services/typebots.ts @@ -24,18 +24,27 @@ import { defaultUrlInputOptions, defaultChoiceInputOptions, defaultSetVariablesOptions, - defaultConditionOptions, defaultRedirectOptions, defaultGoogleSheetsOptions, defaultGoogleAnalyticsOptions, defaultWebhookOptions, StepWithOptionsType, + defaultWebhookAttributes, + Webhook, + Item, + ItemType, + defaultConditionContent, } from 'models' import shortId, { generate } from 'short-uuid' import { Typebot } from 'models' import useSWR from 'swr' import { fetcher, toKebabCase } from './utils' -import { isBubbleStepType, stepTypeHasOption } from 'utils' +import { + isBubbleStepType, + stepTypeHasItems, + stepTypeHasOption, + stepTypeHasWebhook, +} from 'utils' import { deepEqual } from 'fast-equals' import { stringify } from 'qs' import { isChoiceInput, isConditionStep, sendRequest } from 'utils' @@ -125,9 +134,35 @@ export const parseNewStep = ( options: stepTypeHasOption(type) ? parseDefaultStepOptions(type) : undefined, + webhook: stepTypeHasWebhook(type) ? parseDefaultWebhook() : undefined, + items: stepTypeHasItems(type) ? parseDefaultItems(type, id) : undefined, } as DraggableStep } +const parseDefaultWebhook = (): Webhook => ({ + id: generate(), + ...defaultWebhookAttributes, +}) + +const parseDefaultItems = ( + type: LogicStepType.CONDITION | InputStepType.CHOICE, + stepId: string +): Item[] => { + switch (type) { + case InputStepType.CHOICE: + return [{ id: generate(), stepId, type: ItemType.BUTTON }] + case LogicStepType.CONDITION: + return [ + { + id: generate(), + stepId, + type: ItemType.CONDITION, + content: defaultConditionContent, + }, + ] + } +} + const parseDefaultContent = (type: BubbleStepType): BubbleStepContent => { switch (type) { case BubbleStepType.TEXT: @@ -154,11 +189,9 @@ const parseDefaultStepOptions = (type: StepWithOptionsType): StepOptions => { case InputStepType.URL: return defaultUrlInputOptions case InputStepType.CHOICE: - return { ...defaultChoiceInputOptions, itemIds: [generate()] } + return defaultChoiceInputOptions case LogicStepType.SET_VARIABLE: return defaultSetVariablesOptions - case LogicStepType.CONDITION: - return defaultConditionOptions case LogicStepType.REDIRECT: return defaultRedirectOptions case IntegrationStepType.GOOGLE_SHEETS: @@ -166,7 +199,7 @@ const parseDefaultStepOptions = (type: StepWithOptionsType): StepOptions => { case IntegrationStepType.GOOGLE_ANALYTICS: return defaultGoogleAnalyticsOptions case IntegrationStepType.WEBHOOK: - return { ...defaultWebhookOptions, webhookId: generate() } + return defaultWebhookOptions } } @@ -181,7 +214,6 @@ export const checkIfPublished = ( publicTypebot: PublicTypebot ) => deepEqual(typebot.blocks, publicTypebot.blocks) && - deepEqual(typebot.steps, publicTypebot.steps) && typebot.name === publicTypebot.name && typebot.publicId === publicTypebot.publicId && deepEqual(typebot.settings, publicTypebot.settings) && @@ -214,18 +246,15 @@ export const parseNewTypebot = ({ id: startBlockId, title: 'Start', graphCoordinates: { x: 0, y: 0 }, - stepIds: [startStepId], + steps: [startStep], } return { folderId, name, ownerId, - blocks: { byId: { [startBlockId]: startBlock }, allIds: [startBlockId] }, - steps: { byId: { [startStepId]: startStep }, allIds: [startStepId] }, - choiceItems: { byId: {}, allIds: [] }, - variables: { byId: {}, allIds: [] }, - edges: { byId: {}, allIds: [] }, - webhooks: { byId: {}, allIds: [] }, + blocks: [startBlock], + edges: [], + variables: [], theme: defaultTheme, settings: defaultSettings, } diff --git a/apps/builder/services/utils/utils.ts b/apps/builder/services/utils/utils.ts index de8ae90cc..947573d54 100644 --- a/apps/builder/services/utils/utils.ts +++ b/apps/builder/services/utils/utils.ts @@ -100,11 +100,8 @@ export const removeUndefinedFields = (obj: T): T => export const stepHasOptions = (step: Step) => 'options' in step -export const parseVariableHighlight = (content: string, typebot?: Typebot) => { - if (!typebot) return content - const varNames = typebot.variables.allIds.map( - (varId) => typebot.variables.byId[varId].name - ) +export const parseVariableHighlight = (content: string, typebot: Typebot) => { + const varNames = typebot.variables.map((v) => v.name) return content.replace(/\{\{(.*?)\}\}/g, (fullMatch, foundVar) => { if (varNames.some((val) => foundVar.includes(val))) { return `${fullMatch.replace( @@ -115,3 +112,8 @@ export const parseVariableHighlight = (content: string, typebot?: Typebot) => { return fullMatch }) } + +export const setMultipleRefs = + (refs: React.MutableRefObject[]) => + (elem: HTMLDivElement) => + refs.forEach((ref) => (ref.current = elem)) diff --git a/apps/viewer/layouts/TypebotPage.tsx b/apps/viewer/layouts/TypebotPage.tsx index e62a7f98b..052a6afba 100644 --- a/apps/viewer/layouts/TypebotPage.tsx +++ b/apps/viewer/layouts/TypebotPage.tsx @@ -5,7 +5,6 @@ import { upsertAnswer } from 'services/answer' import { SEO } from '../components/Seo' import { createResult, updateResult } from '../services/result' import { ErrorPage } from './ErrorPage' -import { NotFoundPage } from './NotFoundPage' export type TypebotPageProps = { typebot?: PublicTypebot @@ -15,7 +14,11 @@ export type TypebotPageProps = { const sessionStorageKey = 'resultId' -export const TypebotPage = ({ typebot, isIE, url }: TypebotPageProps) => { +export const TypebotPage = ({ + typebot, + isIE, + url, +}: TypebotPageProps & { typebot: PublicTypebot }) => { const [error, setError] = useState( isIE ? new Error('Internet explorer is not supported') : undefined ) @@ -27,7 +30,6 @@ export const TypebotPage = ({ typebot, isIE, url }: TypebotPageProps) => { }, []) const initializeResult = async () => { - if (!typebot) return const resultIdFromSession = sessionStorage.getItem(sessionStorageKey) if (resultIdFromSession) setResultId(resultIdFromSession) else { @@ -52,9 +54,6 @@ export const TypebotPage = ({ typebot, isIE, url }: TypebotPageProps) => { if (error) setError(error) } - if (!typebot) { - return - } if (error) { return } diff --git a/apps/viewer/pages/[publicId].tsx b/apps/viewer/pages/[publicId].tsx index 24a5ff3b8..9991ade9c 100644 --- a/apps/viewer/pages/[publicId].tsx +++ b/apps/viewer/pages/[publicId].tsx @@ -1,3 +1,4 @@ +import { NotFoundPage } from 'layouts/NotFoundPage' import { PublicTypebot } from 'models' import { GetServerSideProps, GetServerSidePropsContext } from 'next' import { TypebotPage, TypebotPageProps } from '../layouts/TypebotPage' @@ -12,7 +13,6 @@ export const getServerSideProps: GetServerSideProps = async ( try { if (!context.req.headers.host) return { props: {} } typebot = await getTypebotFromPublicId(context.query.publicId?.toString()) - if (!typebot) return { props: {} } return { props: { typebot, @@ -41,5 +41,7 @@ const getTypebotFromPublicId = async ( return (typebot as unknown as PublicTypebot | undefined) ?? undefined } -const App = (props: TypebotPageProps) => +const App = ({ typebot, ...props }: TypebotPageProps) => + typebot ? : + export default App diff --git a/apps/viewer/pages/index.tsx b/apps/viewer/pages/index.tsx index f643b1e3d..02f1d68fd 100644 --- a/apps/viewer/pages/index.tsx +++ b/apps/viewer/pages/index.tsx @@ -1,3 +1,4 @@ +import { NotFoundPage } from 'layouts/NotFoundPage' import { PublicTypebot } from 'models' import { GetServerSideProps, GetServerSidePropsContext } from 'next' import { TypebotPage, TypebotPageProps } from '../layouts/TypebotPage' @@ -12,7 +13,6 @@ export const getServerSideProps: GetServerSideProps = async ( try { if (!context.req.headers.host) return { props: {} } typebot = await getTypebotFromUrl(context.req.headers.host) - if (!typebot) return { props: {} } return { props: { typebot, @@ -42,5 +42,6 @@ const getTypebotFromUrl = async ( return (typebot as unknown as PublicTypebot | undefined) ?? undefined } -const App = (props: TypebotPageProps) => +const App = ({ typebot, ...props }: TypebotPageProps) => + typebot ? : export default App diff --git a/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx b/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx index b774c382f..ee02de073 100644 --- a/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx +++ b/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx @@ -4,7 +4,7 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group' import { ChatStep } from './ChatStep' import { AvatarSideContainer } from './AvatarSideContainer' import { HostAvatarsContext } from '../../contexts/HostAvatarsContext' -import { Step } from 'models' +import { PublicStep } from 'models' import { useTypebot } from '../../contexts/TypebotContext' import { isBubbleStep, @@ -14,26 +14,28 @@ import { isLogicStep, } from 'utils' import { executeLogic } from 'services/logic' -import { getSingleChoiceTargetId } from 'services/inputs' import { executeIntegration } from 'services/integration' type ChatBlockProps = { - stepIds: string[] - startStepId?: string + steps: PublicStep[] + startStepIndex: number + blockIndex: number onBlockEnd: (edgeId?: string) => void } export const ChatBlock = ({ - stepIds, - startStepId, + steps, + startStepIndex, + blockIndex, onBlockEnd, }: ChatBlockProps) => { const { typebot, updateVariableValue } = useTypebot() - const [displayedSteps, setDisplayedSteps] = useState([]) + const [displayedSteps, setDisplayedSteps] = useState([]) + + const currentStepIndex = displayedSteps.length - 1 useEffect(() => { - const nextStep = - typebot.steps.byId[startStepId ?? stepIds[displayedSteps.length]] + const nextStep = steps[startStepIndex] if (nextStep) setDisplayedSteps([...displayedSteps, nextStep]) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -60,6 +62,7 @@ export const ChatBlock = ({ typebot.typebotId, currentStep, typebot.variables, + { blockIndex, stepIndex: currentStepIndex }, updateVariableValue ) nextEdgeId ? onBlockEnd(nextEdgeId) : displayNextStep() @@ -85,18 +88,17 @@ export const ChatBlock = ({ } const isSingleChoiceStep = isChoiceInput(currentStep) && !currentStep.options.isMultipleChoice - if (isSingleChoiceStep) - return onBlockEnd( - getSingleChoiceTargetId( - currentStep, - typebot.choiceItems, - answerContent - ) + if (isSingleChoiceStep) { + onBlockEnd( + currentStep.items.find((i) => i.content === answerContent) + ?.outgoingEdgeId ) - if (currentStep?.edgeId || displayedSteps.length === stepIds.length) - return onBlockEnd(currentStep.edgeId) + } + + if (currentStep?.outgoingEdgeId || displayedSteps.length === steps.length) + return onBlockEnd(currentStep.outgoingEdgeId) } - const nextStep = typebot.steps.byId[stepIds[displayedSteps.length]] + const nextStep = steps[displayedSteps.length] if (nextStep) setDisplayedSteps([...displayedSteps, nextStep]) } diff --git a/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx b/packages/bot-engine/src/components/ChatBlock/ChatStep/ChatStep.tsx index a018e4bd0..cf20a1e3c 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 { InputStep, InputStepType, Step } from 'models' +import { InputStep, InputStepType, PublicStep, Step } from 'models' import { GuestBubble } from './bubbles/GuestBubble' import { TextForm } from './inputs/TextForm' import { isBubbleStep, isInputStep } from 'utils' @@ -13,7 +13,7 @@ export const ChatStep = ({ step, onTransitionEnd, }: { - step: Step + step: PublicStep onTransitionEnd: (answerContent?: string) => void }) => { const { addAnswer } = useAnswers() @@ -63,6 +63,6 @@ const InputChatStep = ({ case InputStepType.DATE: return case InputStepType.CHOICE: - return + return } } diff --git a/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/VideoBubble.tsx b/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/VideoBubble.tsx index fef80fe04..63a8727fa 100644 --- a/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/VideoBubble.tsx +++ b/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/VideoBubble.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react' import { useHostAvatars } from 'contexts/HostAvatarsContext' import { useTypebot } from 'contexts/TypebotContext' import { - Table, Variable, VideoBubbleContent, VideoBubbleContentType, @@ -83,7 +82,7 @@ const VideoContent = ({ }: { content?: VideoBubbleContent isTyping: boolean - variables: Table + variables: Variable[] }) => { const url = useMemo( () => parseVariables({ text: content?.url, variables: variables }), diff --git a/packages/bot-engine/src/components/ChatBlock/ChatStep/inputs/ChoiceForm.tsx b/packages/bot-engine/src/components/ChatBlock/ChatStep/inputs/ChoiceForm.tsx index 8959d48f4..438f15eb5 100644 --- a/packages/bot-engine/src/components/ChatBlock/ChatStep/inputs/ChoiceForm.tsx +++ b/packages/bot-engine/src/components/ChatBlock/ChatStep/inputs/ChoiceForm.tsx @@ -1,65 +1,61 @@ -import { ChoiceInputOptions } from 'models' -import React, { useMemo, useState } from 'react' -import { filterTable } from 'utils' -import { useTypebot } from '../../../../contexts/TypebotContext' +import { ChoiceInputStep } from 'models' +import React, { useState } from 'react' import { SendButton } from './SendButton' type ChoiceFormProps = { - options?: ChoiceInputOptions + step: ChoiceInputStep onSubmit: (value: string) => void } -export const ChoiceForm = ({ options, onSubmit }: ChoiceFormProps) => { - const { typebot } = useTypebot() - const items = useMemo( - () => filterTable(options?.itemIds ?? [], typebot.choiceItems), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ) - const [selectedIds, setSelectedIds] = useState([]) +export const ChoiceForm = ({ step, onSubmit }: ChoiceFormProps) => { + const [selectedIndices, setSelectedIndices] = useState([]) - const handleClick = (itemId: string) => (e: React.MouseEvent) => { + const handleClick = (itemIndex: number) => (e: React.MouseEvent) => { e.preventDefault() - if (options?.isMultipleChoice) toggleSelectedItemId(itemId) - else onSubmit(items.byId[itemId].content ?? '') + if (step.options?.isMultipleChoice) toggleSelectedItemIndex(itemIndex) + else onSubmit(step.items[itemIndex].content ?? '') } - const toggleSelectedItemId = (itemId: string) => { - const existingIndex = selectedIds.indexOf(itemId) + const toggleSelectedItemIndex = (itemIndex: number) => { + const existingIndex = selectedIndices.indexOf(itemIndex) if (existingIndex !== -1) { - selectedIds.splice(existingIndex, 1) - setSelectedIds([...selectedIds]) + selectedIndices.splice(existingIndex, 1) + setSelectedIndices([...selectedIndices]) } else { - setSelectedIds([...selectedIds, itemId]) + setSelectedIndices([...selectedIndices, itemIndex]) } } const handleSubmit = () => - onSubmit(selectedIds.map((itemId) => items.byId[itemId].content).join(', ')) + onSubmit( + selectedIndices + .map((itemIndex) => step.items[itemIndex].content) + .join(', ') + ) return (
- {options?.itemIds.map((itemId) => ( + {step.items.map((item, idx) => ( ))}
- {selectedIds.length > 0 && ( - + {selectedIndices.length > 0 && ( + )}
diff --git a/packages/bot-engine/src/components/ConversationContainer.tsx b/packages/bot-engine/src/components/ConversationContainer.tsx index 4f58e5a0a..823c6a1c3 100644 --- a/packages/bot-engine/src/components/ConversationContainer.tsx +++ b/packages/bot-engine/src/components/ConversationContainer.tsx @@ -5,11 +5,12 @@ import { useFrame } from 'react-frame-component' import { setCssVariablesValue } from '../services/theme' import { useAnswers } from '../contexts/AnswersContext' import { deepEqual } from 'fast-equals' -import { Answer, Block, PublicTypebot } from 'models' +import { Answer, Edge, PublicBlock, PublicTypebot } from 'models' +import { byId } from 'utils' type Props = { typebot: PublicTypebot - onNewBlockVisible: (edgeId: string) => void + onNewBlockVisible: (edge: Edge) => void onNewAnswer: (answer: Answer) => void onCompleted: () => void } @@ -21,30 +22,29 @@ export const ConversationContainer = ({ }: Props) => { const { document: frameDocument } = useFrame() const [displayedBlocks, setDisplayedBlocks] = useState< - { block: Block; startStepId?: string }[] + { block: PublicBlock; startStepIndex: number }[] >([]) const [localAnswer, setLocalAnswer] = useState() const { answers } = useAnswers() const bottomAnchor = useRef(null) const displayNextBlock = (edgeId?: string) => { - const edge = typebot.edges.byId[edgeId ?? ''] - if (!edge) return onCompleted() - const nextBlock = { - block: typebot.blocks.byId[edge.to.blockId], - startStepId: edge.to.stepId, - } + const nextEdge = typebot.edges.find(byId(edgeId)) + if (!nextEdge) return onCompleted() + const nextBlock = typebot.blocks.find(byId(nextEdge.to.blockId)) if (!nextBlock) return onCompleted() - onNewBlockVisible(edge.id) - setDisplayedBlocks([...displayedBlocks, nextBlock]) + const startStepIndex = nextEdge.to.stepId + ? nextBlock.steps.findIndex(byId(nextEdge.to.stepId)) + : 0 + onNewBlockVisible(nextEdge) + setDisplayedBlocks([ + ...displayedBlocks, + { block: nextBlock, startStepIndex }, + ]) } useEffect(() => { - const blocks = typebot.blocks - const firstEdgeId = - typebot.steps.byId[blocks.byId[blocks.allIds[0]].stepIds[0]].edgeId - if (!firstEdgeId) return - displayNextBlock(firstEdgeId) + displayNextBlock(typebot.blocks[0].steps[0].outgoingEdgeId) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -68,8 +68,9 @@ export const ConversationContainer = ({ {displayedBlocks.map((displayedBlock, idx) => ( ))} diff --git a/packages/bot-engine/src/components/TypebotViewer.tsx b/packages/bot-engine/src/components/TypebotViewer.tsx index 9a4535a21..afffc1405 100644 --- a/packages/bot-engine/src/components/TypebotViewer.tsx +++ b/packages/bot-engine/src/components/TypebotViewer.tsx @@ -10,11 +10,11 @@ import phoneNumberInputStyle from 'react-phone-number-input/style.css' import phoneSyle from '../assets/phone.css' import { ConversationContainer } from './ConversationContainer' import { AnswersContext } from '../contexts/AnswersContext' -import { Answer, BackgroundType, PublicTypebot } from 'models' +import { Answer, BackgroundType, Edge, PublicTypebot } from 'models' export type TypebotViewerProps = { typebot: PublicTypebot - onNewBlockVisible?: (edgeId: string) => void + onNewBlockVisible?: (edge: Edge) => void onNewAnswer?: (answer: Answer) => void onCompleted?: () => void } @@ -31,8 +31,8 @@ export const TypebotViewer = ({ : 'transparent', [typebot?.theme?.general?.background] ) - const handleNewBlockVisible = (blockId: string) => { - if (onNewBlockVisible) onNewBlockVisible(blockId) + const handleNewBlockVisible = (edge: Edge) => { + if (onNewBlockVisible) onNewBlockVisible(edge) } const handleNewAnswer = (answer: Answer) => { if (onNewAnswer) onNewAnswer(answer) diff --git a/packages/bot-engine/src/contexts/TypebotContext.tsx b/packages/bot-engine/src/contexts/TypebotContext.tsx index ee4d4e03e..e9961feb6 100644 --- a/packages/bot-engine/src/contexts/TypebotContext.tsx +++ b/packages/bot-engine/src/contexts/TypebotContext.tsx @@ -20,13 +20,9 @@ export const TypebotContext = ({ const updateVariableValue = (variableId: string, value: string) => { setLocalTypebot((typebot) => ({ ...typebot, - variables: { - ...typebot.variables, - byId: { - ...typebot.variables.byId, - [variableId]: { ...typebot.variables.byId[variableId], value }, - }, - }, + variables: typebot.variables.map((v) => + v.id === variableId ? { ...v, value } : v + ), })) } return ( diff --git a/packages/bot-engine/src/services/inputs.ts b/packages/bot-engine/src/services/inputs.ts deleted file mode 100644 index 828857d88..000000000 --- a/packages/bot-engine/src/services/inputs.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ChoiceInputStep, ChoiceItem, Table } from 'models' - -export const getSingleChoiceTargetId = ( - currentStep: ChoiceInputStep, - choiceItems: Table, - answerContent?: string -): string | undefined => { - const itemId = currentStep.options.itemIds.find( - (itemId) => choiceItems.byId[itemId].content === answerContent - ) - if (!itemId) throw new Error('itemId should exist') - return choiceItems.byId[itemId].edgeId ?? currentStep.edgeId -} diff --git a/packages/bot-engine/src/services/integration.ts b/packages/bot-engine/src/services/integration.ts index 2413503a3..da1152f57 100644 --- a/packages/bot-engine/src/services/integration.ts +++ b/packages/bot-engine/src/services/integration.ts @@ -5,7 +5,6 @@ import { GoogleSheetsAction, GoogleSheetsInsertRowOptions, Variable, - Table, GoogleSheetsUpdateRowOptions, Cell, GoogleSheetsGetOptions, @@ -19,10 +18,12 @@ import { parseVariables, parseVariablesInObject } from './variable' const safeEval = eval +type Indices = { blockIndex: number; stepIndex: number } export const executeIntegration = ( typebotId: string, step: IntegrationStep, - variables: Table, + variables: Variable[], + indices: Indices, updateVariableValue: (variableId: string, value: string) => void ) => { switch (step.type) { @@ -31,13 +32,19 @@ export const executeIntegration = ( case IntegrationStepType.GOOGLE_ANALYTICS: return executeGoogleAnalyticsIntegration(step, variables) case IntegrationStepType.WEBHOOK: - return executeWebhook(typebotId, step, variables, updateVariableValue) + return executeWebhook( + typebotId, + step, + variables, + indices, + updateVariableValue + ) } } export const executeGoogleAnalyticsIntegration = async ( step: GoogleAnalyticsStep, - variables: Table + variables: Variable[] ) => { if (!step.options?.trackingId) return const { default: initGoogleAnalytics } = await import('../../lib/gtag') @@ -47,10 +54,10 @@ export const executeGoogleAnalyticsIntegration = async ( const executeGoogleSheetIntegration = async ( step: GoogleSheetsStep, - variables: Table, + variables: Variable[], updateVariableValue: (variableId: string, value: string) => void ) => { - if (!('action' in step.options)) return step.edgeId + if (!('action' in step.options)) return step.outgoingEdgeId switch (step.options.action) { case GoogleSheetsAction.INSERT_ROW: await insertRowInGoogleSheets(step.options, variables) @@ -62,12 +69,12 @@ const executeGoogleSheetIntegration = async ( await getRowFromGoogleSheets(step.options, variables, updateVariableValue) break } - return step.edgeId + return step.outgoingEdgeId } const insertRowInGoogleSheets = async ( options: GoogleSheetsInsertRowOptions, - variables: Table + variables: Variable[] ) => { if (!options.cellsToInsert) return return sendRequest({ @@ -82,7 +89,7 @@ const insertRowInGoogleSheets = async ( const updateRowInGoogleSheets = async ( options: GoogleSheetsUpdateRowOptions, - variables: Table + variables: Variable[] ) => { if (!options.cellsToUpsert || !options.referenceCell) return return sendRequest({ @@ -104,7 +111,7 @@ const updateRowInGoogleSheets = async ( const getRowFromGoogleSheets = async ( options: GoogleSheetsGetOptions, - variables: Table, + variables: Variable[], updateVariableValue: (variableId: string, value: string) => void ) => { if (!options.referenceCell || !options.cellsToExtract) return @@ -118,9 +125,7 @@ const getRowFromGoogleSheets = async ( variables, }), }, - columns: options.cellsToExtract.allIds.map( - (id) => options.cellsToExtract?.byId[id].column - ), + columns: options.cellsToExtract.map((cell) => cell.column), }, { indices: false } ) @@ -129,18 +134,15 @@ const getRowFromGoogleSheets = async ( method: 'GET', }) if (!data) return - options.cellsToExtract.allIds.forEach((cellId) => { - const cell = options.cellsToExtract?.byId[cellId] - if (!cell) return + options.cellsToExtract.forEach((cell) => updateVariableValue(cell.variableId ?? '', data[cell.column ?? '']) - }) + ) } const parseCellValues = ( - cells: Table, - variables: Table + cells: Cell[], + variables: Variable[] ): { [key: string]: string } => - cells.allIds.reduce((row, id) => { - const cell = cells.byId[id] + cells.reduce((row, cell) => { return !cell.column || !cell.value ? row : { @@ -152,20 +154,21 @@ const parseCellValues = ( const executeWebhook = async ( typebotId: string, step: WebhookStep, - variables: Table, + variables: Variable[], + indices: Indices, updateVariableValue: (variableId: string, value: string) => void ) => { - if (!step.options?.webhookId) return step.edgeId + if (!step.webhook) return step.outgoingEdgeId + const { blockIndex, stepIndex } = indices const { data, error } = await sendRequest({ - url: `http://localhost:3000/api/typebots/${typebotId}/webhooks/${step.options?.webhookId}/execute`, + url: `http://localhost:3000/api/typebots/${typebotId}/blocks/${blockIndex}/steps/${stepIndex}/executeWebhook`, method: 'POST', body: { variables, }, }) console.error(error) - step.options.responseVariableMapping?.allIds.forEach((varMappingId) => { - const varMapping = step.options?.responseVariableMapping?.byId[varMappingId] + step.options.responseVariableMapping.forEach((varMapping) => { if (!varMapping?.bodyPath || !varMapping.variableId) return const value = safeEval(`(${JSON.stringify(data)}).${varMapping?.bodyPath}`) updateVariableValue(varMapping.variableId, value) diff --git a/packages/bot-engine/src/services/logic.ts b/packages/bot-engine/src/services/logic.ts index 175deb30a..869ff3257 100644 --- a/packages/bot-engine/src/services/logic.ts +++ b/packages/bot-engine/src/services/logic.ts @@ -3,20 +3,21 @@ import { LogicStepType, LogicalOperator, ConditionStep, - Table, Variable, ComparisonOperators, SetVariableStep, RedirectStep, + Comparison, } from 'models' import { isDefined, isNotDefined } from 'utils' import { sanitizeUrl } from './utils' import { isMathFormula, evaluateExpression, parseVariables } from './variable' type EdgeId = string + export const executeLogic = ( step: LogicStep, - variables: Table, + variables: Variable[], updateVariableValue: (variableId: string, expression: string) => void ): EdgeId | undefined => { switch (step.type) { @@ -31,40 +32,36 @@ export const executeLogic = ( const executeSetVariable = ( step: SetVariableStep, - variables: Table, + variables: Variable[], updateVariableValue: (variableId: string, expression: string) => void ): EdgeId | undefined => { if (!step.options?.variableId || !step.options.expressionToEvaluate) - return step.edgeId + return step.outgoingEdgeId const expression = step.options.expressionToEvaluate const evaluatedExpression = isMathFormula(expression) ? evaluateExpression(parseVariables({ text: expression, variables })) : expression updateVariableValue(step.options.variableId, evaluatedExpression) - return step.edgeId + return step.outgoingEdgeId } const executeCondition = ( step: ConditionStep, - variables: Table + variables: Variable[] ): EdgeId | undefined => { + const { content } = step.items[0] const isConditionPassed = - step.options?.logicalOperator === LogicalOperator.AND - ? step.options?.comparisons.allIds.every( - executeComparison(step, variables) - ) - : step.options?.comparisons.allIds.some( - executeComparison(step, variables) - ) - return isConditionPassed ? step.trueEdgeId : step.falseEdgeId + content.logicalOperator === LogicalOperator.AND + ? content.comparisons.every(executeComparison(variables)) + : content.comparisons.some(executeComparison(variables)) + return isConditionPassed ? step.items[0].outgoingEdgeId : step.outgoingEdgeId } const executeComparison = - (step: ConditionStep, variables: Table) => - (comparisonId: string) => { - const comparison = step.options?.comparisons.byId[comparisonId] + (variables: Variable[]) => (comparison: Comparison) => { if (!comparison?.variableId) return false - const inputValue = variables.byId[comparison.variableId].value ?? '' + const inputValue = + variables.find((v) => v.id === comparison.variableId)?.value ?? '' const { value } = comparison if (isNotDefined(value)) return false switch (comparison.comparisonOperator) { @@ -91,12 +88,12 @@ const executeComparison = const executeRedirect = ( step: RedirectStep, - variables: Table + variables: Variable[] ): EdgeId | undefined => { - if (!step.options?.url) return step.edgeId + if (!step.options?.url) return step.outgoingEdgeId window.open( sanitizeUrl(parseVariables({ text: step.options?.url, variables })), step.options.isNewTab ? '_blank' : '_self' ) - return step.edgeId + return step.outgoingEdgeId } diff --git a/packages/bot-engine/src/services/variable.ts b/packages/bot-engine/src/services/variable.ts index b7f6e11eb..0f2b9a79e 100644 --- a/packages/bot-engine/src/services/variable.ts +++ b/packages/bot-engine/src/services/variable.ts @@ -1,4 +1,4 @@ -import { Table, Variable } from 'models' +import { Variable } from 'models' import { isDefined } from 'utils' const safeEval = eval @@ -11,16 +11,16 @@ export const parseVariables = ({ variables, }: { text?: string - variables: Table + variables: Variable[] }): string => { if (!text || text === '') return '' return text.replace(/\{\{(.*?)\}\}/g, (_, fullVariableString) => { const matchedVarName = fullVariableString.replace(/{{|}}/g, '') - const matchedVariableId = variables.allIds.find((variableId) => { - const variable = variables.byId[variableId] - return matchedVarName === variable.name && isDefined(variable.value) - }) - return variables.byId[matchedVariableId ?? '']?.value ?? '' + return ( + variables.find((v) => { + return matchedVarName === v.name && isDefined(v.value) + })?.value ?? '' + ) }) } @@ -50,7 +50,7 @@ const countDecimals = (value: number) => { export const parseVariablesInObject = ( object: { [key: string]: string | number }, - variables: Table + variables: Variable[] ) => Object.keys(object).reduce((newObj, key) => { const currentValue = object[key] diff --git a/packages/bot-engine/tsconfig.json b/packages/bot-engine/tsconfig.json index 2292756c5..a8a746894 100644 --- a/packages/bot-engine/tsconfig.json +++ b/packages/bot-engine/tsconfig.json @@ -14,6 +14,7 @@ "moduleResolution": "node", "allowSyntheticDefaultImports": true, "emitDeclarationOnly": true, - "baseUrl": "./src" + "baseUrl": "./src", + "downlevelIteration": true } } diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 166e285f9..bc589d385 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -106,30 +106,25 @@ model Typebot { results Result[] folderId String? folder DashboardFolder? @relation(fields: [folderId], references: [id]) - blocks Json - steps Json - choiceItems Json - variables Json - webhooks Json - edges Json + blocks Json[] + variables Json[] + edges Json[] theme Json settings Json publicId String? @unique } model PublicTypebot { - id String @id @default(cuid()) - typebotId String @unique - typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade) - name String - blocks Json - steps Json - choiceItems Json - variables Json - edges Json - theme Json - settings Json - publicId String? @unique + id String @id @default(cuid()) + typebotId String @unique + typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade) + name String + blocks Json[] + variables Json[] + edges Json[] + theme Json + settings Json + publicId String? @unique } model Result { diff --git a/packages/models/src/publicTypebot.ts b/packages/models/src/publicTypebot.ts index 41ab43eb0..913892d72 100644 --- a/packages/models/src/publicTypebot.ts +++ b/packages/models/src/publicTypebot.ts @@ -1,23 +1,16 @@ +import { Block, Edge, Settings, Step, Theme, Variable } from './typebot' import { PublicTypebot as PublicTypebotFromPrisma } from 'db' -import { Block, ChoiceItem, Edge, Settings, Step, Theme } from './typebot' -import { Variable } from './typebot/variable' -import { Table } from './utils' export type PublicTypebot = Omit< PublicTypebotFromPrisma, - | 'blocks' - | 'startBlock' - | 'theme' - | 'settings' - | 'steps' - | 'choiceItems' - | 'variables' + 'blocks' | 'theme' | 'settings' | 'variables' | 'edges' > & { - blocks: Table - steps: Table - choiceItems: Table - variables: Table - edges: Table + blocks: PublicBlock[] + variables: Variable[] + edges: Edge[] theme: Theme settings: Settings } + +export type PublicBlock = Omit & { steps: PublicStep[] } +export type PublicStep = Omit & { webhook?: string } diff --git a/packages/models/src/typebot/steps/index.ts b/packages/models/src/typebot/steps/index.ts index 32874be24..e03aa24f1 100644 --- a/packages/models/src/typebot/steps/index.ts +++ b/packages/models/src/typebot/steps/index.ts @@ -3,3 +3,4 @@ export * from './bubble' export * from './inputs' export * from './logic' export * from './integration' +export * from './item' diff --git a/packages/models/src/typebot/steps/inputs.ts b/packages/models/src/typebot/steps/inputs.ts index 9e3d98738..2c0f29f6a 100644 --- a/packages/models/src/typebot/steps/inputs.ts +++ b/packages/models/src/typebot/steps/inputs.ts @@ -1,3 +1,5 @@ +import { ItemBase, ItemType } from '.' +import { Item } from './item' import { StepBase } from './steps' export type InputStep = @@ -62,14 +64,13 @@ export type PhoneNumberInputOptions = OptionBase & InputTextOptionsBase export type ChoiceInputStep = StepBase & { type: InputStepType.CHOICE + items: ButtonItem[] options: ChoiceInputOptions } -export type ChoiceItem = { - id: string - stepId: string +export type ButtonItem = ItemBase & { + type: ItemType.BUTTON content?: string - edgeId?: string } type OptionBase = { variableId?: string } @@ -78,7 +79,6 @@ type InputTextOptionsBase = { } export type ChoiceInputOptions = OptionBase & { - itemIds: string[] isMultipleChoice: boolean buttonLabel: string } @@ -140,5 +140,4 @@ export const defaultPhoneInputOptions: PhoneNumberInputOptions = { export const defaultChoiceInputOptions: ChoiceInputOptions = { buttonLabel: defaultButtonLabel, isMultipleChoice: false, - itemIds: [], } diff --git a/packages/models/src/typebot/steps/integration.ts b/packages/models/src/typebot/steps/integration.ts index 7641028c1..17ed73b73 100644 --- a/packages/models/src/typebot/steps/integration.ts +++ b/packages/models/src/typebot/steps/integration.ts @@ -1,5 +1,4 @@ import { StepBase } from '.' -import { Table } from '../..' export type IntegrationStep = | GoogleSheetsStep @@ -30,6 +29,7 @@ export type GoogleAnalyticsStep = StepBase & { export type WebhookStep = StepBase & { type: IntegrationStepType.WEBHOOK options: WebhookOptions + webhook: Webhook } export type GoogleAnalyticsOptions = { @@ -58,34 +58,41 @@ export type GoogleSheetsOptionsBase = { sheetId?: string } -export type Cell = { column?: string; value?: string } -export type ExtractingCell = { column?: string; variableId?: string } +export type Cell = { id: string; column?: string; value?: string } +export type ExtractingCell = { + id: string + column?: string + variableId?: string +} export type GoogleSheetsGetOptions = NonNullable & { action: GoogleSheetsAction.GET referenceCell?: Cell - cellsToExtract: Table + cellsToExtract: ExtractingCell[] } export type GoogleSheetsInsertRowOptions = NonNullable & { action: GoogleSheetsAction.INSERT_ROW - cellsToInsert: Table + cellsToInsert: Cell[] } export type GoogleSheetsUpdateRowOptions = NonNullable & { action: GoogleSheetsAction.UPDATE_ROW referenceCell?: Cell - cellsToUpsert: Table + cellsToUpsert: Cell[] } -export type ResponseVariableMapping = { bodyPath?: string; variableId?: string } +export type ResponseVariableMapping = { + id: string + bodyPath?: string + variableId?: string +} export type WebhookOptions = { - webhookId: string - variablesForTest: Table - responseVariableMapping: Table + variablesForTest: VariableForTest[] + responseVariableMapping: ResponseVariableMapping[] } export enum HttpMethod { @@ -100,15 +107,19 @@ export enum HttpMethod { TRACE = 'TRACE', } -export type KeyValue = { key?: string; value?: string } -export type VariableForTest = { variableId?: string; value?: string } +export type KeyValue = { id: string; key?: string; value?: string } +export type VariableForTest = { + id: string + variableId?: string + value?: string +} export type Webhook = { id: string url?: string method: HttpMethod - queryParams: Table - headers: Table + queryParams: KeyValue[] + headers: KeyValue[] body?: string } @@ -122,12 +133,12 @@ export const defaultGoogleSheetsOptions: GoogleSheetsOptions = {} export const defaultGoogleAnalyticsOptions: GoogleAnalyticsOptions = {} export const defaultWebhookOptions: Omit = { - responseVariableMapping: { byId: {}, allIds: [] }, - variablesForTest: { byId: {}, allIds: [] }, + responseVariableMapping: [], + variablesForTest: [], } export const defaultWebhookAttributes: Omit = { method: HttpMethod.GET, - headers: { byId: {}, allIds: [] }, - queryParams: { byId: {}, allIds: [] }, + headers: [], + queryParams: [], } diff --git a/packages/models/src/typebot/steps/item.ts b/packages/models/src/typebot/steps/item.ts new file mode 100644 index 000000000..1b4f498a0 --- /dev/null +++ b/packages/models/src/typebot/steps/item.ts @@ -0,0 +1,20 @@ +import { ButtonItem, ConditionItem } from '.' + +export type Item = ButtonItem | ConditionItem + +export enum ItemType { + BUTTON, + CONDITION, +} + +export type ItemBase = { + id: string + stepId: string + outgoingEdgeId?: string +} + +export type ItemIndices = { + blockIndex: number + stepIndex: number + itemIndex: number +} diff --git a/packages/models/src/typebot/steps/logic.ts b/packages/models/src/typebot/steps/logic.ts index 6504ed310..8bf136035 100644 --- a/packages/models/src/typebot/steps/logic.ts +++ b/packages/models/src/typebot/steps/logic.ts @@ -1,5 +1,5 @@ -import { StepBase } from '.' -import { Table } from '../..' +import { ItemType, StepBase } from '.' +import { ItemBase } from './item' export type LogicStep = SetVariableStep | ConditionStep | RedirectStep @@ -9,10 +9,7 @@ export enum LogicStepType { REDIRECT = 'Redirect', } -export type LogicStepOptions = - | SetVariableOptions - | ConditionOptions - | RedirectOptions +export type LogicStepOptions = SetVariableOptions | RedirectOptions export type SetVariableStep = StepBase & { type: LogicStepType.SET_VARIABLE @@ -21,9 +18,12 @@ export type SetVariableStep = StepBase & { export type ConditionStep = StepBase & { type: LogicStepType.CONDITION - options: ConditionOptions - trueEdgeId?: string - falseEdgeId?: string + items: [ConditionItem] +} + +export type ConditionItem = ItemBase & { + type: ItemType.CONDITION + content: ConditionContent } export type RedirectStep = StepBase & { @@ -45,8 +45,8 @@ export enum ComparisonOperators { IS_SET = 'Is set', } -export type ConditionOptions = { - comparisons: Table +export type ConditionContent = { + comparisons: Comparison[] logicalOperator: LogicalOperator } @@ -69,8 +69,8 @@ export type RedirectOptions = { export const defaultSetVariablesOptions: SetVariableOptions = {} -export const defaultConditionOptions: ConditionOptions = { - comparisons: { byId: {}, allIds: [] }, +export const defaultConditionContent: ConditionContent = { + comparisons: [], logicalOperator: LogicalOperator.AND, } diff --git a/packages/models/src/typebot/steps/steps.ts b/packages/models/src/typebot/steps/steps.ts index 04189eccd..d56823853 100644 --- a/packages/models/src/typebot/steps/steps.ts +++ b/packages/models/src/typebot/steps/steps.ts @@ -2,8 +2,12 @@ import { InputStepOptions, IntegrationStepOptions, IntegrationStepType, + Item, LogicStepOptions, + RedirectStep, + SetVariableStep, } from '.' +import { Edge } from '..' import { BubbleStep, BubbleStepType } from './bubble' import { InputStep, InputStepType } from './inputs' import { IntegrationStep } from './integration' @@ -31,11 +35,16 @@ export type DraggableStepType = | LogicStepType | IntegrationStepType -export type StepWithOptions = InputStep | LogicStep | IntegrationStep +export type StepWithOptions = + | InputStep + | SetVariableStep + | RedirectStep + | IntegrationStep export type StepWithOptionsType = | InputStepType - | LogicStepType + | LogicStepType.REDIRECT + | LogicStepType.SET_VARIABLE | IntegrationStepType export type StepOptions = @@ -43,9 +52,16 @@ export type StepOptions = | LogicStepOptions | IntegrationStepOptions -export type StepBase = { id: string; blockId: string; edgeId?: string } +export type StepWithItems = Omit & { items: Item[] } + +export type StepBase = { id: string; blockId: string; outgoingEdgeId?: string } export type StartStep = StepBase & { type: 'start' label: string } + +export type StepIndices = { + blockIndex: number + stepIndex: number +} diff --git a/packages/models/src/typebot/typebot.ts b/packages/models/src/typebot/typebot.ts index 1b2449c54..46c68e54e 100644 --- a/packages/models/src/typebot/typebot.ts +++ b/packages/models/src/typebot/typebot.ts @@ -1,28 +1,16 @@ import { Typebot as TypebotFromPrisma } from 'db' -import { ChoiceItem } from './steps/inputs' -import { Table } from '../utils' import { Settings } from './settings' import { Step } from './steps/steps' import { Theme } from './theme' import { Variable } from './variable' -import { Webhook } from '.' export type Typebot = Omit< TypebotFromPrisma, - | 'blocks' - | 'theme' - | 'settings' - | 'steps' - | 'choiceItems' - | 'variables' - | 'webhooks' + 'blocks' | 'theme' | 'settings' | 'variables' | 'edges' > & { - blocks: Table - steps: Table - choiceItems: Table - variables: Table - edges: Table - webhooks: Table + blocks: Block[] + variables: Variable[] + edges: Edge[] theme: Theme settings: Settings } @@ -34,14 +22,13 @@ export type Block = { x: number y: number } - stepIds: string[] + steps: Step[] } export type Source = { blockId: string stepId: string - buttonId?: string - conditionType?: 'true' | 'false' + itemId?: string } export type Target = { blockId: string; stepId?: string } export type Edge = { diff --git a/packages/models/src/utils.ts b/packages/models/src/utils.ts index fe61c4a39..fdd9fdf5e 100644 --- a/packages/models/src/utils.ts +++ b/packages/models/src/utils.ts @@ -1,3 +1 @@ -export type Table = { byId: { [key: string]: T }; allIds: string[] } - -export const defaultTable = { byId: {}, allIds: [] } +export type IdMap = { [id: string]: T } diff --git a/packages/utils/src/utils.ts b/packages/utils/src/utils.ts index e98350a06..bb9f954e6 100644 --- a/packages/utils/src/utils.ts +++ b/packages/utils/src/utils.ts @@ -10,12 +10,12 @@ import { LogicStep, LogicStepType, Step, - Table, TextInputStep, TextBubbleStep, WebhookStep, StepType, StepWithOptionsType, + PublicStep, } from 'models' export const sendRequest = async ({ @@ -50,39 +50,44 @@ export const isNotDefined = ( value: T | undefined | null ): value is undefined | null => value === undefined || value === null -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 => +export const isInputStep = (step: Step | PublicStep): step is InputStep => (Object.values(InputStepType) as string[]).includes(step.type) -export const isBubbleStep = (step: Step): step is BubbleStep => +export const isBubbleStep = (step: Step | PublicStep): step is BubbleStep => (Object.values(BubbleStepType) as string[]).includes(step.type) -export const isLogicStep = (step: Step): step is LogicStep => +export const isLogicStep = (step: Step | PublicStep): step is LogicStep => (Object.values(LogicStepType) as string[]).includes(step.type) -export const isTextBubbleStep = (step: Step): step is TextBubbleStep => - step.type === BubbleStepType.TEXT +export const isTextBubbleStep = ( + step: Step | PublicStep +): step is TextBubbleStep => step.type === BubbleStepType.TEXT -export const isTextInputStep = (step: Step): step is TextInputStep => - step.type === InputStepType.TEXT +export const isTextInputStep = ( + step: Step | PublicStep +): step is TextInputStep => step.type === InputStepType.TEXT -export const isChoiceInput = (step: Step): step is ChoiceInputStep => - step.type === InputStepType.CHOICE +export const isChoiceInput = ( + step: Step | PublicStep +): step is ChoiceInputStep => step.type === InputStepType.CHOICE -export const isSingleChoiceInput = (step: Step): step is ChoiceInputStep => - step.type === InputStepType.CHOICE && !step.options.isMultipleChoice +export const isSingleChoiceInput = ( + step: Step | PublicStep +): step is ChoiceInputStep => + step.type === InputStepType.CHOICE && + 'options' in step && + !step.options.isMultipleChoice -export const isConditionStep = (step: Step): step is ConditionStep => - step.type === LogicStepType.CONDITION +export const isConditionStep = ( + step: Step | PublicStep +): step is ConditionStep => step.type === LogicStepType.CONDITION -export const isIntegrationStep = (step: Step): step is IntegrationStep => +export const isIntegrationStep = ( + step: Step | PublicStep +): step is IntegrationStep => (Object.values(IntegrationStepType) as string[]).includes(step.type) -export const isWebhookStep = (step: Step): step is WebhookStep => +export const isWebhookStep = (step: Step | PublicStep): step is WebhookStep => step.type === IntegrationStepType.WEBHOOK export const isBubbleStepType = (type: StepType): type is BubbleStepType => @@ -95,3 +100,18 @@ export const stepTypeHasOption = ( .concat(Object.values(LogicStepType)) .concat(Object.values(IntegrationStepType)) .includes(type) + +export const stepTypeHasWebhook = ( + type: StepType +): type is IntegrationStepType.WEBHOOK => type === IntegrationStepType.WEBHOOK + +export const stepTypeHasItems = ( + type: StepType +): type is LogicStepType.CONDITION | InputStepType.CHOICE => + type === LogicStepType.CONDITION || type === InputStepType.CHOICE + +export const stepHasItems = ( + step: Step +): step is ConditionStep | ChoiceInputStep => 'items' in step + +export const byId = (id?: string) => (obj: { id: string }) => obj.id === id