2
0

refactor(graph): ♻️ Add Edges table in Typebot

This commit is contained in:
Baptiste Arnaud
2022-01-19 18:54:49 +01:00
parent ab34f95cce
commit 8bbd8977b2
59 changed files with 1118 additions and 991 deletions

View File

@@ -5,26 +5,31 @@ import {
getAnchorsPosition, getAnchorsPosition,
computeEdgePath, computeEdgePath,
getEndpointTopOffset, getEndpointTopOffset,
getSourceEndpointId,
} from 'services/graph' } from 'services/graph'
type Props = { stepId: string } type Props = { edgeId: string }
export const Edge = ({ stepId }: Props) => { export const Edge = ({ edgeId }: Props) => {
const { typebot } = useAnalyticsGraph() const { typebot } = useAnalyticsGraph()
const step = typebot?.steps.byId[stepId] const edge = typebot?.edges.byId[edgeId]
const { sourceEndpoints, targetEndpoints, graphPosition } = useGraph() const { sourceEndpoints, targetEndpoints, graphPosition } = useGraph()
const [sourceTop, setSourceTop] = useState( const [sourceTop, setSourceTop] = useState(
getEndpointTopOffset(graphPosition, sourceEndpoints, stepId) getEndpointTopOffset(
graphPosition,
sourceEndpoints,
getSourceEndpointId(edge)
)
) )
const [targetTop, setTargetTop] = useState( const [targetTop, setTargetTop] = useState(
getEndpointTopOffset(graphPosition, sourceEndpoints, step?.target?.stepId) getEndpointTopOffset(graphPosition, sourceEndpoints, edge?.to.stepId)
) )
useEffect(() => { useEffect(() => {
const newSourceTop = getEndpointTopOffset( const newSourceTop = getEndpointTopOffset(
graphPosition, graphPosition,
sourceEndpoints, sourceEndpoints,
stepId getSourceEndpointId(edge)
) )
const sensibilityThreshold = 10 const sensibilityThreshold = 10
const newSourceTopIsTooClose = const newSourceTopIsTooClose =
@@ -39,7 +44,7 @@ export const Edge = ({ stepId }: Props) => {
const newTargetTop = getEndpointTopOffset( const newTargetTop = getEndpointTopOffset(
graphPosition, graphPosition,
targetEndpoints, targetEndpoints,
step?.target?.stepId edge?.to.stepId
) )
const sensibilityThreshold = 10 const sensibilityThreshold = 10
const newSourceTopIsTooClose = const newSourceTopIsTooClose =
@@ -51,15 +56,14 @@ export const Edge = ({ stepId }: Props) => {
}, [graphPosition]) }, [graphPosition])
const { sourceBlock, targetBlock } = useMemo(() => { const { sourceBlock, targetBlock } = useMemo(() => {
if (!typebot) return {} if (!typebot || !edge) return {}
if (!step?.target) return {} const targetBlock = typebot.blocks.byId[edge.to.blockId]
const targetBlock = typebot.blocks.byId[step.target.blockId] const sourceBlock = typebot.blocks.byId[edge.from.blockId]
const sourceBlock = typebot.blocks.byId[step.blockId]
return { return {
sourceBlock, sourceBlock,
targetBlock, targetBlock,
} }
}, [step?.blockId, step?.target, typebot]) }, [edge, typebot])
const path = useMemo(() => { const path = useMemo(() => {
if (!sourceBlock || !targetBlock) return `` if (!sourceBlock || !targetBlock) return ``

View File

@@ -1,8 +1,7 @@
import { chakra } from '@chakra-ui/system' import { chakra } from '@chakra-ui/system'
import { useAnalyticsGraph } from 'contexts/AnalyticsGraphProvider' import { useAnalyticsGraph } from 'contexts/AnalyticsGraphProvider'
import React, { useMemo } from 'react' import React from 'react'
import { AnswersCount } from 'services/analytics' import { AnswersCount } from 'services/analytics'
import { isDefined } from 'utils'
import { DropOffBlock } from '../blocks/DropOffBlock' import { DropOffBlock } from '../blocks/DropOffBlock'
import { DropOffEdge } from './DropOffEdge' import { DropOffEdge } from './DropOffEdge'
import { Edge } from './Edge' import { Edge } from './Edge'
@@ -12,13 +11,6 @@ type Props = { answersCounts?: AnswersCount[] }
export const Edges = ({ answersCounts }: Props) => { export const Edges = ({ answersCounts }: Props) => {
const { typebot } = useAnalyticsGraph() const { typebot } = useAnalyticsGraph()
const stepIdsWithTarget: string[] = useMemo(() => {
if (!typebot) return []
return typebot.steps.allIds.filter((stepId) =>
isDefined(typebot.steps.byId[stepId].target)
)
}, [typebot])
return ( return (
<> <>
<chakra.svg <chakra.svg
@@ -29,8 +21,8 @@ export const Edges = ({ answersCounts }: Props) => {
left="0" left="0"
top="0" top="0"
> >
{stepIdsWithTarget.map((stepId) => ( {typebot?.edges.allIds.map((edgeId) => (
<Edge key={stepId} stepId={stepId} /> <Edge key={edgeId} edgeId={edgeId} />
))} ))}
<marker <marker
id={'arrow'} id={'arrow'}

View File

@@ -3,6 +3,7 @@ import { useAnalyticsGraph } from 'contexts/AnalyticsGraphProvider'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { AnswersCount } from 'services/analytics' import { AnswersCount } from 'services/analytics'
import { computeSourceCoordinates } from 'services/graph' import { computeSourceCoordinates } from 'services/graph'
import { isDefined } from 'utils'
type Props = { type Props = {
answersCounts: AnswersCount[] answersCounts: AnswersCount[]
@@ -20,11 +21,12 @@ export const DropOffBlock = ({ answersCounts, blockId }: Props) => {
const { totalDroppedUser, dropOffRate } = useMemo(() => { const { totalDroppedUser, dropOffRate } = useMemo(() => {
if (!typebot || totalAnswers === undefined) if (!typebot || totalAnswers === undefined)
return { previousTotal: undefined, dropOffRate: undefined } return { previousTotal: undefined, dropOffRate: undefined }
const previousBlockIds = typebot.blocks.allIds.filter(() => const previousBlockIds = typebot.edges.allIds
typebot.steps.allIds.find( .map((edgeId) => {
(sId) => typebot.steps.byId[sId].target?.blockId === blockId const edge = typebot.edges.byId[edgeId]
) return edge.to.blockId === blockId ? edge.from.blockId : undefined
) })
.filter((blockId) => isDefined(blockId))
const previousTotal = answersCounts const previousTotal = answersCounts
.filter((a) => previousBlockIds.includes(a.blockId)) .filter((a) => previousBlockIds.includes(a.blockId))
.reduce((prev, acc) => acc.totalAnswers + prev, 0) .reduce((prev, acc) => acc.totalAnswers + prev, 0)

View File

@@ -20,18 +20,17 @@ type Props = {
} }
export const BlockNode = ({ block }: Props) => { export const BlockNode = ({ block }: Props) => {
const { connectingIds, setConnectingIds, previewingIds } = useGraph() const { connectingIds, setConnectingIds, previewingEdgeId } = useGraph()
const { typebot, updateBlock } = useTypebot() const { typebot, updateBlock } = useTypebot()
const { setMouseOverBlockId } = useDnd() const { setMouseOverBlockId } = useDnd()
const { draggedStep, draggedStepType } = useDnd() const { draggedStep, draggedStepType } = useDnd()
const [isMouseDown, setIsMouseDown] = useState(false) const [isMouseDown, setIsMouseDown] = useState(false)
const [isConnecting, setIsConnecting] = useState(false) const [isConnecting, setIsConnecting] = useState(false)
const isPreviewing = useMemo( const isPreviewing = useMemo(() => {
() => if (!previewingEdgeId) return
previewingIds.sourceId === block.id || const edge = typebot?.edges.byId[previewingEdgeId]
previewingIds.targetId === block.id, return edge?.to.blockId === block.id || edge?.from.blockId === block.id
[block.id, previewingIds.sourceId, previewingIds.targetId] }, [block.id, previewingEdgeId, typebot?.edges.byId])
)
useEffect(() => { useEffect(() => {
setIsConnecting( setIsConnecting(

View File

@@ -128,7 +128,7 @@ export const ChoiceItemNode = ({
source={{ source={{
blockId: typebot.steps.byId[item.stepId].blockId, blockId: typebot.steps.byId[item.stepId].blockId,
stepId: item.stepId, stepId: item.stepId,
choiceItemId: item.id, nodeId: item.id,
}} }}
pos="absolute" pos="absolute"
right="15px" right="15px"

View File

@@ -49,9 +49,7 @@ export const SettingsPopoverContent = ({ step, onExpandClick }: Props) => {
<PopoverContent onMouseDown={handleMouseDown} pos="relative"> <PopoverContent onMouseDown={handleMouseDown} pos="relative">
<PopoverArrow /> <PopoverArrow />
<PopoverBody <PopoverBody
px="6" p="6"
pb="6"
pt="4"
overflowY="scroll" overflowY="scroll"
maxH="400px" maxH="400px"
ref={ref} ref={ref}

View File

@@ -1,12 +1,13 @@
import { Box, BoxProps, Flex } from '@chakra-ui/react' import { Box, BoxProps, Flex } from '@chakra-ui/react'
import { ConnectingSourceIds, useGraph } from 'contexts/GraphContext' import { useGraph } from 'contexts/GraphContext'
import { Source } from 'models'
import React, { MouseEvent, useEffect, useRef } from 'react' import React, { MouseEvent, useEffect, useRef } from 'react'
export const SourceEndpoint = ({ export const SourceEndpoint = ({
source, source,
...props ...props
}: BoxProps & { }: BoxProps & {
source: ConnectingSourceIds source: Source
}) => { }) => {
const { setConnectingIds, addSourceEndpoint: addEndpoint } = useGraph() const { setConnectingIds, addSourceEndpoint: addEndpoint } = useGraph()
const ref = useRef<HTMLDivElement | null>(null) const ref = useRef<HTMLDivElement | null>(null)
@@ -18,8 +19,7 @@ export const SourceEndpoint = ({
useEffect(() => { useEffect(() => {
if (!ref.current) return if (!ref.current) return
const id = const id = source.nodeId ?? source.stepId + (source.conditionType ?? '')
source.choiceItemId ?? source.stepId + (source.conditionType ?? '')
addEndpoint({ addEndpoint({
id, id,
ref, ref,

View File

@@ -1,5 +1,4 @@
import { import {
Box,
Flex, Flex,
HStack, HStack,
Popover, Popover,
@@ -7,12 +6,11 @@ import {
useDisclosure, useDisclosure,
useEventListener, useEventListener,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import React, { useEffect, useMemo, useState } from 'react' import React, { useEffect, useState } from 'react'
import { Block, DraggableStep, Step } from 'models' import { DraggableStep, Step } from 'models'
import { useGraph } from 'contexts/GraphContext' import { useGraph } from 'contexts/GraphContext'
import { StepIcon } from 'components/board/StepTypesList/StepIcon' import { StepIcon } from 'components/board/StepTypesList/StepIcon'
import { import {
isDefined,
isInputStep, isInputStep,
isLogicStep, isLogicStep,
isTextBubbleStep, isTextBubbleStep,
@@ -50,7 +48,7 @@ export const StepNode = ({
}) => { }) => {
const { query } = useRouter() const { query } = useRouter()
const { setConnectingIds, connectingIds } = useGraph() const { setConnectingIds, connectingIds } = useGraph()
const { moveStep, typebot } = useTypebot() const { moveStep } = useTypebot()
const [isConnecting, setIsConnecting] = useState(false) const [isConnecting, setIsConnecting] = useState(false)
const [mouseDownEvent, setMouseDownEvent] = const [mouseDownEvent, setMouseDownEvent] =
useState<{ absolute: Coordinates; relative: Coordinates }>() useState<{ absolute: Coordinates; relative: Coordinates }>()
@@ -132,30 +130,6 @@ export const StepNode = ({
setIsEditing(false) setIsEditing(false)
} }
const connectedStubPosition: 'right' | 'left' | undefined = useMemo(() => {
if (!typebot) return
const currentBlock = typebot.blocks?.byId[step.blockId]
const isDragginConnectorFromCurrentBlock =
connectingIds?.source.blockId === currentBlock?.id &&
connectingIds?.target?.blockId
const targetBlockId = isDragginConnectorFromCurrentBlock
? connectingIds.target?.blockId
: step.target?.blockId
const targetedBlock = targetBlockId && typebot.blocks.byId[targetBlockId]
return targetedBlock
? targetedBlock.graphCoordinates.x <
(currentBlock as Block).graphCoordinates.x
? 'left'
: 'right'
: undefined
}, [
typebot,
step.blockId,
step.target?.blockId,
connectingIds?.source.blockId,
connectingIds?.target?.blockId,
])
return isEditing && isTextBubbleStep(step) ? ( return isEditing && isTextBubbleStep(step) ? (
<TextEditor <TextEditor
stepId={step.id} stepId={step.id}
@@ -184,16 +158,6 @@ export const StepNode = ({
data-testid={`step-${step.id}`} data-testid={`step-${step.id}`}
w="full" w="full"
> >
{connectedStubPosition === 'left' && (
<Box
h="2px"
pos="absolute"
left="-18px"
top="25px"
w="18px"
bgColor="blue.500"
/>
)}
<HStack <HStack
flex="1" flex="1"
userSelect="none" userSelect="none"
@@ -225,24 +189,6 @@ export const StepNode = ({
/> />
)} )}
</HStack> </HStack>
{isDefined(connectedStubPosition) &&
hasDefaultConnector(step) &&
isConnectable && (
<Box
h="2px"
pos="absolute"
right={
connectedStubPosition === 'left' ? undefined : '-18px'
}
left={
connectedStubPosition === 'left' ? '-18px' : undefined
}
top="25px"
w="18px"
bgColor="gray.500"
/>
)}
</Flex> </Flex>
</PopoverTrigger> </PopoverTrigger>
{hasPopover(step) && ( {hasPopover(step) && (

View File

@@ -1,8 +1,9 @@
import { useEventListener } from '@chakra-ui/hooks' import { useEventListener } from '@chakra-ui/hooks'
import assert from 'assert'
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader' import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
import { useGraph, ConnectingIds } from 'contexts/GraphContext' import { useGraph, ConnectingIds } from 'contexts/GraphContext'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { Step, Target } from 'models' import { Target } from 'models'
import React, { useMemo, useState } from 'react' import React, { useMemo, useState } from 'react'
import { import {
computeConnectingEdgePath, computeConnectingEdgePath,
@@ -18,7 +19,7 @@ export const DrawingEdge = () => {
sourceEndpoints, sourceEndpoints,
targetEndpoints, targetEndpoints,
} = useGraph() } = useGraph()
const { typebot, updateStep, updateChoiceItem } = useTypebot() const { typebot, createEdge } = useTypebot()
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }) const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
const sourceBlock = useMemo( const sourceBlock = useMemo(
@@ -32,7 +33,7 @@ export const DrawingEdge = () => {
return getEndpointTopOffset( return getEndpointTopOffset(
graphPosition, graphPosition,
sourceEndpoints, sourceEndpoints,
connectingIds.source.choiceItemId ?? connectingIds.source.nodeId ??
connectingIds.source.stepId + (connectingIds.source.conditionType ?? '') connectingIds.source.stepId + (connectingIds.source.conditionType ?? '')
) )
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -79,23 +80,8 @@ export const DrawingEdge = () => {
}) })
const createNewEdge = (connectingIds: ConnectingIds) => { const createNewEdge = (connectingIds: ConnectingIds) => {
if (connectingIds.source.choiceItemId) { assert(connectingIds.target)
updateChoiceItem(connectingIds.source.choiceItemId, { createEdge({ from: connectingIds.source, to: connectingIds.target })
target: connectingIds.target,
})
} else if (connectingIds.source.conditionType === 'true') {
updateStep(connectingIds.source.stepId, {
trueTarget: connectingIds.target,
} as Step)
} else if (connectingIds.source.conditionType === 'false') {
updateStep(connectingIds.source.stepId, {
falseTarget: connectingIds.target,
} as Step)
} else {
updateStep(connectingIds.source.stepId, {
target: connectingIds.target,
})
}
} }
if ((mousePosition.x === 0 && mousePosition.y === 0) || !connectingIds) if ((mousePosition.x === 0 && mousePosition.y === 0) || !connectingIds)

View File

@@ -1,14 +1,11 @@
import { isDefined } from '@udecode/plate-core'
import assert from 'assert'
import { Coordinates, useGraph } from 'contexts/GraphContext' import { Coordinates, useGraph } from 'contexts/GraphContext'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { ChoiceItem } from 'models'
import React, { useEffect, useMemo, useState } from 'react' import React, { useEffect, useMemo, useState } from 'react'
import { import {
getAnchorsPosition, getAnchorsPosition,
computeEdgePath, computeEdgePath,
getEndpointTopOffset, getEndpointTopOffset,
getTarget, getSourceEndpointId,
} from 'services/graph' } from 'services/graph'
export type AnchorsPositionProps = { export type AnchorsPositionProps = {
@@ -18,44 +15,31 @@ export type AnchorsPositionProps = {
totalSegments: number totalSegments: number
} }
export enum EdgeType { export const Edge = ({ edgeId }: { edgeId: string }) => {
STEP,
CHOICE_ITEM,
CONDITION_TRUE,
CONDITION_FALSE,
}
export const Edge = ({
type,
stepId,
item,
}: {
type: EdgeType
stepId: string
item?: ChoiceItem
}) => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
const { previewingIds, sourceEndpoints, targetEndpoints, graphPosition } = const { previewingEdgeId, sourceEndpoints, targetEndpoints, graphPosition } =
useGraph() useGraph()
const step = typebot?.steps.byId[stepId] const edge = useMemo(
const isPreviewing = useMemo( () => typebot?.edges.byId[edgeId],
() => [edgeId, typebot?.edges.byId]
previewingIds.sourceId === step?.blockId &&
previewingIds.targetId === step?.target?.blockId,
[previewingIds.sourceId, previewingIds.targetId, step]
) )
const isPreviewing = previewingEdgeId === edgeId
const [sourceTop, setSourceTop] = useState( const [sourceTop, setSourceTop] = useState(
getEndpointTopOffset(graphPosition, sourceEndpoints, item?.id ?? step?.id) getEndpointTopOffset(
graphPosition,
sourceEndpoints,
getSourceEndpointId(edge)
)
) )
const [targetTop, setTargetTop] = useState( const [targetTop, setTargetTop] = useState(
getEndpointTopOffset(graphPosition, targetEndpoints, step?.id) getEndpointTopOffset(graphPosition, targetEndpoints, edge?.to.stepId)
) )
useEffect(() => { useEffect(() => {
const newSourceTop = getEndpointTopOffset( const newSourceTop = getEndpointTopOffset(
graphPosition, graphPosition,
sourceEndpoints, sourceEndpoints,
getSourceEndpointId() getSourceEndpointId(edge)
) )
const sensibilityThreshold = 10 const sensibilityThreshold = 10
const newSourceTopIsTooClose = const newSourceTopIsTooClose =
@@ -66,26 +50,12 @@ export const Edge = ({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [typebot?.blocks, typebot?.steps, graphPosition, sourceEndpoints]) }, [typebot?.blocks, typebot?.steps, graphPosition, sourceEndpoints])
const getSourceEndpointId = () => {
switch (type) {
case EdgeType.STEP:
return step?.id
case EdgeType.CHOICE_ITEM:
return item?.id
case EdgeType.CONDITION_TRUE:
return step?.id + 'true'
case EdgeType.CONDITION_FALSE:
return step?.id + 'false'
}
}
useEffect(() => { useEffect(() => {
if (!step) return if (!edge) return
const target = getTarget(step, type)
const newTargetTop = getEndpointTopOffset( const newTargetTop = getEndpointTopOffset(
graphPosition, graphPosition,
targetEndpoints, targetEndpoints,
target?.blockId ?? target?.stepId edge?.to.stepId
) )
const sensibilityThreshold = 10 const sensibilityThreshold = 10
const newSourceTopIsTooClose = const newSourceTopIsTooClose =
@@ -97,20 +67,17 @@ export const Edge = ({
}, [typebot?.blocks, typebot?.steps, graphPosition, targetEndpoints]) }, [typebot?.blocks, typebot?.steps, graphPosition, targetEndpoints])
const { sourceBlock, targetBlock } = useMemo(() => { const { sourceBlock, targetBlock } = useMemo(() => {
if (!typebot) return {} if (!typebot || !edge?.from.stepId) return {}
const step = typebot.steps.byId[stepId] const sourceBlock = typebot.blocks.byId[edge.from.blockId]
const sourceBlock = typebot.blocks.byId[step.blockId] const targetBlock = typebot.blocks.byId[edge.to.blockId]
const targetBlockId = getTarget(step, type)?.blockId
assert(isDefined(targetBlockId))
const targetBlock = typebot.blocks.byId[targetBlockId]
return { return {
sourceBlock, sourceBlock,
targetBlock, targetBlock,
} }
}, [stepId, type, typebot]) }, [edge?.from.blockId, edge?.from.stepId, edge?.to.blockId, typebot])
const path = useMemo(() => { const path = useMemo(() => {
if (!sourceBlock || !targetBlock || !step) return `` if (!sourceBlock || !targetBlock) return ``
const anchorsPosition = getAnchorsPosition({ const anchorsPosition = getAnchorsPosition({
sourceBlock, sourceBlock,
targetBlock, targetBlock,
@@ -118,7 +85,7 @@ export const Edge = ({
targetTop, targetTop,
}) })
return computeEdgePath(anchorsPosition) return computeEdgePath(anchorsPosition)
}, [sourceBlock, sourceTop, step, targetBlock, targetTop]) }, [sourceBlock, sourceTop, targetBlock, targetTop])
if (sourceTop === 0) return <></> if (sourceTop === 0) return <></>
return ( return (

View File

@@ -1,39 +1,11 @@
import { chakra } from '@chakra-ui/system' import { chakra } from '@chakra-ui/system'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { ChoiceItem, ConditionStep } from 'models' import React from 'react'
import React, { useMemo } from 'react'
import { isConditionStep, isDefined, isSingleChoiceInput } from 'utils'
import { DrawingEdge } from './DrawingEdge' import { DrawingEdge } from './DrawingEdge'
import { Edge, EdgeType } from './Edge' import { Edge } from './Edge'
export const Edges = () => { export const Edges = () => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
const stepIdsWithTarget: string[] = useMemo(() => {
if (!typebot) return []
return typebot.steps.allIds.filter((stepId) =>
isDefined(typebot.steps.byId[stepId].target)
)
}, [typebot])
const singleChoiceItemsWithTarget: ChoiceItem[] = useMemo(() => {
if (!typebot) return []
return typebot.choiceItems.allIds
.filter(
(itemId) =>
isDefined(typebot.choiceItems.byId[itemId].target) &&
isSingleChoiceInput(
typebot.steps.byId[typebot.choiceItems.byId[itemId].stepId]
)
)
.map((itemId) => typebot.choiceItems.byId[itemId])
}, [typebot])
const conditionStepIdsWithTarget = useMemo(
() =>
typebot?.steps.allIds.filter((stepId) => {
const step = typebot.steps.byId[stepId]
return isConditionStep(step) && (step.trueTarget || step.falseTarget)
}),
[typebot?.steps.allIds, typebot?.steps.byId]
)
return ( return (
<chakra.svg <chakra.svg
@@ -45,19 +17,8 @@ export const Edges = () => {
top="0" top="0"
> >
<DrawingEdge /> <DrawingEdge />
{stepIdsWithTarget.map((stepId) => ( {typebot?.edges.allIds.map((edgeId) => (
<Edge key={stepId} stepId={stepId} type={EdgeType.STEP} /> <Edge key={edgeId} edgeId={edgeId} />
))}
{singleChoiceItemsWithTarget.map((item) => (
<Edge
key={item.id}
stepId={item.stepId}
item={item}
type={EdgeType.CHOICE_ITEM}
/>
))}
{conditionStepIdsWithTarget?.map((stepId) => (
<ConditionStepEdges key={stepId} stepId={stepId} />
))} ))}
<marker <marker
id={'arrow'} id={'arrow'}
@@ -92,18 +53,3 @@ export const Edges = () => {
</chakra.svg> </chakra.svg>
) )
} }
const ConditionStepEdges = ({ stepId }: { stepId: string }) => {
const { typebot } = useTypebot()
const step = typebot?.steps.byId[stepId] as ConditionStep
return (
<>
{step.trueTarget && (
<Edge type={EdgeType.CONDITION_TRUE} stepId={stepId} />
)}
{step.falseTarget && (
<Edge type={EdgeType.CONDITION_FALSE} stepId={stepId} />
)}
</>
)
}

View File

@@ -19,7 +19,7 @@ import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
export const PreviewDrawer = () => { export const PreviewDrawer = () => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
const { setRightPanel } = useEditor() const { setRightPanel } = useEditor()
const { previewingIds, setPreviewingIds } = useGraph() const { setPreviewingEdgeId } = useGraph()
const [isResizing, setIsResizing] = useState(false) const [isResizing, setIsResizing] = useState(false)
const [width, setWidth] = useState(500) const [width, setWidth] = useState(500)
const [isResizeHandleVisible, setIsResizeHandleVisible] = useState(false) const [isResizeHandleVisible, setIsResizeHandleVisible] = useState(false)
@@ -45,13 +45,7 @@ export const PreviewDrawer = () => {
} }
useEventListener('mouseup', handleMouseUp) useEventListener('mouseup', handleMouseUp)
const handleNewBlockVisible = (targetId: string) => const handleNewBlockVisible = (edgeId: string) => setPreviewingEdgeId(edgeId)
setPreviewingIds({
sourceId: !previewingIds.sourceId
? typebot?.blocks.allIds[0]
: previewingIds.targetId,
targetId: targetId,
})
const handleRestartClick = () => setRestartKey((key) => key + 1) const handleRestartClick = () => setRestartKey((key) => key + 1)

View File

@@ -1,9 +1,8 @@
import { import {
HStack,
IconButton, IconButton,
Input, Input,
InputGroup,
InputProps, InputProps,
InputRightElement,
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
@@ -55,6 +54,7 @@ export const InputWithVariableButton = ({
if (!inputRef.current) return if (!inputRef.current) return
inputRef.current.selectionStart = inputRef.current.selectionEnd = inputRef.current.selectionStart = inputRef.current.selectionEnd =
carretPosition + `{{${variable.name}}}`.length carretPosition + `{{${variable.name}}}`.length
setCarretPosition(inputRef.current.selectionStart)
}, 100) }, 100)
} }
@@ -67,7 +67,7 @@ export const InputWithVariableButton = ({
setValue(e.target.value) setValue(e.target.value)
return ( return (
<InputGroup> <HStack>
<Input <Input
ref={inputRef} ref={inputRef}
onKeyUp={handleKeyUp} onKeyUp={handleKeyUp}
@@ -77,26 +77,24 @@ export const InputWithVariableButton = ({
{...props} {...props}
bgColor={'white'} bgColor={'white'}
/> />
<InputRightElement> <Popover matchWidth isLazy>
<Popover matchWidth isLazy closeOnBlur={false}> <PopoverTrigger>
<PopoverTrigger> <IconButton
<IconButton aria-label="Insert a variable"
aria-label="Insert a variable" icon={<UserIcon />}
icon={<UserIcon />} pos="relative"
size="sm" ml="2"
pos="relative" />
/> </PopoverTrigger>
</PopoverTrigger> <PopoverContent w="full">
<PopoverContent w="full"> <VariableSearchInput
<VariableSearchInput onSelectVariable={handleVariableSelected}
onSelectVariable={handleVariableSelected} placeholder="Search for a variable"
placeholder="Search for a variable" shadow="lg"
shadow="lg" isDefaultOpen
isDefaultOpen />
/> </PopoverContent>
</PopoverContent> </Popover>
</Popover> </HStack>
</InputRightElement>
</InputGroup>
) )
} }

View File

@@ -9,7 +9,6 @@ import {
Button, Button,
InputProps, InputProps,
IconButton, IconButton,
Portal,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { PlusIcon, TrashIcon } from 'assets/icons' import { PlusIcon, TrashIcon } from 'assets/icons'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
@@ -98,8 +97,8 @@ export const VariableSearchInput = ({
isOpen={isOpen} isOpen={isOpen}
initialFocusRef={inputRef} initialFocusRef={inputRef}
matchWidth matchWidth
offset={[0, 0]}
isLazy isLazy
offset={[0, 2]}
> >
<PopoverTrigger> <PopoverTrigger>
<Input <Input

View File

@@ -1,4 +1,4 @@
import { Block, Step, Table, Target } from 'models' import { Block, Source, Step, Table, Target } from 'models'
import { import {
createContext, createContext,
Dispatch, Dispatch,
@@ -43,19 +43,10 @@ export type Node = Omit<Block, 'steps'> & {
const graphPositionDefaultValue = { x: 400, y: 100, scale: 1 } const graphPositionDefaultValue = { x: 400, y: 100, scale: 1 }
export type ConnectingIds = { export type ConnectingIds = {
source: ConnectingSourceIds source: Source
target?: Target target?: Target
} }
export type ConnectingSourceIds = {
blockId: string
stepId: string
choiceItemId?: string
conditionType?: 'true' | 'false'
}
type PreviewingIdsProps = { sourceId?: string; targetId?: string }
type StepId = string type StepId = string
type NodeId = string type NodeId = string
export type Endpoint = { export type Endpoint = {
@@ -68,8 +59,8 @@ const graphContext = createContext<{
setGraphPosition: Dispatch<SetStateAction<Position>> setGraphPosition: Dispatch<SetStateAction<Position>>
connectingIds: ConnectingIds | null connectingIds: ConnectingIds | null
setConnectingIds: Dispatch<SetStateAction<ConnectingIds | null>> setConnectingIds: Dispatch<SetStateAction<ConnectingIds | null>>
previewingIds: PreviewingIdsProps previewingEdgeId?: string
setPreviewingIds: Dispatch<SetStateAction<PreviewingIdsProps>> setPreviewingEdgeId: Dispatch<SetStateAction<string | undefined>>
sourceEndpoints: Table<Endpoint> sourceEndpoints: Table<Endpoint>
addSourceEndpoint: (endpoint: Endpoint) => void addSourceEndpoint: (endpoint: Endpoint) => void
targetEndpoints: Table<Endpoint> targetEndpoints: Table<Endpoint>
@@ -84,7 +75,7 @@ const graphContext = createContext<{
export const GraphProvider = ({ children }: { children: ReactNode }) => { export const GraphProvider = ({ children }: { children: ReactNode }) => {
const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue) const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue)
const [connectingIds, setConnectingIds] = useState<ConnectingIds | null>(null) const [connectingIds, setConnectingIds] = useState<ConnectingIds | null>(null)
const [previewingIds, setPreviewingIds] = useState<PreviewingIdsProps>({}) const [previewingEdgeId, setPreviewingEdgeId] = useState<string>()
const [sourceEndpoints, setSourceEndpoints] = useState<Table<Endpoint>>({ const [sourceEndpoints, setSourceEndpoints] = useState<Table<Endpoint>>({
byId: {}, byId: {},
allIds: [], allIds: [],
@@ -115,8 +106,8 @@ export const GraphProvider = ({ children }: { children: ReactNode }) => {
setGraphPosition, setGraphPosition,
connectingIds, connectingIds,
setConnectingIds, setConnectingIds,
previewingIds, previewingEdgeId,
setPreviewingIds, setPreviewingEdgeId,
sourceEndpoints, sourceEndpoints,
targetEndpoints, targetEndpoints,
addSourceEndpoint, addSourceEndpoint,

View File

@@ -28,6 +28,7 @@ import { useImmer, Updater } from 'use-immer'
import { stepsAction, StepsActions } from './actions/steps' import { stepsAction, StepsActions } from './actions/steps'
import { choiceItemsAction, ChoiceItemsActions } from './actions/choiceItems' import { choiceItemsAction, ChoiceItemsActions } from './actions/choiceItems'
import { variablesAction, VariablesActions } from './actions/variables' import { variablesAction, VariablesActions } from './actions/variables'
import { edgesAction, EdgesActions } from './actions/edges'
type UpdateTypebotPayload = Partial<{ type UpdateTypebotPayload = Partial<{
theme: Theme theme: Theme
@@ -50,7 +51,8 @@ const typebotContext = createContext<
} & BlocksActions & } & BlocksActions &
StepsActions & StepsActions &
ChoiceItemsActions & ChoiceItemsActions &
VariablesActions VariablesActions &
EdgesActions
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
>({}) >({})
@@ -211,6 +213,7 @@ export const TypebotContext = ({
...stepsAction(setLocalTypebot as Updater<Typebot>), ...stepsAction(setLocalTypebot as Updater<Typebot>),
...choiceItemsAction(setLocalTypebot as Updater<Typebot>), ...choiceItemsAction(setLocalTypebot as Updater<Typebot>),
...variablesAction(setLocalTypebot as Updater<Typebot>), ...variablesAction(setLocalTypebot as Updater<Typebot>),
...edgesAction(setLocalTypebot as Updater<Typebot>),
}} }}
> >
{children} {children}

View File

@@ -3,6 +3,7 @@ import { WritableDraft } from 'immer/dist/internal'
import { Block, DraggableStep, DraggableStepType, Typebot } from 'models' import { Block, DraggableStep, DraggableStepType, Typebot } from 'models'
import { parseNewBlock } from 'services/typebots' import { parseNewBlock } from 'services/typebots'
import { Updater } from 'use-immer' import { Updater } from 'use-immer'
import { deleteEdgeDraft } from './edges'
import { createStepDraft, deleteStepDraft } from './steps' import { createStepDraft, deleteStepDraft } from './steps'
export type BlocksActions = { export type BlocksActions = {
@@ -44,6 +45,7 @@ export const blocksActions = (setTypebot: Updater<Typebot>): BlocksActions => ({
deleteBlock: (blockId: string) => deleteBlock: (blockId: string) =>
setTypebot((typebot) => { setTypebot((typebot) => {
deleteStepsInsideBlock(typebot, blockId) deleteStepsInsideBlock(typebot, blockId)
deleteAssociatedEdges(typebot, blockId)
deleteBlockDraft(typebot)(blockId) deleteBlockDraft(typebot)(blockId)
}), }),
}) })
@@ -55,6 +57,16 @@ export const removeEmptyBlocks = (typebot: WritableDraft<Typebot>) => {
emptyBlockIds.forEach(deleteBlockDraft(typebot)) emptyBlockIds.forEach(deleteBlockDraft(typebot))
} }
const deleteAssociatedEdges = (
typebot: WritableDraft<Typebot>,
blockId: string
) => {
typebot.edges.allIds.forEach((edgeId) => {
if (typebot.edges.byId[edgeId].to.blockId === blockId)
deleteEdgeDraft(typebot, edgeId)
})
}
const deleteStepsInsideBlock = ( const deleteStepsInsideBlock = (
typebot: WritableDraft<Typebot>, typebot: WritableDraft<Typebot>,
blockId: string blockId: string

View File

@@ -0,0 +1,69 @@
import { Typebot, Edge, ConditionStep } from 'models'
import { Updater } from 'use-immer'
import { WritableDraft } from 'immer/dist/types/types-external'
import { generate } from 'short-uuid'
export type EdgesActions = {
createEdge: (edge: Omit<Edge, 'id'>) => void
updateEdge: (edgeId: string, updates: Partial<Omit<Edge, 'id'>>) => void
deleteEdge: (edgeId: string) => void
}
export const edgesAction = (setTypebot: Updater<Typebot>): EdgesActions => ({
createEdge: (edge: Omit<Edge, 'id'>) => {
setTypebot((typebot) => {
const newEdge = {
...edge,
id: generate(),
}
if (edge.from.nodeId) {
deleteEdgeDraft(
typebot,
typebot.choiceItems.byId[edge.from.nodeId].edgeId
)
typebot.choiceItems.byId[edge.from.nodeId].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)
})
},
updateEdge: (edgeId: string, updates: Partial<Omit<Edge, 'id'>>) =>
setTypebot((typebot) => {
typebot.edges.byId[edgeId] = {
...typebot.edges.byId[edgeId],
...updates,
}
}),
deleteEdge: (edgeId: string) => {
setTypebot((typebot) => {
deleteEdgeDraft(typebot, edgeId)
})
},
})
export const deleteEdgeDraft = (
typebot: WritableDraft<Typebot>,
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)
}

View File

@@ -11,6 +11,7 @@ import { removeEmptyBlocks } from './blocks'
import { WritableDraft } from 'immer/dist/types/types-external' import { WritableDraft } from 'immer/dist/types/types-external'
import { createChoiceItemDraft, deleteChoiceItemDraft } from './choiceItems' import { createChoiceItemDraft, deleteChoiceItemDraft } from './choiceItems'
import { isChoiceInput } from 'utils' import { isChoiceInput } from 'utils'
import { deleteEdgeDraft } from './edges'
export type StepsActions = { export type StepsActions = {
createStep: ( createStep: (
@@ -50,12 +51,23 @@ export const stepsAction = (setTypebot: Updater<Typebot>): StepsActions => ({
setTypebot((typebot) => { setTypebot((typebot) => {
const step = typebot.steps.byId[stepId] const step = typebot.steps.byId[stepId]
if (isChoiceInput(step)) deleteChoiceItemsInsideStep(typebot, step) if (isChoiceInput(step)) deleteChoiceItemsInsideStep(typebot, step)
deleteAssociatedEdges(typebot, stepId)
removeStepIdFromBlock(typebot, stepId) removeStepIdFromBlock(typebot, stepId)
deleteStepDraft(typebot, stepId) deleteStepDraft(typebot, stepId)
}) })
}, },
}) })
const deleteAssociatedEdges = (
typebot: WritableDraft<Typebot>,
stepId: string
) => {
typebot.edges.allIds.forEach((edgeId) => {
if (typebot.edges.byId[edgeId].from.stepId === stepId)
deleteEdgeDraft(typebot, edgeId)
})
}
const removeStepIdFromBlock = ( const removeStepIdFromBlock = (
typebot: WritableDraft<Typebot>, typebot: WritableDraft<Typebot>,
stepId: string stepId: string

View File

@@ -1,71 +1,96 @@
{ {
"id": "typebot4", "id": "ckylszb9z0354z31a623dg7ji",
"createdAt": "2022-01-17T14:37:01.826Z", "createdAt": "2022-01-19T17:12:27.863Z",
"updatedAt": "2022-01-17T14:37:01.826Z", "updatedAt": "2022-01-19T17:12:27.863Z",
"name": "My typebot", "name": "My typebot",
"ownerId": "user2", "ownerId": "ckylsz8yy0335z31amvq0jwtt",
"publishedTypebotId": null, "publishedTypebotId": null,
"folderId": null, "folderId": null,
"blocks": { "blocks": {
"byId": { "byId": {
"bec5A5bLwenmZpCJc8FRaM": { "j24wz82YG3rjXMgrmCiTLy": {
"id": "bec5A5bLwenmZpCJc8FRaM", "id": "j24wz82YG3rjXMgrmCiTLy",
"title": "Start", "title": "Start",
"stepIds": ["uDMB7a2ucg17WGvbQJjeRn"], "stepIds": ["1NdXPCiRicqDA8k4JfnXfi"],
"graphCoordinates": { "x": 0, "y": 0 } "graphCoordinates": { "x": 0, "y": 0 }
}, },
"bcyiT7P6E99YnHKnpxs4Yux": { "bmaKTUXkT2cc3wtKfK7ra71": {
"id": "bcyiT7P6E99YnHKnpxs4Yux", "id": "bmaKTUXkT2cc3wtKfK7ra71",
"title": "Block #2", "title": "Block #2",
"stepIds": ["step1"], "graphCoordinates": { "x": 175, "y": 197 },
"graphCoordinates": { "x": 411, "y": 108 } "stepIds": ["spHxPWbSqkVZW9gqH86ovC5"]
}, },
"bgpNxHtBBXWrP1QMe2A8hZ9": { "bnt8fM5Wgc8gBg2iSmUcfJu": {
"id": "bgpNxHtBBXWrP1QMe2A8hZ9", "id": "bnt8fM5Wgc8gBg2iSmUcfJu",
"title": "Block #3", "title": "Block #3",
"graphCoordinates": { "x": 1, "y": 236 }, "graphCoordinates": { "x": 504, "y": 347 },
"stepIds": ["sj72oDhJEe4N92KTt64GKWs"] "stepIds": ["siPoEE9H27hVHqykth3a7Kj"]
} }
}, },
"allIds": [ "allIds": [
"bec5A5bLwenmZpCJc8FRaM", "j24wz82YG3rjXMgrmCiTLy",
"bcyiT7P6E99YnHKnpxs4Yux", "bmaKTUXkT2cc3wtKfK7ra71",
"bgpNxHtBBXWrP1QMe2A8hZ9" "bnt8fM5Wgc8gBg2iSmUcfJu"
] ]
}, },
"steps": { "steps": {
"byId": { "byId": {
"step1": { "1NdXPCiRicqDA8k4JfnXfi": {
"id": "step1", "id": "1NdXPCiRicqDA8k4JfnXfi",
"type": "Google Sheets",
"blockId": "bcyiT7P6E99YnHKnpxs4Yux"
},
"uDMB7a2ucg17WGvbQJjeRn": {
"id": "uDMB7a2ucg17WGvbQJjeRn",
"type": "start", "type": "start",
"label": "Start", "label": "Start",
"target": { "blockId": "bgpNxHtBBXWrP1QMe2A8hZ9" }, "blockId": "j24wz82YG3rjXMgrmCiTLy",
"blockId": "bec5A5bLwenmZpCJc8FRaM" "edgeId": "benDCcLMUWNvi6Fg6CXE9H"
}, },
"sj72oDhJEe4N92KTt64GKWs": { "spHxPWbSqkVZW9gqH86ovC5": {
"id": "sj72oDhJEe4N92KTt64GKWs", "id": "spHxPWbSqkVZW9gqH86ovC5",
"blockId": "bgpNxHtBBXWrP1QMe2A8hZ9", "blockId": "bmaKTUXkT2cc3wtKfK7ra71",
"type": "email input", "type": "email input",
"target": { "blockId": "bcyiT7P6E99YnHKnpxs4Yux" }, "options": { "variableId": "oexLr4sJQNVdSnYCGgGRB3" },
"options": { "variableId": "8H3aQsNji2Gyfpp3RPozNN" } "edgeId": "6Tax9rw7L8kmRn9JRD2Mrg"
},
"siPoEE9H27hVHqykth3a7Kj": {
"id": "siPoEE9H27hVHqykth3a7Kj",
"blockId": "bnt8fM5Wgc8gBg2iSmUcfJu",
"type": "Google Sheets"
} }
}, },
"allIds": ["uDMB7a2ucg17WGvbQJjeRn", "step1", "sj72oDhJEe4N92KTt64GKWs"] "allIds": [
"1NdXPCiRicqDA8k4JfnXfi",
"spHxPWbSqkVZW9gqH86ovC5",
"siPoEE9H27hVHqykth3a7Kj"
]
}, },
"choiceItems": { "byId": {}, "allIds": [] }, "choiceItems": { "byId": {}, "allIds": [] },
"variables": { "variables": {
"byId": { "byId": {
"8H3aQsNji2Gyfpp3RPozNN": { "oexLr4sJQNVdSnYCGgGRB3": {
"id": "8H3aQsNji2Gyfpp3RPozNN", "id": "oexLr4sJQNVdSnYCGgGRB3",
"name": "Email" "name": "Email"
} }
}, },
"allIds": ["8H3aQsNji2Gyfpp3RPozNN"] "allIds": ["oexLr4sJQNVdSnYCGgGRB3"]
},
"edges": {
"byId": {
"benDCcLMUWNvi6Fg6CXE9H": {
"from": {
"blockId": "j24wz82YG3rjXMgrmCiTLy",
"stepId": "1NdXPCiRicqDA8k4JfnXfi"
},
"to": { "blockId": "bmaKTUXkT2cc3wtKfK7ra71" },
"id": "benDCcLMUWNvi6Fg6CXE9H"
},
"6Tax9rw7L8kmRn9JRD2Mrg": {
"from": {
"blockId": "bmaKTUXkT2cc3wtKfK7ra71",
"stepId": "spHxPWbSqkVZW9gqH86ovC5"
},
"to": { "blockId": "bnt8fM5Wgc8gBg2iSmUcfJu" },
"id": "6Tax9rw7L8kmRn9JRD2Mrg"
}
},
"allIds": ["benDCcLMUWNvi6Fg6CXE9H", "6Tax9rw7L8kmRn9JRD2Mrg"]
}, },
"theme": { "theme": {
"general": { "general": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -1,70 +1,70 @@
{ {
"id": "typebot4", "id": "ckyltevlb0559z31an8cmkyrp",
"createdAt": "2022-01-17T14:37:01.826Z", "createdAt": "2022-01-19T17:24:34.031Z",
"updatedAt": "2022-01-17T14:37:01.826Z", "updatedAt": "2022-01-19T17:24:34.031Z",
"name": "My typebot", "name": "My typebot",
"ownerId": "user2", "ownerId": "ckyltekzq0533z31ad8opmacz",
"publishedTypebotId": null, "publishedTypebotId": null,
"folderId": null, "folderId": null,
"blocks": { "blocks": {
"byId": { "byId": {
"bec5A5bLwenmZpCJc8FRaM": { "kPupUcEn7TcBGKHUpgK2Q5": {
"id": "bec5A5bLwenmZpCJc8FRaM", "id": "kPupUcEn7TcBGKHUpgK2Q5",
"title": "Start", "title": "Start",
"stepIds": ["uDMB7a2ucg17WGvbQJjeRn"], "stepIds": ["nP5oWm7PxigMupyWpPLq24"],
"graphCoordinates": { "x": 0, "y": 0 } "graphCoordinates": { "x": 0, "y": 0 }
}, },
"bcyiT7P6E99YnHKnpxs4Yux": { "bi4J5fv9DFn1zPSqGf8eRht": {
"id": "bcyiT7P6E99YnHKnpxs4Yux", "id": "bi4J5fv9DFn1zPSqGf8eRht",
"title": "Block #2", "title": "Block #2",
"stepIds": ["step1"], "graphCoordinates": { "x": 104, "y": 201 },
"graphCoordinates": { "x": 411, "y": 108 } "stepIds": ["scH9qdXwFfAScoavj6UzQNT"]
}, },
"bgpNxHtBBXWrP1QMe2A8hZ9": { "bwqGhUsa2SKaxXSrKtapVc8": {
"id": "bgpNxHtBBXWrP1QMe2A8hZ9", "id": "bwqGhUsa2SKaxXSrKtapVc8",
"title": "Block #3", "title": "Block #3",
"stepIds": ["sj72oDhJEe4N92KTt64GKWs"], "graphCoordinates": { "x": 458, "y": 292 },
"graphCoordinates": { "x": 1, "y": 236 } "stepIds": ["shZdc8Qw48domEbS7vLW5eN"]
}, },
"buwMV9tx2EFcMbRkNTELt3J": { "bqmgS9hLUu2RA2oxVv7hMka": {
"id": "buwMV9tx2EFcMbRkNTELt3J", "id": "bqmgS9hLUu2RA2oxVv7hMka",
"title": "Block #4", "title": "Block #4",
"graphCoordinates": { "x": 441, "y": 330 }, "graphCoordinates": { "x": 102, "y": 432 },
"stepIds": ["s2R2bk7qfSRVgTyRmPhVw7p"] "stepIds": ["s4z6G3MGAyhXChU9jakQWer"]
} }
}, },
"allIds": [ "allIds": [
"bec5A5bLwenmZpCJc8FRaM", "kPupUcEn7TcBGKHUpgK2Q5",
"bcyiT7P6E99YnHKnpxs4Yux", "bi4J5fv9DFn1zPSqGf8eRht",
"bgpNxHtBBXWrP1QMe2A8hZ9", "bwqGhUsa2SKaxXSrKtapVc8",
"buwMV9tx2EFcMbRkNTELt3J" "bqmgS9hLUu2RA2oxVv7hMka"
] ]
}, },
"steps": { "steps": {
"byId": { "byId": {
"step1": { "nP5oWm7PxigMupyWpPLq24": {
"id": "step1", "id": "nP5oWm7PxigMupyWpPLq24",
"type": "Google Sheets",
"blockId": "bcyiT7P6E99YnHKnpxs4Yux",
"target": { "blockId": "buwMV9tx2EFcMbRkNTELt3J" }
},
"uDMB7a2ucg17WGvbQJjeRn": {
"id": "uDMB7a2ucg17WGvbQJjeRn",
"type": "start", "type": "start",
"label": "Start", "label": "Start",
"target": { "blockId": "bgpNxHtBBXWrP1QMe2A8hZ9" }, "blockId": "kPupUcEn7TcBGKHUpgK2Q5",
"blockId": "bec5A5bLwenmZpCJc8FRaM" "edgeId": "kCLXGLpiM2F6pn4wFYc1f5"
}, },
"sj72oDhJEe4N92KTt64GKWs": { "scH9qdXwFfAScoavj6UzQNT": {
"id": "sj72oDhJEe4N92KTt64GKWs", "id": "scH9qdXwFfAScoavj6UzQNT",
"blockId": "bi4J5fv9DFn1zPSqGf8eRht",
"type": "email input", "type": "email input",
"target": { "blockId": "bcyiT7P6E99YnHKnpxs4Yux" }, "options": { "variableId": "ifXp66N1meAtoUDcbqWxuD" },
"blockId": "bgpNxHtBBXWrP1QMe2A8hZ9", "edgeId": "7Czn5hJFUfpkRGtwGnKxtt"
"options": { "variableId": "8H3aQsNji2Gyfpp3RPozNN" }
}, },
"s2R2bk7qfSRVgTyRmPhVw7p": { "shZdc8Qw48domEbS7vLW5eN": {
"id": "s2R2bk7qfSRVgTyRmPhVw7p", "id": "shZdc8Qw48domEbS7vLW5eN",
"blockId": "buwMV9tx2EFcMbRkNTELt3J", "blockId": "bwqGhUsa2SKaxXSrKtapVc8",
"type": "Google Sheets",
"edgeId": "eMhGokwHMDRDrynSvpiRje"
},
"s4z6G3MGAyhXChU9jakQWer": {
"id": "s4z6G3MGAyhXChU9jakQWer",
"blockId": "bqmgS9hLUu2RA2oxVv7hMka",
"type": "text", "type": "text",
"content": { "content": {
"html": "<div>Your name is: {{First name}} {{Last name}}</div>", "html": "<div>Your name is: {{First name}} {{Last name}}</div>",
@@ -81,21 +81,54 @@
} }
}, },
"allIds": [ "allIds": [
"uDMB7a2ucg17WGvbQJjeRn", "nP5oWm7PxigMupyWpPLq24",
"step1", "scH9qdXwFfAScoavj6UzQNT",
"sj72oDhJEe4N92KTt64GKWs", "shZdc8Qw48domEbS7vLW5eN",
"s2R2bk7qfSRVgTyRmPhVw7p" "s4z6G3MGAyhXChU9jakQWer"
] ]
}, },
"choiceItems": { "byId": {}, "allIds": [] }, "choiceItems": { "byId": {}, "allIds": [] },
"variables": { "variables": {
"byId": { "byId": {
"8H3aQsNji2Gyfpp3RPozNN": { "ifXp66N1meAtoUDcbqWxuD": {
"id": "8H3aQsNji2Gyfpp3RPozNN", "id": "ifXp66N1meAtoUDcbqWxuD",
"name": "Email" "name": "Email"
} }
}, },
"allIds": ["8H3aQsNji2Gyfpp3RPozNN"] "allIds": ["ifXp66N1meAtoUDcbqWxuD"]
},
"edges": {
"byId": {
"kCLXGLpiM2F6pn4wFYc1f5": {
"from": {
"blockId": "kPupUcEn7TcBGKHUpgK2Q5",
"stepId": "nP5oWm7PxigMupyWpPLq24"
},
"to": { "blockId": "bi4J5fv9DFn1zPSqGf8eRht" },
"id": "kCLXGLpiM2F6pn4wFYc1f5"
},
"7Czn5hJFUfpkRGtwGnKxtt": {
"from": {
"blockId": "bi4J5fv9DFn1zPSqGf8eRht",
"stepId": "scH9qdXwFfAScoavj6UzQNT"
},
"to": { "blockId": "bwqGhUsa2SKaxXSrKtapVc8" },
"id": "7Czn5hJFUfpkRGtwGnKxtt"
},
"eMhGokwHMDRDrynSvpiRje": {
"from": {
"blockId": "bwqGhUsa2SKaxXSrKtapVc8",
"stepId": "shZdc8Qw48domEbS7vLW5eN"
},
"to": { "blockId": "bqmgS9hLUu2RA2oxVv7hMka" },
"id": "eMhGokwHMDRDrynSvpiRje"
}
},
"allIds": [
"kCLXGLpiM2F6pn4wFYc1f5",
"7Czn5hJFUfpkRGtwGnKxtt",
"eMhGokwHMDRDrynSvpiRje"
]
}, },
"theme": { "theme": {
"general": { "general": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -1,110 +1,71 @@
{ {
"id": "typebot4", "id": "ckylsd52p0114z31aobllswmu",
"createdAt": "2022-01-14T14:23:36.576Z", "createdAt": "2022-01-19T16:55:13.393Z",
"updatedAt": "2022-01-14T14:23:36.576Z", "updatedAt": "2022-01-19T16:55:13.393Z",
"name": "My typebot", "name": "My typebot",
"ownerId": "ckye5hs3e1801em1a0eodjj3f", "ownerId": "ckylsbdf60088z31ayqytest6",
"publishedTypebotId": null, "publishedTypebotId": null,
"folderId": null, "folderId": null,
"blocks": { "blocks": {
"byId": { "byId": {
"1YV9MuAa6dd6eNxC5BipDZ": { "2x83WHtEBkiv7pk7KgqJwZ": {
"id": "1YV9MuAa6dd6eNxC5BipDZ", "id": "2x83WHtEBkiv7pk7KgqJwZ",
"title": "Start", "title": "Start",
"stepIds": ["3EzaqYRLFqFQFbCj2gQP3q"], "stepIds": ["1A76iZBgXG7hvkG2koCxe4"],
"graphCoordinates": { "x": 0, "y": 0 } "graphCoordinates": { "x": 0, "y": 0 }
}, },
"bnmeD9SVeGPhF4qvwKfxE8R": { "bwga7RwqQWbowdHph27DM1N": {
"id": "bnmeD9SVeGPhF4qvwKfxE8R", "id": "bwga7RwqQWbowdHph27DM1N",
"title": "Block #2", "title": "Block #2",
"stepIds": ["condition1", "condition2"], "graphCoordinates": { "x": 78, "y": 224 },
"graphCoordinates": { "x": 194, "y": 228 } "stepIds": ["srwUKaUFFmehppJ2ZDqp4xG", "sxvzuo48GHi3AcAfmiFyYC1"]
}, },
"b3EaF53FGbQH5MhbBPUjDjb": { "bu8whx817bJBG37FQrtD5dD": {
"id": "b3EaF53FGbQH5MhbBPUjDjb", "id": "bu8whx817bJBG37FQrtD5dD",
"title": "Block #3",
"graphCoordinates": { "x": 430, "y": 287 },
"stepIds": ["ituVWW1AvQeVdFHTwsiVao", "5SLc4whZooZVUfr1bmTNSC"]
},
"b59jwmEdwZUvJszV394x44u": {
"id": "b59jwmEdwZUvJszV394x44u",
"title": "Block #4", "title": "Block #4",
"graphCoordinates": { "x": 375, "y": -39 }, "graphCoordinates": { "x": 844, "y": 185 },
"stepIds": ["siBqadjM6AJXf25Ct4413dM", "sqTSo2heZ5vdfzJjNZfYUK5"] "stepIds": ["sm1YcKTL9cQMCGywzo1wyBB"]
}, },
"b2Wjyg4MsqB5xhQYQPbPYkc": { "baVF9HqhuSnLDZqY9eRPpcp": {
"id": "b2Wjyg4MsqB5xhQYQPbPYkc", "id": "baVF9HqhuSnLDZqY9eRPpcp",
"title": "Block #4",
"graphCoordinates": { "x": 712, "y": 186 },
"stepIds": ["srFH8bxpJZuShgr2hmz4uhx"]
},
"b4KB24EywCVt3zX3opqMvYS": {
"id": "b4KB24EywCVt3zX3opqMvYS",
"title": "Block #5", "title": "Block #5",
"graphCoordinates": { "x": 700, "y": 340 }, "graphCoordinates": { "x": 841, "y": 356 },
"stepIds": ["ssBZF1FgMDYZJTbmTzb8Uks"] "stepIds": ["sb3o6J8Fybg6u8KuayKviJq"]
}, },
"bppe1zzyayc8ub14ozqJEXb": { "b9aEH46RHuZWTdQwZJ6KBWR": {
"id": "bppe1zzyayc8ub14ozqJEXb", "id": "b9aEH46RHuZWTdQwZJ6KBWR",
"title": "Block #6", "title": "Block #6",
"graphCoordinates": { "x": 713, "y": 491 }, "graphCoordinates": { "x": 839, "y": 523 },
"stepIds": ["scQmWL2qGp1oXnEYdqjLcDv"] "stepIds": ["scKogEJSTq4kPeHRhwTTjit"]
} }
}, },
"allIds": [ "allIds": [
"1YV9MuAa6dd6eNxC5BipDZ", "2x83WHtEBkiv7pk7KgqJwZ",
"bnmeD9SVeGPhF4qvwKfxE8R", "bwga7RwqQWbowdHph27DM1N",
"b3EaF53FGbQH5MhbBPUjDjb", "bu8whx817bJBG37FQrtD5dD",
"b2Wjyg4MsqB5xhQYQPbPYkc", "b59jwmEdwZUvJszV394x44u",
"b4KB24EywCVt3zX3opqMvYS", "baVF9HqhuSnLDZqY9eRPpcp",
"bppe1zzyayc8ub14ozqJEXb" "b9aEH46RHuZWTdQwZJ6KBWR"
] ]
}, },
"steps": { "steps": {
"byId": { "byId": {
"3EzaqYRLFqFQFbCj2gQP3q": { "1A76iZBgXG7hvkG2koCxe4": {
"id": "3EzaqYRLFqFQFbCj2gQP3q", "id": "1A76iZBgXG7hvkG2koCxe4",
"type": "start", "type": "start",
"label": "Start", "label": "Start",
"target": { "blockId": "b3EaF53FGbQH5MhbBPUjDjb" }, "blockId": "2x83WHtEBkiv7pk7KgqJwZ",
"blockId": "1YV9MuAa6dd6eNxC5BipDZ" "edgeId": "jjNy2hYgrQgPS9EBMKA7MH"
}, },
"condition1": { "srwUKaUFFmehppJ2ZDqp4xG": {
"id": "condition1", "id": "srwUKaUFFmehppJ2ZDqp4xG",
"type": "Condition", "blockId": "bwga7RwqQWbowdHph27DM1N",
"blockId": "bnmeD9SVeGPhF4qvwKfxE8R",
"trueTarget": { "blockId": "b2Wjyg4MsqB5xhQYQPbPYkc" },
"options": {
"comparisons": {
"byId": { "comparison1": { "comparisonOperator": "Equal to" } },
"allIds": ["comparison1"]
},
"logicalOperator": "AND"
}
},
"condition2": {
"id": "condition2",
"blockId": "bnmeD9SVeGPhF4qvwKfxE8R",
"type": "Condition",
"trueTarget": { "blockId": "b4KB24EywCVt3zX3opqMvYS" },
"falseTarget": { "blockId": "bppe1zzyayc8ub14ozqJEXb" },
"options": {
"comparisons": {
"byId": { "comparison1": { "comparisonOperator": "Equal to" } },
"allIds": ["comparison1"]
},
"logicalOperator": "AND"
}
},
"sqTSo2heZ5vdfzJjNZfYUK5": {
"id": "sqTSo2heZ5vdfzJjNZfYUK5",
"blockId": "b3EaF53FGbQH5MhbBPUjDjb",
"type": "number input",
"options": {
"variableId": "icVxLRv1sQnPyNwRX9cjK9",
"min": 0,
"max": 150,
"labels": { "placeholder": "Type your age..." }
},
"target": { "blockId": "bnmeD9SVeGPhF4qvwKfxE8R" }
},
"siBqadjM6AJXf25Ct4413dM": {
"id": "siBqadjM6AJXf25Ct4413dM",
"blockId": "b3EaF53FGbQH5MhbBPUjDjb",
"type": "text", "type": "text",
"content": { "content": {
"html": "<div>How old are you?</div>", "html": "<div>How old are you?</div>",
@@ -114,39 +75,78 @@
"plainText": "How old are you?" "plainText": "How old are you?"
} }
}, },
"srFH8bxpJZuShgr2hmz4uhx": { "sxvzuo48GHi3AcAfmiFyYC1": {
"id": "srFH8bxpJZuShgr2hmz4uhx", "id": "sxvzuo48GHi3AcAfmiFyYC1",
"blockId": "b2Wjyg4MsqB5xhQYQPbPYkc", "blockId": "bwga7RwqQWbowdHph27DM1N",
"type": "number input",
"options": { "variableId": "dEz689uVm8AxUM8TrbQd2t" },
"edgeId": "7mcWaWohM9zGtLX8ZSnqFy"
},
"ituVWW1AvQeVdFHTwsiVao": {
"id": "ituVWW1AvQeVdFHTwsiVao",
"blockId": "bu8whx817bJBG37FQrtD5dD",
"type": "Condition",
"options": {
"comparisons": {
"byId": {
"ituVWW1AvQeVdFHTwsiVao": {
"id": "ituVWW1AvQeVdFHTwsiVao",
"comparisonOperator": "Equal to",
"variableId": "ixdebNyG6gYgmowpkioQtG"
}
},
"allIds": ["ituVWW1AvQeVdFHTwsiVao"]
},
"logicalOperator": "AND"
},
"trueEdgeId": "iBPsFyBsPv6Rbdfo2QdJyi"
},
"5SLc4whZooZVUfr1bmTNSC": {
"id": "5SLc4whZooZVUfr1bmTNSC",
"blockId": "bu8whx817bJBG37FQrtD5dD",
"type": "Condition",
"options": {
"comparisons": {
"byId": {
"5SLc4whZooZVUfr1bmTNSC": {
"id": "5SLc4whZooZVUfr1bmTNSC",
"comparisonOperator": "Equal to"
}
},
"allIds": ["5SLc4whZooZVUfr1bmTNSC"]
},
"logicalOperator": "AND"
},
"trueEdgeId": "354PJ2jD5U3J2APqLsPJrp",
"falseEdgeId": "94bmeCLigEUUpWYw2xsAVB"
},
"sm1YcKTL9cQMCGywzo1wyBB": {
"id": "sm1YcKTL9cQMCGywzo1wyBB",
"blockId": "b59jwmEdwZUvJszV394x44u",
"type": "text", "type": "text",
"content": { "content": {
"html": "<div>Wow you are older than 80</div>", "html": "<div>You are older than 80</div>",
"richText": [ "richText": [
{ { "type": "p", "children": [{ "text": "You are older than 80" }] }
"type": "p",
"children": [{ "text": "Wow you are older than 80" }]
}
], ],
"plainText": "Wow you are older than 80" "plainText": "You are older than 80"
} }
}, },
"ssBZF1FgMDYZJTbmTzb8Uks": { "sb3o6J8Fybg6u8KuayKviJq": {
"id": "ssBZF1FgMDYZJTbmTzb8Uks", "id": "sb3o6J8Fybg6u8KuayKviJq",
"blockId": "b4KB24EywCVt3zX3opqMvYS", "blockId": "baVF9HqhuSnLDZqY9eRPpcp",
"type": "text", "type": "text",
"content": { "content": {
"html": "<div>Wow you are older than 20</div>", "html": "<div>You are older than 20</div>",
"richText": [ "richText": [
{ { "type": "p", "children": [{ "text": "You are older than 20" }] }
"type": "p",
"children": [{ "text": "Wow you are older than 20" }]
}
], ],
"plainText": "Wow you are older than 20" "plainText": "You are older than 20"
} }
}, },
"scQmWL2qGp1oXnEYdqjLcDv": { "scKogEJSTq4kPeHRhwTTjit": {
"id": "scQmWL2qGp1oXnEYdqjLcDv", "id": "scKogEJSTq4kPeHRhwTTjit",
"blockId": "bppe1zzyayc8ub14ozqJEXb", "blockId": "b9aEH46RHuZWTdQwZJ6KBWR",
"type": "text", "type": "text",
"content": { "content": {
"html": "<div>You are younger than 20</div>", "html": "<div>You are younger than 20</div>",
@@ -158,25 +158,80 @@
} }
}, },
"allIds": [ "allIds": [
"3EzaqYRLFqFQFbCj2gQP3q", "1A76iZBgXG7hvkG2koCxe4",
"condition1", "srwUKaUFFmehppJ2ZDqp4xG",
"condition2", "sxvzuo48GHi3AcAfmiFyYC1",
"sqTSo2heZ5vdfzJjNZfYUK5", "ituVWW1AvQeVdFHTwsiVao",
"siBqadjM6AJXf25Ct4413dM", "5SLc4whZooZVUfr1bmTNSC",
"srFH8bxpJZuShgr2hmz4uhx", "sxvzuo48GHi3AcAfmiFyYC1",
"ssBZF1FgMDYZJTbmTzb8Uks", "sm1YcKTL9cQMCGywzo1wyBB",
"scQmWL2qGp1oXnEYdqjLcDv" "sb3o6J8Fybg6u8KuayKviJq",
"scKogEJSTq4kPeHRhwTTjit"
] ]
}, },
"choiceItems": { "byId": {}, "allIds": [] }, "choiceItems": { "byId": {}, "allIds": [] },
"variables": { "variables": {
"byId": { "byId": {
"icVxLRv1sQnPyNwRX9cjK9": { "dEz689uVm8AxUM8TrbQd2t": {
"id": "icVxLRv1sQnPyNwRX9cjK9", "id": "dEz689uVm8AxUM8TrbQd2t",
"name": "Age" "name": "Age"
} }
}, },
"allIds": ["icVxLRv1sQnPyNwRX9cjK9"] "allIds": ["dEz689uVm8AxUM8TrbQd2t"]
},
"edges": {
"byId": {
"jjNy2hYgrQgPS9EBMKA7MH": {
"from": {
"blockId": "2x83WHtEBkiv7pk7KgqJwZ",
"stepId": "1A76iZBgXG7hvkG2koCxe4"
},
"to": { "blockId": "bwga7RwqQWbowdHph27DM1N" },
"id": "jjNy2hYgrQgPS9EBMKA7MH"
},
"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"
}
},
"allIds": [
"jjNy2hYgrQgPS9EBMKA7MH",
"iBPsFyBsPv6Rbdfo2QdJyi",
"354PJ2jD5U3J2APqLsPJrp",
"94bmeCLigEUUpWYw2xsAVB",
"7mcWaWohM9zGtLX8ZSnqFy"
]
}, },
"theme": { "theme": {
"general": { "general": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -1,121 +1,144 @@
{ {
"id": "typebot4", "id": "ckylrr3qh0030fn1a3nszzxiu",
"createdAt": "2022-01-13T08:10:06.705Z", "createdAt": "2022-01-19T16:38:05.225Z",
"updatedAt": "2022-01-13T08:10:06.705Z", "updatedAt": "2022-01-19T16:38:05.225Z",
"name": "My typebot", "name": "My typebot",
"ownerId": "ckybcurfh1612li1a62gqojvj", "ownerId": "ckylrpsmt0006fn1ah956d0z1",
"publishedTypebotId": null, "publishedTypebotId": null,
"folderId": null, "folderId": null,
"blocks": { "blocks": {
"byId": { "byId": {
"eXpkU5dMsjRPeY3WverF45": { "kmUzhRFzSKjkaipYNcku9S": {
"id": "eXpkU5dMsjRPeY3WverF45", "id": "kmUzhRFzSKjkaipYNcku9S",
"title": "Start", "title": "Start",
"stepIds": ["vWwzs6EmChn2PJcJQMr2gT"], "stepIds": ["6XgP3JoCh7Y4M8GCX9DKym"],
"graphCoordinates": { "x": 0, "y": 0 } "graphCoordinates": { "x": 0, "y": 0 }
}, },
"b87iNgPqGvKrybzHcTD4Sze": { "bwWRAaX5m6NZyZ9jjpXmWSb": {
"id": "b87iNgPqGvKrybzHcTD4Sze", "id": "bwWRAaX5m6NZyZ9jjpXmWSb",
"title": "Block #4",
"stepIds": ["s8a2ASVaM3PoaD3y9amHrtT", "sc7FNu4BUCmmhmP14hk2Hij"],
"graphCoordinates": { "x": 584, "y": 389 }
},
"bg22czQixaBBY8M8LJ9wEm1": {
"id": "bg22czQixaBBY8M8LJ9wEm1",
"title": "Block #3",
"stepIds": ["set-var-1", "set-var-2"],
"graphCoordinates": { "x": 581, "y": 97 }
},
"bvGtcdRjZtbbaimKyUD3NAW": {
"id": "bvGtcdRjZtbbaimKyUD3NAW",
"title": "Block #2", "title": "Block #2",
"stepIds": ["sgFoKA5Y4MXBkqFzCGCWGeg", "number-step"], "graphCoordinates": { "x": -21, "y": 221 },
"graphCoordinates": { "x": 190, "y": 222 } "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"]
} }
}, },
"allIds": [ "allIds": [
"eXpkU5dMsjRPeY3WverF45", "kmUzhRFzSKjkaipYNcku9S",
"bvGtcdRjZtbbaimKyUD3NAW", "bwWRAaX5m6NZyZ9jjpXmWSb",
"bg22czQixaBBY8M8LJ9wEm1", "baUyUnNBxZzPe1z5PqE4NkD",
"b87iNgPqGvKrybzHcTD4Sze" "bwkKNpJmAFCCLbZSnPnnLnR"
] ]
}, },
"steps": { "steps": {
"byId": { "byId": {
"vWwzs6EmChn2PJcJQMr2gT": { "6XgP3JoCh7Y4M8GCX9DKym": {
"id": "vWwzs6EmChn2PJcJQMr2gT", "id": "6XgP3JoCh7Y4M8GCX9DKym",
"type": "start", "type": "start",
"label": "Start", "label": "Start",
"target": { "blockId": "bvGtcdRjZtbbaimKyUD3NAW" }, "blockId": "kmUzhRFzSKjkaipYNcku9S",
"blockId": "eXpkU5dMsjRPeY3WverF45" "edgeId": "ahfJ4fUuvxX2dcBMk876tf"
}, },
"set-var-2": { "s8n3nJajsBaYqrFeRYVvcf6": {
"id": "set-var-2", "id": "s8n3nJajsBaYqrFeRYVvcf6",
"type": "Set variable", "blockId": "bwWRAaX5m6NZyZ9jjpXmWSb",
"target": { "blockId": "b87iNgPqGvKrybzHcTD4Sze" },
"blockId": "bg22czQixaBBY8M8LJ9wEm1"
},
"s8a2ASVaM3PoaD3y9amHrtT": {
"id": "s8a2ASVaM3PoaD3y9amHrtT",
"type": "text",
"blockId": "b87iNgPqGvKrybzHcTD4Sze",
"content": {
"html": "<div>Total: {{Total}}</div>",
"richText": [
{ "type": "p", "children": [{ "text": "Total: {{Total}}" }] }
],
"plainText": "Total: {{Total}}"
}
},
"sgFoKA5Y4MXBkqFzCGCWGeg": {
"id": "sgFoKA5Y4MXBkqFzCGCWGeg",
"type": "text",
"blockId": "bvGtcdRjZtbbaimKyUD3NAW",
"content": {
"html": "<div>1000 * ?</div>",
"richText": [{ "type": "p", "children": [{ "text": "1000 * ?" }] }],
"plainText": "1000 * ?"
}
},
"number-step": {
"id": "number-step",
"type": "number input", "type": "number input",
"target": { "blockId": "bg22czQixaBBY8M8LJ9wEm1" }, "edgeId": "dcJedLC7qsLtsmm1wbiFFc"
"blockId": "bvGtcdRjZtbbaimKyUD3NAW"
}, },
"set-var-1": { "sqMVMXeRYp4inLcRqej2Wac": {
"id": "set-var-1", "id": "sqMVMXeRYp4inLcRqej2Wac",
"blockId": "bg22czQixaBBY8M8LJ9wEm1", "blockId": "bwWRAaX5m6NZyZ9jjpXmWSb",
"type": "text",
"content": {
"html": "<div>How old are you?</div>",
"richText": [
{ "type": "p", "children": [{ "text": "How old are you?" }] }
],
"plainText": "How old are you?"
}
},
"shfL5ueQDuj2RPcJPWZGArT": {
"id": "shfL5ueQDuj2RPcJPWZGArT",
"blockId": "baUyUnNBxZzPe1z5PqE4NkD",
"type": "Set variable" "type": "Set variable"
}, },
"sc7FNu4BUCmmhmP14hk2Hij": { "sugJ6xN3jFys1CjWfsxGhiJ": {
"id": "sc7FNu4BUCmmhmP14hk2Hij", "id": "sugJ6xN3jFys1CjWfsxGhiJ",
"blockId": "b87iNgPqGvKrybzHcTD4Sze", "blockId": "baUyUnNBxZzPe1z5PqE4NkD",
"type": "Set variable",
"edgeId": "sA5gvCVVBVYdGsdeSGF5ei"
},
"shR7ae3iNEvB6arCSu7wVFF": {
"id": "shR7ae3iNEvB6arCSu7wVFF",
"blockId": "bwkKNpJmAFCCLbZSnPnnLnR",
"type": "text", "type": "text",
"content": { "content": {
"html": "<div>Custom var: {{Custom var}}</div>", "html": "<div>Total: {{Total}}</div><div>Custom var: {{Custom var}}</div>",
"richText": [ "richText": [
{ "type": "p", "children": [{ "text": "Total: {{Total}}" }] },
{ {
"type": "p", "type": "p",
"children": [{ "text": "Custom var: {{Custom var}}" }] "children": [{ "text": "Custom var: {{Custom var}}" }]
} }
], ],
"plainText": "Custom var: {{Custom var}}" "plainText": "Total: {{Total}}Custom var: {{Custom var}}"
} }
} }
}, },
"allIds": [ "allIds": [
"vWwzs6EmChn2PJcJQMr2gT", "6XgP3JoCh7Y4M8GCX9DKym",
"s8a2ASVaM3PoaD3y9amHrtT", "s8n3nJajsBaYqrFeRYVvcf6",
"set-var-2", "sqMVMXeRYp4inLcRqej2Wac",
"sgFoKA5Y4MXBkqFzCGCWGeg", "shfL5ueQDuj2RPcJPWZGArT",
"number-step", "sugJ6xN3jFys1CjWfsxGhiJ",
"set-var-1", "shR7ae3iNEvB6arCSu7wVFF"
"sc7FNu4BUCmmhmP14hk2Hij"
] ]
}, },
"choiceItems": { "byId": {}, "allIds": [] }, "choiceItems": { "byId": {}, "allIds": [] },
"variables": { "byId": {}, "allIds": [] }, "variables": { "byId": {}, "allIds": [] },
"edges": {
"byId": {
"ahfJ4fUuvxX2dcBMk876tf": {
"from": {
"blockId": "kmUzhRFzSKjkaipYNcku9S",
"stepId": "6XgP3JoCh7Y4M8GCX9DKym"
},
"to": { "blockId": "bwWRAaX5m6NZyZ9jjpXmWSb" },
"id": "ahfJ4fUuvxX2dcBMk876tf"
},
"dcJedLC7qsLtsmm1wbiFFc": {
"from": {
"blockId": "bwWRAaX5m6NZyZ9jjpXmWSb",
"stepId": "s8n3nJajsBaYqrFeRYVvcf6"
},
"to": { "blockId": "baUyUnNBxZzPe1z5PqE4NkD" },
"id": "dcJedLC7qsLtsmm1wbiFFc"
},
"sA5gvCVVBVYdGsdeSGF5ei": {
"from": {
"blockId": "baUyUnNBxZzPe1z5PqE4NkD",
"stepId": "sugJ6xN3jFys1CjWfsxGhiJ"
},
"to": { "blockId": "bwkKNpJmAFCCLbZSnPnnLnR" },
"id": "sA5gvCVVBVYdGsdeSGF5ei"
}
},
"allIds": [
"ahfJ4fUuvxX2dcBMk876tf",
"dcJedLC7qsLtsmm1wbiFFc",
"sA5gvCVVBVYdGsdeSGF5ei"
]
},
"theme": { "theme": {
"general": { "general": {
"font": "Open Sans", "font": "Open Sans",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -1,70 +1,70 @@
{ {
"id": "typebot4", "id": "ckylsr69q0240z31afjhedyxo",
"createdAt": "2022-01-12T08:20:11.572Z", "createdAt": "2022-01-19T17:06:08.126Z",
"updatedAt": "2022-01-12T08:20:11.572Z", "updatedAt": "2022-01-19T17:06:08.126Z",
"name": "My typebot", "name": "My typebot",
"ownerId": "ckyb9vs110792li1az8bve32o", "ownerId": "ckylsr4fi0220z31apbinpy9d",
"publishedTypebotId": null, "publishedTypebotId": null,
"folderId": null, "folderId": null,
"blocks": { "blocks": {
"byId": { "byId": {
"iE81k3ViAne3mfPEWeZJcq": { "weeBMMXxNKwEonMfDX8Z5k": {
"id": "iE81k3ViAne3mfPEWeZJcq", "id": "weeBMMXxNKwEonMfDX8Z5k",
"title": "Start", "title": "Start",
"stepIds": ["fLNNAAtRkrw7GG5iqCCdYx"], "stepIds": ["nEXiHesKXRQJhQbaWfbDVH"],
"graphCoordinates": { "x": 0, "y": 0 } "graphCoordinates": { "x": 0, "y": 0 }
}, },
"bcs6TWbHd9inLRDyGZaX7SD": { "bg2MBdkf6y7g6WsbqAP3eAT": {
"id": "bcs6TWbHd9inLRDyGZaX7SD", "id": "bg2MBdkf6y7g6WsbqAP3eAT",
"title": "Block #2", "title": "Block #2",
"graphCoordinates": { "x": 362, "y": 202 }, "graphCoordinates": { "x": 120, "y": 221 },
"stepIds": ["sdExhY2LZ6VZ69s2WFmBcZg"] "stepIds": ["sqzMjp1Ba4jTL3A6iJehC6C"]
}, },
"bgjJd4KTrHSy2GD7AfCPECR": { "bj5BE1yKPzFFhvRk6cMnmsQ": {
"id": "bgjJd4KTrHSy2GD7AfCPECR", "id": "bj5BE1yKPzFFhvRk6cMnmsQ",
"title": "Block #3", "title": "Block #3",
"graphCoordinates": { "x": 846, "y": 186 }, "graphCoordinates": { "x": 529, "y": 130 },
"stepIds": ["sgwvBXuNbZ16vvJK7rAA8Gw"] "stepIds": ["s8zPdEj96z8EoJG2zBqgoE8"]
}, },
"bkLYB73fk4GePgqDLnUHy1t": { "bdET8zLFQbwpTaAmi4wmezE": {
"id": "bkLYB73fk4GePgqDLnUHy1t", "id": "bdET8zLFQbwpTaAmi4wmezE",
"title": "Block #4", "title": "Block #4",
"graphCoordinates": { "x": 851, "y": 498 }, "graphCoordinates": { "x": 538, "y": 386 },
"stepIds": ["sbE2QKjYNXwrBexFgYrkMcn"] "stepIds": ["sjZ28izS5e3VjNynFKT2F7E"]
} }
}, },
"allIds": [ "allIds": [
"iE81k3ViAne3mfPEWeZJcq", "weeBMMXxNKwEonMfDX8Z5k",
"bcs6TWbHd9inLRDyGZaX7SD", "bg2MBdkf6y7g6WsbqAP3eAT",
"bgjJd4KTrHSy2GD7AfCPECR", "bj5BE1yKPzFFhvRk6cMnmsQ",
"bkLYB73fk4GePgqDLnUHy1t" "bdET8zLFQbwpTaAmi4wmezE"
] ]
}, },
"steps": { "steps": {
"byId": { "byId": {
"fLNNAAtRkrw7GG5iqCCdYx": { "nEXiHesKXRQJhQbaWfbDVH": {
"id": "fLNNAAtRkrw7GG5iqCCdYx", "id": "nEXiHesKXRQJhQbaWfbDVH",
"type": "start", "type": "start",
"label": "Start", "label": "Start",
"blockId": "iE81k3ViAne3mfPEWeZJcq", "blockId": "weeBMMXxNKwEonMfDX8Z5k",
"target": { "blockId": "bcs6TWbHd9inLRDyGZaX7SD" } "edgeId": "uh95dDpiiZdYxpPFsUqZEg"
}, },
"sdExhY2LZ6VZ69s2WFmBcZg": { "sqzMjp1Ba4jTL3A6iJehC6C": {
"id": "sdExhY2LZ6VZ69s2WFmBcZg", "id": "sqzMjp1Ba4jTL3A6iJehC6C",
"blockId": "bcs6TWbHd9inLRDyGZaX7SD", "blockId": "bg2MBdkf6y7g6WsbqAP3eAT",
"type": "choice input", "type": "choice input",
"options": { "options": {
"itemIds": [ "itemIds": [
"rFcixXR7vVXDeUuWajc7gR", "bWrsg18ucP9cdtFKhzgHbF",
"1Ksnvcvtpn358jX5rtEfKa", "p7Z57shv7p79KiwAtdi8Y3",
"8PLMwgYdaEzG6FAvCo46L8" "wjMRa2GBBnME9bEiNi6XgP"
] ]
}, },
"target": { "blockId": "bkLYB73fk4GePgqDLnUHy1t" } "edgeId": "asT5shwJqDQ67qPuydR4gy"
}, },
"sgwvBXuNbZ16vvJK7rAA8Gw": { "s8zPdEj96z8EoJG2zBqgoE8": {
"id": "sgwvBXuNbZ16vvJK7rAA8Gw", "id": "s8zPdEj96z8EoJG2zBqgoE8",
"blockId": "bgjJd4KTrHSy2GD7AfCPECR", "blockId": "bj5BE1yKPzFFhvRk6cMnmsQ",
"type": "text", "type": "text",
"content": { "content": {
"html": "<div>I love burgers!</div>", "html": "<div>I love burgers!</div>",
@@ -74,9 +74,9 @@
"plainText": "I love burgers!" "plainText": "I love burgers!"
} }
}, },
"sbE2QKjYNXwrBexFgYrkMcn": { "sjZ28izS5e3VjNynFKT2F7E": {
"id": "sbE2QKjYNXwrBexFgYrkMcn", "id": "sjZ28izS5e3VjNynFKT2F7E",
"blockId": "bkLYB73fk4GePgqDLnUHy1t", "blockId": "bdET8zLFQbwpTaAmi4wmezE",
"type": "text", "type": "text",
"content": { "content": {
"html": "<div>Cool!</div>", "html": "<div>Cool!</div>",
@@ -86,35 +86,70 @@
} }
}, },
"allIds": [ "allIds": [
"fLNNAAtRkrw7GG5iqCCdYx", "nEXiHesKXRQJhQbaWfbDVH",
"sdExhY2LZ6VZ69s2WFmBcZg", "sqzMjp1Ba4jTL3A6iJehC6C",
"sgwvBXuNbZ16vvJK7rAA8Gw", "s8zPdEj96z8EoJG2zBqgoE8",
"sbE2QKjYNXwrBexFgYrkMcn" "sjZ28izS5e3VjNynFKT2F7E"
] ]
}, },
"choiceItems": { "choiceItems": {
"byId": { "byId": {
"rFcixXR7vVXDeUuWajc7gR": { "bWrsg18ucP9cdtFKhzgHbF": {
"id": "rFcixXR7vVXDeUuWajc7gR", "id": "bWrsg18ucP9cdtFKhzgHbF",
"stepId": "sdExhY2LZ6VZ69s2WFmBcZg", "stepId": "sqzMjp1Ba4jTL3A6iJehC6C",
"content": "Burgers", "content": "Burgers",
"target": { "blockId": "bgjJd4KTrHSy2GD7AfCPECR" } "edgeId": "jfR6AUWt9b4dhjnUHXB179"
}, },
"1Ksnvcvtpn358jX5rtEfKa": { "p7Z57shv7p79KiwAtdi8Y3": {
"id": "1Ksnvcvtpn358jX5rtEfKa", "id": "p7Z57shv7p79KiwAtdi8Y3",
"stepId": "sdExhY2LZ6VZ69s2WFmBcZg", "stepId": "sqzMjp1Ba4jTL3A6iJehC6C",
"content": "Hot dogs" "content": "Hot dogs"
}, },
"8PLMwgYdaEzG6FAvCo46L8": { "wjMRa2GBBnME9bEiNi6XgP": {
"id": "8PLMwgYdaEzG6FAvCo46L8", "id": "wjMRa2GBBnME9bEiNi6XgP",
"stepId": "sdExhY2LZ6VZ69s2WFmBcZg", "stepId": "sqzMjp1Ba4jTL3A6iJehC6C",
"content": "Carpaccio" "content": "Carpaccio"
} }
}, },
"allIds": [ "allIds": [
"rFcixXR7vVXDeUuWajc7gR", "bWrsg18ucP9cdtFKhzgHbF",
"1Ksnvcvtpn358jX5rtEfKa", "p7Z57shv7p79KiwAtdi8Y3",
"8PLMwgYdaEzG6FAvCo46L8" "wjMRa2GBBnME9bEiNi6XgP"
]
},
"variables": { "byId": {}, "allIds": [] },
"edges": {
"byId": {
"uh95dDpiiZdYxpPFsUqZEg": {
"from": {
"blockId": "weeBMMXxNKwEonMfDX8Z5k",
"stepId": "nEXiHesKXRQJhQbaWfbDVH"
},
"to": { "blockId": "bg2MBdkf6y7g6WsbqAP3eAT" },
"id": "uh95dDpiiZdYxpPFsUqZEg"
},
"jfR6AUWt9b4dhjnUHXB179": {
"from": {
"blockId": "bg2MBdkf6y7g6WsbqAP3eAT",
"stepId": "sqzMjp1Ba4jTL3A6iJehC6C",
"nodeId": "bWrsg18ucP9cdtFKhzgHbF"
},
"to": { "blockId": "bj5BE1yKPzFFhvRk6cMnmsQ" },
"id": "jfR6AUWt9b4dhjnUHXB179"
},
"asT5shwJqDQ67qPuydR4gy": {
"from": {
"blockId": "bg2MBdkf6y7g6WsbqAP3eAT",
"stepId": "sqzMjp1Ba4jTL3A6iJehC6C"
},
"to": { "blockId": "bdET8zLFQbwpTaAmi4wmezE" },
"id": "asT5shwJqDQ67qPuydR4gy"
}
},
"allIds": [
"uh95dDpiiZdYxpPFsUqZEg",
"jfR6AUWt9b4dhjnUHXB179",
"asT5shwJqDQ67qPuydR4gy"
] ]
}, },
"theme": { "theme": {
@@ -126,6 +161,5 @@
"settings": { "settings": {
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 } "typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
}, },
"variables": { "byId": {}, "allIds": [] },
"publicId": null "publicId": null
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -148,7 +148,10 @@ const parseTypebotToPublicTypebot = (
publicId: typebot.publicId, publicId: typebot.publicId,
choiceItems: typebot.choiceItems, choiceItems: typebot.choiceItems,
variables: typebot.variables, variables: typebot.variables,
edges: typebot.edges,
}) })
export const loadRawTypebotInDatabase = (typebot: Typebot) => export const loadRawTypebotInDatabase = (typebot: Typebot) =>
prisma.typebot.create({ data: { ...typebot, ownerId: userIds[1] } as any }) prisma.typebot.create({
data: { ...typebot, id: 'typebot4', ownerId: userIds[1] } as any,
})

View File

@@ -64,7 +64,7 @@ export const parseTestTypebot = ({
type: 'start', type: 'start',
blockId: 'block0', blockId: 'block0',
label: 'Start', label: 'Start',
target: { blockId: 'block1' }, edgeId: 'edge1',
}, },
...steps.byId, ...steps.byId,
}, },
@@ -75,6 +75,16 @@ export const parseTestTypebot = ({
publishedTypebotId: null, publishedTypebotId: null,
updatedAt: new Date(), updatedAt: new Date(),
variables: { byId: {}, allIds: [] }, variables: { byId: {}, allIds: [] },
edges: {
byId: {
edge1: {
id: 'edge1',
from: { blockId: 'block0', stepId: 'step0' },
to: { blockId: 'block1' },
},
},
allIds: ['edge1'],
},
} }
} }

View File

@@ -82,6 +82,11 @@ Cypress.Commands.add(
} }
) )
Cypress.Commands.add('createVariable', (name: string) => {
cy.findByTestId('variables-input').type(name)
cy.findByRole('menuitem', { name: `Create "${name}"` }).click()
})
const getDocumentScroll = () => { const getDocumentScroll = () => {
if (document.scrollingElement) { if (document.scrollingElement) {
const { scrollTop, scrollLeft } = document.scrollingElement const { scrollTop, scrollLeft } = document.scrollingElement

View File

@@ -21,6 +21,7 @@ declare global {
signOut(): Chainable<any> signOut(): Chainable<any>
signIn(email: string): Chainable<any> signIn(email: string): Chainable<any>
loadTypebotFixtureInDatabase(path: string): Chainable<any> loadTypebotFixtureInDatabase(path: string): Chainable<any>
createVariable(name: string): Chainable<any>
mouseMoveBy( mouseMoveBy(
x: number, x: number,
y: number, y: number,

View File

@@ -1,261 +0,0 @@
import { createTypebotWithStep } from 'cypress/plugins/data'
import { preventUserFromRefreshing } from 'cypress/plugins/utils'
import { getIframeBody } from 'cypress/support'
import { InputStepType } from 'models'
describe('Text input', () => {
beforeEach(() => {
cy.task('seed')
createTypebotWithStep({ type: InputStepType.TEXT })
cy.signOut()
})
afterEach(() => {
cy.window().then((win) => {
win.removeEventListener('beforeunload', preventUserFromRefreshing)
})
})
it('options should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody()
.findByPlaceholderText('Type your answer...')
.should('have.attr', 'type')
.should('equal', 'text')
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')
cy.findByTestId('step-step1').click({ force: true })
cy.findByRole('textbox', { name: 'Placeholder:' })
.clear()
.type('Your name...')
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
cy.findByRole('button', { name: 'Restart' }).click()
cy.findByTestId('step-step1').should('contain.text', 'Your name...')
getIframeBody().findByPlaceholderText('Your name...').should('exist')
getIframeBody().findByRole('button', { name: 'Go' })
cy.findByTestId('step-step1').click({ force: true })
cy.findByRole('checkbox', { name: 'Long text?' }).check({ force: true })
cy.findByRole('button', { name: 'Restart' }).click()
getIframeBody().findByTestId('textarea').should('exist')
})
})
describe('Number input', () => {
beforeEach(() => {
cy.task('seed')
createTypebotWithStep({ type: InputStepType.NUMBER })
cy.signOut()
})
it('options should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody()
.findByPlaceholderText('Type your answer...')
.should('have.attr', 'type')
.should('equal', 'number')
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')
cy.findByTestId('step-step1').click({ force: true })
cy.findByRole('textbox', { name: 'Placeholder:' })
.clear()
.type('Your name...')
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
cy.findByRole('spinbutton', { name: 'Min:' }).type('0')
cy.findByRole('spinbutton', { name: 'Max:' }).type('100')
cy.findByRole('spinbutton', { name: 'Step:' }).type('10')
cy.findByRole('button', { name: 'Restart' }).click()
cy.findByTestId('step-step1').should('contain.text', 'Your name...')
getIframeBody()
.findByPlaceholderText('Your name...')
.should('exist')
.type('-1{enter}')
.clear()
.type('150{enter}')
getIframeBody().findByRole('button', { name: 'Go' })
cy.findByTestId('step-step1').click({ force: true })
})
})
describe('Email input', () => {
beforeEach(() => {
cy.task('seed')
createTypebotWithStep({ type: InputStepType.EMAIL })
cy.signOut()
})
it('options should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody()
.findByPlaceholderText('Type your email...')
.should('have.attr', 'type')
.should('equal', 'email')
getIframeBody().findByRole('button', { name: 'Send' })
getIframeBody().findByPlaceholderText('Type your email...').should('exist')
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')
cy.findByTestId('step-step1').click({ force: true })
cy.findByRole('textbox', { name: 'Placeholder:' })
.clear()
.type('Your email...')
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
cy.findByTestId('step-step1').should('contain.text', 'Your email...')
cy.findByRole('button', { name: 'Restart' }).click()
getIframeBody().findByPlaceholderText('Your email...').should('exist')
getIframeBody().findByRole('button', { name: 'Go' })
})
})
describe('URL input', () => {
beforeEach(() => {
cy.task('seed')
createTypebotWithStep({ type: InputStepType.URL })
cy.signOut()
})
it('options should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody()
.findByPlaceholderText('Type your URL...')
.should('have.attr', 'type')
.should('eq', 'url')
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')
cy.findByTestId('step-step1').click({ force: true })
cy.findByRole('textbox', { name: 'Placeholder:' })
.clear()
.type('Your URL...')
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
cy.findByTestId('step-step1').should('contain.text', 'Your URL...')
cy.findByRole('button', { name: 'Restart' }).click()
getIframeBody().findByPlaceholderText('Your URL...').should('exist')
getIframeBody().findByRole('button', { name: 'Go' })
})
})
describe('Date input', () => {
beforeEach(() => {
cy.task('seed')
createTypebotWithStep({ type: InputStepType.DATE })
cy.signOut()
})
it('options should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody()
.findByTestId('from-date')
.should('have.attr', 'type')
.should('eq', 'date')
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')
cy.findByTestId('step-step1').click({ force: true })
cy.findByRole('checkbox', { name: 'Is range?' }).check({ force: true })
cy.findByRole('textbox', { name: 'From label:' }).clear().type('Previous:')
cy.findByRole('textbox', { name: 'To label:' }).clear().type('After:')
cy.findByRole('checkbox', { name: 'With time?' }).check({ force: true })
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
cy.findByRole('button', { name: 'Restart' }).click()
getIframeBody()
.findByTestId('from-date')
.should('have.attr', 'type')
.should('eq', 'datetime-local')
getIframeBody()
.findByTestId('to-date')
.should('have.attr', 'type')
.should('eq', 'datetime-local')
getIframeBody().findByRole('button', { name: 'Go' })
})
})
describe('Phone number input', () => {
beforeEach(() => {
cy.task('seed')
createTypebotWithStep({ type: InputStepType.PHONE })
cy.signOut()
})
it('options should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody()
.findByPlaceholderText('Your phone number...')
.should('have.attr', 'type')
.should('eq', 'tel')
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')
cy.findByTestId('step-step1').click({ force: true })
cy.findByRole('textbox', { name: 'Placeholder:' })
.clear()
.type('+33 XX XX XX XX')
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
cy.findByRole('button', { name: 'Restart' }).click()
getIframeBody()
.findByPlaceholderText('+33 XX XX XX XX')
.type('+33 6 73 18 45 36')
getIframeBody()
.findByRole('img')
.should('have.attr', 'alt')
.should('eq', 'France')
getIframeBody().findByRole('button', { name: 'Go' }).click()
getIframeBody().findByText('+33673184536').should('exist')
})
})
describe('Button input', () => {
beforeEach(() => {
cy.task('seed')
createTypebotWithStep({ type: InputStepType.CHOICE })
cy.signOut()
})
it('Can edit choice items', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit')
cy.findByDisplayValue('Click to edit').type('Item 1{enter}')
cy.findByText('Item 1').trigger('mouseover')
cy.findByRole('button', { name: 'Add item' }).click()
cy.findByDisplayValue('Click to edit').type('Item 2{enter}')
cy.findByRole('button', { name: 'Add item' }).click()
cy.findByDisplayValue('Click to edit').type('Item 3{enter}')
cy.findByText('Item 2').rightclick()
cy.findByRole('menuitem', { name: 'Delete' }).click()
cy.findByText('Item 2').should('not.exist')
cy.findByTestId('step-step1').click({ force: true })
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody().findByRole('button', { name: 'Item 3' }).click()
getIframeBody().findByRole('button', { name: 'Item 3' }).should('not.exist')
getIframeBody().findByText('Item 3')
cy.findByRole('button', { name: 'Close' }).click()
cy.findByTestId('step-step1').click({ force: true })
cy.findByRole('checkbox', { name: 'Multiple choice?' }).check({
force: true,
})
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
cy.wait(200)
cy.findByTestId('step-step1').click({ force: true })
cy.findByText('Item 1').trigger('mouseover')
cy.findByRole('button', { name: 'Add item' }).click()
cy.findByDisplayValue('Click to edit').type('Item 2{enter}')
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody().findByRole('checkbox', { name: 'Item 3' }).click()
getIframeBody().findByRole('checkbox', { name: 'Item 1' }).click()
getIframeBody().findByRole('button', { name: 'Go' }).click()
getIframeBody().findByText('Item 3, Item 1').should('exist')
})
it('Single choice targets should work', () => {
cy.loadTypebotFixtureInDatabase('typebots/singleChoiceTarget.json')
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/edit')
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody().findByRole('button', { name: 'Burgers' }).click()
getIframeBody().findByText('I love burgers!').should('exist')
cy.findByRole('button', { name: 'Restart' }).click()
getIframeBody().findByRole('button', { name: 'Carpaccio' }).click()
getIframeBody().findByText('Cool!').should('exist')
})
})

View File

@@ -0,0 +1,58 @@
import { createTypebotWithStep } from 'cypress/plugins/data'
import { getIframeBody } from 'cypress/support'
import { InputStepType } from 'models'
describe('Button input', () => {
beforeEach(() => {
cy.task('seed')
createTypebotWithStep({ type: InputStepType.CHOICE })
cy.signOut()
})
it('Can edit choice items', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit')
cy.findByDisplayValue('Click to edit').type('Item 1{enter}')
cy.findByText('Item 1').trigger('mouseover')
cy.findByRole('button', { name: 'Add item' }).click()
cy.findByDisplayValue('Click to edit').type('Item 2{enter}')
cy.findByRole('button', { name: 'Add item' }).click()
cy.findByDisplayValue('Click to edit').type('Item 3{enter}')
cy.findByText('Item 2').rightclick()
cy.findByRole('menuitem', { name: 'Delete' }).click()
cy.findByText('Item 2').should('not.exist')
cy.findByTestId('step-step1').click({ force: true })
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody().findByRole('button', { name: 'Item 3' }).click()
getIframeBody().findByRole('button', { name: 'Item 3' }).should('not.exist')
getIframeBody().findByText('Item 3')
cy.findByRole('button', { name: 'Close' }).click()
cy.findByTestId('step-step1').click({ force: true })
cy.findByRole('checkbox', { name: 'Multiple choice?' }).check({
force: true,
})
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
cy.wait(200)
cy.findByTestId('step-step1').click({ force: true })
cy.findByText('Item 1').trigger('mouseover')
cy.findByRole('button', { name: 'Add item' }).click()
cy.findByDisplayValue('Click to edit').type('Item 2{enter}')
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody().findByRole('checkbox', { name: 'Item 3' }).click()
getIframeBody().findByRole('checkbox', { name: 'Item 1' }).click()
getIframeBody().findByRole('button', { name: 'Go' }).click()
getIframeBody().findByText('Item 3, Item 1').should('exist')
})
it('Single choice targets should work', () => {
cy.loadTypebotFixtureInDatabase('typebots/singleChoiceTarget.json')
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/edit')
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody().findByRole('button', { name: 'Burgers' }).click()
getIframeBody().findByText('I love burgers!').should('exist')
cy.findByRole('button', { name: 'Restart' }).click()
getIframeBody().findByRole('button', { name: 'Carpaccio' }).click()
getIframeBody().findByText('Cool!').should('exist')
})
})

View File

@@ -0,0 +1,38 @@
import { createTypebotWithStep } from 'cypress/plugins/data'
import { getIframeBody } from 'cypress/support'
import { InputStepType } from 'models'
describe('Date input', () => {
beforeEach(() => {
cy.task('seed')
createTypebotWithStep({ type: InputStepType.DATE })
cy.signOut()
})
it('options should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody()
.findByTestId('from-date')
.should('have.attr', 'type')
.should('eq', 'date')
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')
cy.findByTestId('step-step1').click({ force: true })
cy.findByRole('checkbox', { name: 'Is range?' }).check({ force: true })
cy.findByRole('textbox', { name: 'From label:' }).clear().type('Previous:')
cy.findByRole('textbox', { name: 'To label:' }).clear().type('After:')
cy.findByRole('checkbox', { name: 'With time?' }).check({ force: true })
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
cy.findByRole('button', { name: 'Restart' }).click()
getIframeBody()
.findByTestId('from-date')
.should('have.attr', 'type')
.should('eq', 'datetime-local')
getIframeBody()
.findByTestId('to-date')
.should('have.attr', 'type')
.should('eq', 'datetime-local')
getIframeBody().findByRole('button', { name: 'Go' })
})
})

View File

@@ -0,0 +1,33 @@
import { createTypebotWithStep } from 'cypress/plugins/data'
import { getIframeBody } from 'cypress/support'
import { InputStepType } from 'models'
describe('Email input', () => {
beforeEach(() => {
cy.task('seed')
createTypebotWithStep({ type: InputStepType.EMAIL })
cy.signOut()
})
it('options should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody()
.findByPlaceholderText('Type your email...')
.should('have.attr', 'type')
.should('equal', 'email')
getIframeBody().findByRole('button', { name: 'Send' })
getIframeBody().findByPlaceholderText('Type your email...').should('exist')
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')
cy.findByTestId('step-step1').click({ force: true })
cy.findByRole('textbox', { name: 'Placeholder:' })
.clear()
.type('Your email...')
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
cy.findByTestId('step-step1').should('contain.text', 'Your email...')
cy.findByRole('button', { name: 'Restart' }).click()
getIframeBody().findByPlaceholderText('Your email...').should('exist')
getIframeBody().findByRole('button', { name: 'Go' })
})
})

View File

@@ -0,0 +1,40 @@
import { createTypebotWithStep } from 'cypress/plugins/data'
import { getIframeBody } from 'cypress/support'
import { InputStepType } from 'models'
describe('Number input', () => {
beforeEach(() => {
cy.task('seed')
createTypebotWithStep({ type: InputStepType.NUMBER })
cy.signOut()
})
it('options should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody()
.findByPlaceholderText('Type your answer...')
.should('have.attr', 'type')
.should('equal', 'number')
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')
cy.findByTestId('step-step1').click({ force: true })
cy.findByRole('textbox', { name: 'Placeholder:' })
.clear()
.type('Your name...')
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
cy.findByRole('spinbutton', { name: 'Min:' }).type('0')
cy.findByRole('spinbutton', { name: 'Max:' }).type('100')
cy.findByRole('spinbutton', { name: 'Step:' }).type('10')
cy.findByRole('button', { name: 'Restart' }).click()
cy.findByTestId('step-step1').should('contain.text', 'Your name...')
getIframeBody()
.findByPlaceholderText('Your name...')
.should('exist')
.type('-1{enter}')
.clear()
.type('150{enter}')
getIframeBody().findByRole('button', { name: 'Go' })
cy.findByTestId('step-step1').click({ force: true })
})
})

View File

@@ -0,0 +1,37 @@
import { createTypebotWithStep } from 'cypress/plugins/data'
import { getIframeBody } from 'cypress/support'
import { InputStepType } from 'models'
describe('Phone number input', () => {
beforeEach(() => {
cy.task('seed')
createTypebotWithStep({ type: InputStepType.PHONE })
cy.signOut()
})
it('options should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody()
.findByPlaceholderText('Your phone number...')
.should('have.attr', 'type')
.should('eq', 'tel')
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')
cy.findByTestId('step-step1').click({ force: true })
cy.findByRole('textbox', { name: 'Placeholder:' })
.clear()
.type('+33 XX XX XX XX')
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
cy.findByRole('button', { name: 'Restart' }).click()
getIframeBody()
.findByPlaceholderText('+33 XX XX XX XX')
.type('+33 6 73 18 45 36')
getIframeBody()
.findByRole('img')
.should('have.attr', 'alt')
.should('eq', 'France')
getIframeBody().findByRole('button', { name: 'Go' }).click()
getIframeBody().findByText('+33673184536').should('exist')
})
})

View File

@@ -0,0 +1,42 @@
import { createTypebotWithStep } from 'cypress/plugins/data'
import { preventUserFromRefreshing } from 'cypress/plugins/utils'
import { getIframeBody } from 'cypress/support'
import { InputStepType } from 'models'
describe('Text input', () => {
beforeEach(() => {
cy.task('seed')
createTypebotWithStep({ type: InputStepType.TEXT })
cy.signOut()
})
afterEach(() => {
cy.window().then((win) => {
win.removeEventListener('beforeunload', preventUserFromRefreshing)
})
})
it('options should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody()
.findByPlaceholderText('Type your answer...')
.should('have.attr', 'type')
.should('equal', 'text')
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')
cy.findByTestId('step-step1').click({ force: true })
cy.findByRole('textbox', { name: 'Placeholder:' })
.clear()
.type('Your name...')
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
cy.findByRole('button', { name: 'Restart' }).click()
cy.findByTestId('step-step1').should('contain.text', 'Your name...')
getIframeBody().findByPlaceholderText('Your name...').should('exist')
getIframeBody().findByRole('button', { name: 'Go' })
cy.findByTestId('step-step1').click({ force: true })
cy.findByRole('checkbox', { name: 'Long text?' }).check({ force: true })
cy.findByRole('button', { name: 'Restart' }).click()
getIframeBody().findByTestId('textarea').should('exist')
})
})

View File

@@ -0,0 +1,31 @@
import { createTypebotWithStep } from 'cypress/plugins/data'
import { getIframeBody } from 'cypress/support'
import { InputStepType } from 'models'
describe('URL input', () => {
beforeEach(() => {
cy.task('seed')
createTypebotWithStep({ type: InputStepType.URL })
cy.signOut()
})
it('options should work', () => {
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot3/edit')
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody()
.findByPlaceholderText('Type your URL...')
.should('have.attr', 'type')
.should('eq', 'url')
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')
cy.findByTestId('step-step1').click({ force: true })
cy.findByRole('textbox', { name: 'Placeholder:' })
.clear()
.type('Your URL...')
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
cy.findByTestId('step-step1').should('contain.text', 'Your URL...')
cy.findByRole('button', { name: 'Restart' }).click()
getIframeBody().findByPlaceholderText('Your URL...').should('exist')
getIframeBody().findByRole('button', { name: 'Go' })
})
})

View File

@@ -121,7 +121,7 @@ describe('Google sheets', () => {
}) })
const fillInSpreadsheetInfo = () => { const fillInSpreadsheetInfo = () => {
cy.findByTestId('step-step1').click() cy.findByText('Configure...').click()
cy.findByRole('button', { name: 'Select an account' }).click() cy.findByRole('button', { name: 'Select an account' }).click()
cy.findByRole('menuitem', { name: 'test2@gmail.com' }).click() cy.findByRole('menuitem', { name: 'test2@gmail.com' }).click()

View File

@@ -1,44 +1,6 @@
import { preventUserFromRefreshing } from 'cypress/plugins/utils' import { preventUserFromRefreshing } from 'cypress/plugins/utils'
import { getIframeBody } from 'cypress/support' import { getIframeBody } from 'cypress/support'
describe('Set variables', () => {
beforeEach(() => {
cy.task('seed')
cy.signOut()
})
afterEach(() => {
cy.window().then((win) => {
win.removeEventListener('beforeunload', preventUserFromRefreshing)
})
})
it('options should work', () => {
cy.loadTypebotFixtureInDatabase('typebots/logic/setVariable.json')
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/edit')
cy.findByTestId('step-number-step').click()
createNewVar('Num')
cy.findByTestId('step-set-var-1').click()
createNewVar('Total')
cy.findByRole('textbox', { name: 'Value / Expression:' }).type(
'1000 * {{Num}}',
{ parseSpecialCharSequences: false }
)
cy.findByTestId('step-set-var-2').click()
createNewVar('Custom var')
cy.findByRole('textbox', { name: 'Value / Expression:' }).type(
'Custom value'
)
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody()
.findByPlaceholderText('Type your answer...')
.type('365{enter}')
getIframeBody().findByText('Total: 365000').should('exist')
getIframeBody().findByText('Custom var: Custom value')
})
})
describe('Condition step', () => { describe('Condition step', () => {
beforeEach(() => { beforeEach(() => {
cy.task('seed') cy.task('seed')
@@ -56,7 +18,7 @@ describe('Condition step', () => {
cy.signIn('test2@gmail.com') cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/edit') cy.visit('/typebots/typebot4/edit')
cy.findByTestId('step-condition1').click() cy.findAllByText('Equal to').first().click()
cy.findByTestId('variables-input').click() cy.findByTestId('variables-input').click()
cy.findByRole('menuitem', { name: 'Age' }).click() cy.findByRole('menuitem', { name: 'Age' }).click()
@@ -71,7 +33,7 @@ describe('Condition step', () => {
cy.findByRole('menuitem', { name: 'Less than' }).click() cy.findByRole('menuitem', { name: 'Less than' }).click()
cy.findAllByPlaceholderText('Type a value...').last().type('100') cy.findAllByPlaceholderText('Type a value...').last().type('100')
cy.findByTestId('step-condition2').click() cy.findAllByText('Equal to').last().click()
cy.findByTestId('variables-input').click() cy.findByTestId('variables-input').click()
cy.findByRole('menuitem', { name: 'Age' }).click() cy.findByRole('menuitem', { name: 'Age' }).click()
@@ -80,20 +42,21 @@ describe('Condition step', () => {
cy.findByPlaceholderText('Type a value...').type('20') cy.findByPlaceholderText('Type a value...').type('20')
cy.findByRole('button', { name: 'Preview' }).click() cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody().findByPlaceholderText('Type your age...').type('15{enter}') getIframeBody()
.findByPlaceholderText('Type your answer...')
.type('15{enter}')
getIframeBody().findByText('You are younger than 20').should('exist') getIframeBody().findByText('You are younger than 20').should('exist')
cy.findByRole('button', { name: 'Restart' }).click() cy.findByRole('button', { name: 'Restart' }).click()
getIframeBody().findByPlaceholderText('Type your age...').type('45{enter}') getIframeBody()
getIframeBody().findByText('Wow you are older than 20').should('exist') .findByPlaceholderText('Type your answer...')
.type('45{enter}')
getIframeBody().findByText('You are older than 20').should('exist')
cy.findByRole('button', { name: 'Restart' }).click() cy.findByRole('button', { name: 'Restart' }).click()
getIframeBody().findByPlaceholderText('Type your age...').type('90{enter}') getIframeBody()
getIframeBody().findByText('Wow you are older than 80').should('exist') .findByPlaceholderText('Type your answer...')
.type('90{enter}')
getIframeBody().findByText('You are older than 80').should('exist')
}) })
}) })
const createNewVar = (name: string) => {
cy.findByTestId('variables-input').type(name)
cy.findByRole('menuitem', { name: `Create "${name}"` }).click()
}

View File

@@ -0,0 +1,40 @@
import { preventUserFromRefreshing } from 'cypress/plugins/utils'
import { getIframeBody } from 'cypress/support'
describe('Set variables', () => {
beforeEach(() => {
cy.task('seed')
cy.signOut()
})
afterEach(() => {
cy.window().then((win) => {
win.removeEventListener('beforeunload', preventUserFromRefreshing)
})
})
it('options should work', () => {
cy.loadTypebotFixtureInDatabase('typebots/logic/setVariable.json')
cy.signIn('test2@gmail.com')
cy.visit('/typebots/typebot4/edit')
cy.findByText('Type your answer...').click()
cy.createVariable('Num')
cy.findAllByText('Click to edit...').first().click()
cy.createVariable('Total')
cy.findByRole('textbox', { name: 'Value / Expression:' }).type(
'1000 * {{Num}}',
{ parseSpecialCharSequences: false }
)
cy.findAllByText('Click to edit...').last().click()
cy.createVariable('Custom var')
cy.findByRole('textbox', { name: 'Value / Expression:' }).type(
'Custom value'
)
cy.findByRole('button', { name: 'Preview' }).click()
getIframeBody()
.findByPlaceholderText('Type your answer...')
.type('365{enter}')
getIframeBody().findByText('Total: 365000').should('exist')
getIframeBody().findByText('Custom var: Custom value')
})
})

View File

@@ -1,9 +1,6 @@
import { Coordinates } from '@dnd-kit/core/dist/types' import { Coordinates } from '@dnd-kit/core/dist/types'
import { Block, Step, Table, Target, Typebot } from 'models' import { Block, Edge, Table, Target, Typebot } from 'models'
import { import { AnchorsPositionProps } from 'components/board/graph/Edges/Edge'
AnchorsPositionProps,
EdgeType,
} from 'components/board/graph/Edges/Edge'
import { import {
stubLength, stubLength,
blockWidth, blockWidth,
@@ -13,7 +10,6 @@ import {
} from 'contexts/GraphContext' } from 'contexts/GraphContext'
import { roundCorners } from 'svg-round-corners' import { roundCorners } from 'svg-round-corners'
import { headerHeight } from 'components/shared/TypebotHeader' import { headerHeight } from 'components/shared/TypebotHeader'
import { isConditionStep } from 'utils'
export const computeDropOffPath = ( export const computeDropOffPath = (
sourcePosition: Coordinates, sourcePosition: Coordinates,
@@ -285,10 +281,10 @@ export const computeEdgePathToMouse = ({
export const getEndpointTopOffset = ( export const getEndpointTopOffset = (
graphPosition: Coordinates, graphPosition: Coordinates,
endpoints: Table<Endpoint>, endpoints: Table<Endpoint>,
id?: string endpointId?: string
): number => { ): number => {
if (!id) return 0 if (!endpointId) return 0
const endpointRef = endpoints.byId[id]?.ref const endpointRef = endpoints.byId[endpointId]?.ref
if (!endpointRef) return 0 if (!endpointRef) return 0
return ( return (
8 + 8 +
@@ -298,17 +294,5 @@ export const getEndpointTopOffset = (
) )
} }
export const getTarget = (step: Step, edgeType: EdgeType) => { export const getSourceEndpointId = (edge?: Edge) =>
if (!step) return edge?.from.nodeId ?? edge?.from.stepId + `${edge?.from.conditionType ?? ''}`
switch (edgeType) {
case EdgeType.STEP:
case EdgeType.CHOICE_ITEM:
return step.target
case EdgeType.CONDITION_TRUE:
if (!isConditionStep(step)) return
return step.trueTarget
case EdgeType.CONDITION_FALSE:
if (!isConditionStep(step)) return
return step.falseTarget
}
}

View File

@@ -18,6 +18,7 @@ export const parseTypebotToPublicTypebot = (
publicId: typebot.publicId, publicId: typebot.publicId,
choiceItems: typebot.choiceItems, choiceItems: typebot.choiceItems,
variables: typebot.variables, variables: typebot.variables,
edges: typebot.edges,
}) })
export const createPublishedTypebot = async ( export const createPublishedTypebot = async (

View File

@@ -234,6 +234,7 @@ export const parseNewTypebot = ({
steps: { byId: { [startStepId]: startStep }, allIds: [startStepId] }, steps: { byId: { [startStepId]: startStep }, allIds: [startStepId] },
choiceItems: { byId: {}, allIds: [] }, choiceItems: { byId: {}, allIds: [] },
variables: { byId: {}, allIds: [] }, variables: { byId: {}, allIds: [] },
edges: { byId: {}, allIds: [] },
theme, theme,
settings, settings,
} }

View File

@@ -4,7 +4,7 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group'
import { ChatStep } from './ChatStep' import { ChatStep } from './ChatStep'
import { AvatarSideContainer } from './AvatarSideContainer' import { AvatarSideContainer } from './AvatarSideContainer'
import { HostAvatarsContext } from '../../contexts/HostAvatarsContext' import { HostAvatarsContext } from '../../contexts/HostAvatarsContext'
import { Step, Target } from 'models' import { Edge, Step, Target } from 'models'
import { useTypebot } from '../../contexts/TypebotContext' import { useTypebot } from '../../contexts/TypebotContext'
import { import {
isChoiceInput, isChoiceInput,
@@ -20,7 +20,7 @@ import { executeIntegration } from 'services/integration'
type ChatBlockProps = { type ChatBlockProps = {
stepIds: string[] stepIds: string[]
startStepId?: string startStepId?: string
onBlockEnd: (target?: Target) => void onBlockEnd: (edgeId?: string) => void
} }
export const ChatBlock = ({ export const ChatBlock = ({
@@ -46,20 +46,20 @@ export const ChatBlock = ({
const currentStep = [...displayedSteps].pop() const currentStep = [...displayedSteps].pop()
if (!currentStep) return if (!currentStep) return
if (isLogicStep(currentStep)) { if (isLogicStep(currentStep)) {
const target = executeLogic( const nextEdgeId = executeLogic(
currentStep, currentStep,
typebot.variables, typebot.variables,
updateVariableValue updateVariableValue
) )
target ? onBlockEnd(target) : displayNextStep() nextEdgeId ? onBlockEnd(nextEdgeId) : displayNextStep()
} }
if (isIntegrationStep(currentStep)) { if (isIntegrationStep(currentStep)) {
const target = await executeIntegration( const nextEdgeId = await executeIntegration(
currentStep, currentStep,
typebot.variables, typebot.variables,
updateVariableValue updateVariableValue
) )
target ? onBlockEnd(target) : displayNextStep() nextEdgeId ? onBlockEnd(nextEdgeId) : displayNextStep()
} }
} }
@@ -90,11 +90,8 @@ export const ChatBlock = ({
answerContent answerContent
) )
) )
if ( if (currentStep?.edgeId || displayedSteps.length === stepIds.length)
currentStep?.target?.blockId || return onBlockEnd(currentStep.edgeId)
displayedSteps.length === stepIds.length
)
return onBlockEnd(currentStep?.target)
} }
const nextStep = typebot.steps.byId[stepIds[displayedSteps.length]] const nextStep = typebot.steps.byId[stepIds[displayedSteps.length]]
if (nextStep) setDisplayedSteps([...displayedSteps, nextStep]) if (nextStep) setDisplayedSteps([...displayedSteps, nextStep])

View File

@@ -5,11 +5,11 @@ import { useFrame } from 'react-frame-component'
import { setCssVariablesValue } from '../services/theme' import { setCssVariablesValue } from '../services/theme'
import { useAnswers } from '../contexts/AnswersContext' import { useAnswers } from '../contexts/AnswersContext'
import { deepEqual } from 'fast-equals' import { deepEqual } from 'fast-equals'
import { Answer, Block, PublicTypebot, Target } from 'models' import { Answer, Block, Edge, PublicTypebot } from 'models'
type Props = { type Props = {
typebot: PublicTypebot typebot: PublicTypebot
onNewBlockVisible: (blockId: string) => void onNewBlockVisible: (edgeId: string) => void
onNewAnswer: (answer: Answer) => void onNewAnswer: (answer: Answer) => void
onCompleted: () => void onCompleted: () => void
} }
@@ -27,22 +27,24 @@ export const ConversationContainer = ({
const { answers } = useAnswers() const { answers } = useAnswers()
const bottomAnchor = useRef<HTMLDivElement | null>(null) const bottomAnchor = useRef<HTMLDivElement | null>(null)
const displayNextBlock = (target?: Target) => { const displayNextBlock = (edgeId?: string) => {
if (!target) return onCompleted() const edge = typebot.edges.byId[edgeId ?? '']
if (!edge) return onCompleted()
const nextBlock = { const nextBlock = {
block: typebot.blocks.byId[target.blockId], block: typebot.blocks.byId[edge.to.blockId],
startStepId: target.stepId, startStepId: edge.to.stepId,
} }
if (!nextBlock) return onCompleted() if (!nextBlock) return onCompleted()
onNewBlockVisible(target.blockId) onNewBlockVisible(edge.id)
setDisplayedBlocks([...displayedBlocks, nextBlock]) setDisplayedBlocks([...displayedBlocks, nextBlock])
} }
useEffect(() => { useEffect(() => {
const blocks = typebot.blocks const blocks = typebot.blocks
const firstTarget = const firstEdgeId =
typebot.steps.byId[blocks.byId[blocks.allIds[0]].stepIds[0]].target typebot.steps.byId[blocks.byId[blocks.allIds[0]].stepIds[0]].edgeId
if (firstTarget) displayNextBlock(firstTarget) if (!firstEdgeId) return
displayNextBlock(firstEdgeId)
}, []) }, [])
useEffect(() => { useEffect(() => {

View File

@@ -11,7 +11,7 @@ import { Answer, BackgroundType, PublicTypebot } from 'models'
export type TypebotViewerProps = { export type TypebotViewerProps = {
typebot: PublicTypebot typebot: PublicTypebot
onNewBlockVisible?: (blockId: string) => void onNewBlockVisible?: (edgeId: string) => void
onNewAnswer?: (answer: Answer) => void onNewAnswer?: (answer: Answer) => void
onCompleted?: () => void onCompleted?: () => void
} }

View File

@@ -4,10 +4,10 @@ export const getSingleChoiceTargetId = (
currentStep: ChoiceInputStep, currentStep: ChoiceInputStep,
choiceItems: Table<ChoiceItem>, choiceItems: Table<ChoiceItem>,
answerContent?: string answerContent?: string
): Target | undefined => { ): string | undefined => {
const itemId = currentStep.options.itemIds.find( const itemId = currentStep.options.itemIds.find(
(itemId) => choiceItems.byId[itemId].content === answerContent (itemId) => choiceItems.byId[itemId].content === answerContent
) )
if (!itemId) throw new Error('itemId should exist') if (!itemId) throw new Error('itemId should exist')
return choiceItems.byId[itemId].target ?? currentStep.target return choiceItems.byId[itemId].edgeId ?? currentStep.edgeId
} }

View File

@@ -44,7 +44,7 @@ const executeGoogleSheetIntegration = async (
variables: Table<Variable>, variables: Table<Variable>,
updateVariableValue: (variableId: string, value: string) => void updateVariableValue: (variableId: string, value: string) => void
) => { ) => {
if (!step.options) return step.target if (!step.options) return step.edgeId
switch (step.options?.action) { switch (step.options?.action) {
case GoogleSheetsAction.INSERT_ROW: case GoogleSheetsAction.INSERT_ROW:
await insertRowInGoogleSheets(step.options, variables) await insertRowInGoogleSheets(step.options, variables)
@@ -56,7 +56,7 @@ const executeGoogleSheetIntegration = async (
await getRowFromGoogleSheets(step.options, variables, updateVariableValue) await getRowFromGoogleSheets(step.options, variables, updateVariableValue)
break break
} }
return step.target return step.edgeId
} }
const insertRowInGoogleSheets = async ( const insertRowInGoogleSheets = async (

View File

@@ -15,7 +15,7 @@ export const executeLogic = (
step: LogicStep, step: LogicStep,
variables: Table<Variable>, variables: Table<Variable>,
updateVariableValue: (variableId: string, expression: string) => void updateVariableValue: (variableId: string, expression: string) => void
): Target | undefined => { ): string | undefined => {
switch (step.type) { switch (step.type) {
case LogicStepType.SET_VARIABLE: { case LogicStepType.SET_VARIABLE: {
if (!step.options?.variableId || !step.options.expressionToEvaluate) if (!step.options?.variableId || !step.options.expressionToEvaluate)
@@ -36,7 +36,7 @@ export const executeLogic = (
: step.options?.comparisons.allIds.some( : step.options?.comparisons.allIds.some(
executeComparison(step, variables) executeComparison(step, variables)
) )
return isConditionPassed ? step.trueTarget : step.falseTarget return isConditionPassed ? step.trueEdgeId : step.falseEdgeId
} }
} }
} }

View File

@@ -110,6 +110,7 @@ model Typebot {
steps Json steps Json
choiceItems Json choiceItems Json
variables Json variables Json
edges Json
theme Json theme Json
settings Json settings Json
publicId String? @unique publicId String? @unique
@@ -124,6 +125,7 @@ model PublicTypebot {
steps Json steps Json
choiceItems Json choiceItems Json
variables Json variables Json
edges Json
theme Json theme Json
settings Json settings Json
publicId String? @unique publicId String? @unique

View File

@@ -1,5 +1,5 @@
import { PublicTypebot as PublicTypebotFromPrisma } from 'db' import { PublicTypebot as PublicTypebotFromPrisma } from 'db'
import { Block, ChoiceItem, Settings, Step, Theme } from './typebot' import { Block, ChoiceItem, Edge, Settings, Step, Theme } from './typebot'
import { Variable } from './typebot/variable' import { Variable } from './typebot/variable'
import { Table } from './utils' import { Table } from './utils'
@@ -17,6 +17,7 @@ export type PublicTypebot = Omit<
steps: Table<Step> steps: Table<Step>
choiceItems: Table<ChoiceItem> choiceItems: Table<ChoiceItem>
variables: Table<Variable> variables: Table<Variable>
edges: Table<Edge>
theme: Theme theme: Theme
settings: Settings settings: Settings
} }

View File

@@ -1,4 +1,3 @@
import { Target } from '.'
import { StepBase } from './steps' import { StepBase } from './steps'
export type InputStep = export type InputStep =
@@ -66,7 +65,7 @@ export type ChoiceItem = {
id: string id: string
stepId: string stepId: string
content?: string content?: string
target?: Target edgeId?: string
} }
type OptionBase = { variableId?: string } type OptionBase = { variableId?: string }

View File

@@ -1,4 +1,4 @@
import { StepBase, Target } from '.' import { StepBase } from '.'
import { Table } from '../..' import { Table } from '../..'
export type LogicStep = SetVariableStep | ConditionStep export type LogicStep = SetVariableStep | ConditionStep
@@ -32,8 +32,8 @@ export enum ComparisonOperators {
export type ConditionStep = StepBase & { export type ConditionStep = StepBase & {
type: LogicStepType.CONDITION type: LogicStepType.CONDITION
options: ConditionOptions options: ConditionOptions
trueTarget?: Target trueEdgeId?: string
falseTarget?: Target falseEdgeId?: string
} }
export type ConditionOptions = { export type ConditionOptions = {

View File

@@ -36,11 +36,9 @@ export type StepOptions =
| LogicStepOptions | LogicStepOptions
| IntegrationStepOptions | IntegrationStepOptions
export type StepBase = { id: string; blockId: string; target?: Target } export type StepBase = { id: string; blockId: string; edgeId?: string }
export type StartStep = StepBase & { export type StartStep = StepBase & {
type: 'start' type: 'start'
label: string label: string
} }
export type Target = { blockId: string; stepId?: string }

View File

@@ -14,6 +14,7 @@ export type Typebot = Omit<
steps: Table<Step> steps: Table<Step>
choiceItems: Table<ChoiceItem> choiceItems: Table<ChoiceItem>
variables: Table<Variable> variables: Table<Variable>
edges: Table<Edge>
theme: Theme theme: Theme
settings: Settings settings: Settings
} }
@@ -27,3 +28,16 @@ export type Block = {
} }
stepIds: string[] stepIds: string[]
} }
export type Source = {
blockId: string
stepId: string
nodeId?: string
conditionType?: 'true' | 'false'
}
export type Target = { blockId: string; stepId?: string }
export type Edge = {
id: string
from: Source
to: Target
}