2
0

chore(editor): ♻️ Revert tables to arrays

Yet another refacto. I improved many many mechanisms on this one including dnd. It is now end 2 end tested 🎉
This commit is contained in:
Baptiste Arnaud
2022-02-04 19:00:08 +01:00
parent 8a350eee6c
commit 524ef0812c
123 changed files with 2998 additions and 3112 deletions

View File

@ -6,6 +6,7 @@ import {
MenuItem, MenuItem,
MenuList, MenuList,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import assert from 'assert'
import { DownloadIcon, MoreVerticalIcon } from 'assets/icons' import { DownloadIcon, MoreVerticalIcon } from 'assets/icons'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import React, { useState } from 'react' import React, { useState } from 'react'
@ -16,7 +17,7 @@ export const BoardMenuButton = (props: MenuButtonProps) => {
const [isDownloading, setIsDownloading] = useState(false) const [isDownloading, setIsDownloading] = useState(false)
const downloadFlow = () => { const downloadFlow = () => {
if (!typebot) return assert(typebot)
setIsDownloading(true) setIsDownloading(true)
const data = const data =
'data:application/json;charset=utf-8,' + 'data:application/json;charset=utf-8,' +
@ -39,6 +40,7 @@ export const BoardMenuButton = (props: MenuButtonProps) => {
colorScheme="blue" colorScheme="blue"
icon={<MoreVerticalIcon transform={'rotate(90deg)'} />} icon={<MoreVerticalIcon transform={'rotate(90deg)'} />}
isLoading={isDownloading} isLoading={isDownloading}
size="sm"
{...props} {...props}
/> />
<MenuList> <MenuList>

View File

@ -1,6 +1,6 @@
import { Flex, HStack, StackProps, Text } from '@chakra-ui/react' import { Flex, HStack, StackProps, Text } from '@chakra-ui/react'
import { StepType, DraggableStepType } from 'models' import { StepType, DraggableStepType } from 'models'
import { useStepDnd } from 'contexts/StepDndContext' import { useStepDnd } from 'contexts/GraphDndContext'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { StepIcon } from './StepIcon' import { StepIcon } from './StepIcon'
import { StepTypeLabel } from './StepTypeLabel' import { StepTypeLabel } from './StepTypeLabel'

View File

@ -16,7 +16,7 @@ import {
IntegrationStepType, IntegrationStepType,
LogicStepType, LogicStepType,
} from 'models' } from 'models'
import { useStepDnd } from 'contexts/StepDndContext' import { useStepDnd } from 'contexts/GraphDndContext'
import React, { useState } from 'react' import React, { useState } from 'react'
import { StepCard, StepCardOverlay } from './StepCard' import { StepCard, StepCardOverlay } from './StepCard'
import { LockedIcon, UnlockedIcon } from 'assets/icons' import { LockedIcon, UnlockedIcon } from 'assets/icons'

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 { setPreviewingEdgeId } = useGraph() const { setPreviewingEdge } = 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,10 +45,13 @@ export const PreviewDrawer = () => {
} }
useEventListener('mouseup', handleMouseUp) useEventListener('mouseup', handleMouseUp)
const handleNewBlockVisible = (edgeId: string) => setPreviewingEdgeId(edgeId)
const handleRestartClick = () => setRestartKey((key) => key + 1) const handleRestartClick = () => setRestartKey((key) => key + 1)
const handleCloseClick = () => {
setPreviewingEdge(undefined)
setRightPanel(undefined)
}
return ( return (
<Flex <Flex
pos="absolute" pos="absolute"
@ -75,7 +78,7 @@ export const PreviewDrawer = () => {
<VStack w="full" spacing={4}> <VStack w="full" spacing={4}>
<Flex justifyContent={'space-between'} w="full"> <Flex justifyContent={'space-between'} w="full">
<Button onClick={handleRestartClick}>Restart</Button> <Button onClick={handleRestartClick}>Restart</Button>
<CloseButton onClick={() => setRightPanel(undefined)} /> <CloseButton onClick={handleCloseClick} />
</Flex> </Flex>
{publicTypebot && ( {publicTypebot && (
@ -89,7 +92,7 @@ export const PreviewDrawer = () => {
> >
<TypebotViewer <TypebotViewer
typebot={publicTypebot} typebot={publicTypebot}
onNewBlockVisible={handleNewBlockVisible} onNewBlockVisible={setPreviewingEdge}
/> />
</Flex> </Flex>
)} )}

View File

@ -24,7 +24,7 @@ export const SubmissionsTable = ({
}: SubmissionsTableProps) => { }: SubmissionsTableProps) => {
const { publishedTypebot } = useTypebot() const { publishedTypebot } = useTypebot()
const columns: any = useMemo( const columns: any = useMemo(
() => parseSubmissionsColumns(publishedTypebot), () => (publishedTypebot ? parseSubmissionsColumns(publishedTypebot) : []),
[publishedTypebot] [publishedTypebot]
) )

View File

@ -1,4 +1,4 @@
import { Flex, FormLabel, Stack, Switch, Text } from '@chakra-ui/react' import { Flex, FormLabel, Stack, Switch } from '@chakra-ui/react'
import { GeneralSettings } from 'models' import { GeneralSettings } from 'models'
import React from 'react' import React from 'react'

View File

@ -1,4 +1,4 @@
import { Flex, FormLabel, Stack, Switch, Text } from '@chakra-ui/react' import { Flex, FormLabel, Stack, Switch } from '@chakra-ui/react'
import { TypingEmulation } from 'models' import { TypingEmulation } from 'models'
import React from 'react' import React from 'react'
import { isDefined } from 'utils' import { isDefined } from 'utils'

View File

@ -3,7 +3,6 @@ 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 { Target } from 'models'
import React, { useMemo, useState } from 'react' import React, { useMemo, useState } from 'react'
import { import {
computeConnectingEdgePath, computeConnectingEdgePath,
@ -20,28 +19,26 @@ export const DrawingEdge = () => {
targetEndpoints, targetEndpoints,
blocksCoordinates, blocksCoordinates,
} = useGraph() } = useGraph()
const { typebot, createEdge } = useTypebot() const { createEdge } = useTypebot()
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }) const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
const sourceBlock = useMemo( const sourceBlockCoordinates =
() => connectingIds && typebot?.blocks.byId[connectingIds.source.blockId], blocksCoordinates && blocksCoordinates[connectingIds?.source.blockId ?? '']
// eslint-disable-next-line react-hooks/exhaustive-deps const targetBlockCoordinates =
[connectingIds] blocksCoordinates && blocksCoordinates[connectingIds?.target?.blockId ?? '']
)
const sourceTop = useMemo(() => { const sourceTop = useMemo(() => {
if (!sourceBlock || !connectingIds) return 0 if (!connectingIds) return 0
return getEndpointTopOffset( return getEndpointTopOffset(
graphPosition, graphPosition,
sourceEndpoints, sourceEndpoints,
connectingIds.source.buttonId ?? connectingIds.source.itemId ?? connectingIds.source.stepId
connectingIds.source.stepId + (connectingIds.source.conditionType ?? '')
) )
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [graphPosition, sourceEndpoints, connectingIds]) }, [graphPosition, sourceEndpoints, connectingIds])
const targetTop = useMemo(() => { const targetTop = useMemo(() => {
if (!sourceBlock || !connectingIds) return 0 if (!connectingIds) return 0
return getEndpointTopOffset( return getEndpointTopOffset(
graphPosition, graphPosition,
targetEndpoints, targetEndpoints,
@ -51,35 +48,24 @@ export const DrawingEdge = () => {
}, [graphPosition, targetEndpoints, connectingIds]) }, [graphPosition, targetEndpoints, connectingIds])
const path = useMemo(() => { const path = useMemo(() => {
if ( if (!sourceTop || !sourceBlockCoordinates) return ``
!sourceBlock ||
!typebot ||
!connectingIds ||
!blocksCoordinates ||
!sourceTop
)
return ``
return connectingIds?.target return targetBlockCoordinates
? computeConnectingEdgePath({ ? computeConnectingEdgePath({
connectingIds: connectingIds as Omit<ConnectingIds, 'target'> & { sourceBlockCoordinates,
target: Target targetBlockCoordinates,
},
sourceTop, sourceTop,
targetTop, targetTop,
blocksCoordinates,
}) })
: computeEdgePathToMouse({ : computeEdgePathToMouse({
blockPosition: blocksCoordinates.byId[sourceBlock.id], sourceBlockCoordinates,
mousePosition, mousePosition,
sourceTop, sourceTop,
}) })
}, [ }, [
sourceBlock,
typebot,
connectingIds,
blocksCoordinates,
sourceTop, sourceTop,
sourceBlockCoordinates,
targetBlockCoordinates,
targetTop, targetTop,
mousePosition, mousePosition,
]) ])

View File

@ -1,5 +1,4 @@
import { Coordinates, useGraph } from 'contexts/GraphContext' import { Coordinates, useGraph } from 'contexts/GraphContext'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { import {
getAnchorsPosition, getAnchorsPosition,
@ -7,6 +6,7 @@ import {
getEndpointTopOffset, getEndpointTopOffset,
getSourceEndpointId, getSourceEndpointId,
} from 'services/graph' } from 'services/graph'
import { Edge as EdgeProps } from 'models'
export type AnchorsPositionProps = { export type AnchorsPositionProps = {
sourcePosition: Coordinates sourcePosition: Coordinates
@ -15,28 +15,20 @@ export type AnchorsPositionProps = {
totalSegments: number totalSegments: number
} }
export const Edge = ({ edgeId }: { edgeId: string }) => { export const Edge = ({ edge }: { edge: EdgeProps }) => {
const { typebot } = useTypebot()
const { const {
previewingEdgeId, previewingEdge,
sourceEndpoints, sourceEndpoints,
targetEndpoints, targetEndpoints,
graphPosition, graphPosition,
blocksCoordinates, blocksCoordinates,
} = useGraph() } = useGraph()
const edge = useMemo( const isPreviewing = previewingEdge?.id === edge.id
() => typebot?.edges.byId[edgeId],
[edgeId, typebot?.edges.byId]
)
const isPreviewing = previewingEdgeId === edgeId
const sourceBlock = edge && typebot?.blocks.byId[edge.from.blockId]
const targetBlock = edge && typebot?.blocks.byId[edge.to.blockId]
const sourceBlockCoordinates = const sourceBlockCoordinates =
sourceBlock && blocksCoordinates?.byId[sourceBlock.id] blocksCoordinates && blocksCoordinates[edge.from.blockId]
const targetBlockCoordinates = const targetBlockCoordinates =
targetBlock && blocksCoordinates?.byId[targetBlock.id] blocksCoordinates && blocksCoordinates[edge.to.blockId]
const sourceTop = useMemo( const sourceTop = useMemo(
() => () =>
@ -77,6 +69,7 @@ export const Edge = ({ edgeId }: { edgeId: string }) => {
if (sourceTop === 0) return <></> if (sourceTop === 0) return <></>
return ( return (
<path <path
data-testid="edge"
d={path} d={path}
stroke={isPreviewing ? '#1a5fff' : '#718096'} stroke={isPreviewing ? '#1a5fff' : '#718096'}
strokeWidth="2px" strokeWidth="2px"

View File

@ -1,12 +1,13 @@
import { chakra } from '@chakra-ui/system' import { chakra } from '@chakra-ui/system'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { Edge as EdgeProps } from 'models'
import React from 'react' import React from 'react'
import { DrawingEdge } from './DrawingEdge' import { DrawingEdge } from './DrawingEdge'
import { Edge } from './Edge' import { Edge } from './Edge'
export const Edges = () => { type Props = {
const { typebot } = useTypebot() edges: EdgeProps[]
}
export const Edges = ({ edges }: Props) => {
return ( return (
<chakra.svg <chakra.svg
width="full" width="full"
@ -17,8 +18,8 @@ export const Edges = () => {
top="0" top="0"
> >
<DrawingEdge /> <DrawingEdge />
{typebot?.edges.allIds.map((edgeId) => ( {edges.map((edge) => (
<Edge key={edgeId} edgeId={edgeId} /> <Edge key={edge.id} edge={edge} />
))} ))}
<marker <marker
id={'arrow'} id={'arrow'}

View File

@ -19,29 +19,32 @@ export const SourceEndpoint = ({
} }
useEffect(() => { useEffect(() => {
if (ranOnce || !ref.current) return if (ranOnce || !ref.current || Object.keys(blocksCoordinates).length === 0)
const id = source.buttonId ?? source.stepId + (source.conditionType ?? '') return
const id = source.itemId ?? source.stepId
addSourceEndpoint({ addSourceEndpoint({
id, id,
ref, ref,
}) })
setRanOnce(true) setRanOnce(true)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ref.current]) }, [ref.current, blocksCoordinates])
if (!blocksCoordinates) return <></> if (!blocksCoordinates) return <></>
return ( return (
<Flex <Flex
ref={ref} ref={ref}
data-testid="endpoint"
boxSize="18px" boxSize="18px"
rounded="full" rounded="full"
onMouseDown={handleMouseDown} onMouseDownCapture={handleMouseDown}
cursor="copy" cursor="copy"
borderWidth="1px" borderWidth="1px"
borderColor="gray.400" borderColor="gray.400"
bgColor="white" bgColor="white"
justify="center" justify="center"
align="center" align="center"
pointerEvents="all"
{...props} {...props}
> >
<Box bgColor="gray.400" rounded="full" boxSize="6px" /> <Box bgColor="gray.400" rounded="full" boxSize="6px" />

View File

@ -2,7 +2,7 @@ import { Flex, FlexProps, useEventListener } from '@chakra-ui/react'
import React, { useRef, useMemo, useEffect } from 'react' import React, { useRef, useMemo, useEffect } from 'react'
import { blockWidth, useGraph } from 'contexts/GraphContext' import { blockWidth, useGraph } from 'contexts/GraphContext'
import { BlockNode } from './Nodes/BlockNode/BlockNode' import { BlockNode } from './Nodes/BlockNode/BlockNode'
import { useStepDnd } from 'contexts/StepDndContext' import { useStepDnd } from 'contexts/GraphDndContext'
import { Edges } from './Edges' import { Edges } from './Edges'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader' import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
@ -58,6 +58,7 @@ export const Graph = ({
useEventListener('wheel', handleMouseWheel, graphContainerRef.current) useEventListener('wheel', handleMouseWheel, graphContainerRef.current)
const handleMouseUp = (e: MouseEvent) => { const handleMouseUp = (e: MouseEvent) => {
if (!typebot) return
if (!draggedStep && !draggedStepType) return if (!draggedStep && !draggedStepType) return
const coordinates = { const coordinates = {
x: e.clientX - graphPosition.x - blockWidth / 3, x: e.clientX - graphPosition.x - blockWidth / 3,
@ -69,6 +70,7 @@ export const Graph = ({
id, id,
...coordinates, ...coordinates,
step: draggedStep ?? (draggedStepType as DraggableStepType), step: draggedStep ?? (draggedStepType as DraggableStepType),
indices: { blockIndex: typebot.blocks.length, stepIndex: 0 },
}) })
setDraggedStep(undefined) setDraggedStep(undefined)
setDraggedStepType(undefined) setDraggedStepType(undefined)
@ -84,7 +86,6 @@ export const Graph = ({
const handleClick = () => setOpenedStepId(undefined) const handleClick = () => setOpenedStepId(undefined)
useEventListener('click', handleClick, editorContainerRef.current) useEventListener('click', handleClick, editorContainerRef.current)
if (!typebot) return <></>
return ( return (
<Flex ref={graphContainerRef} {...props}> <Flex ref={graphContainerRef} {...props}>
<Flex <Flex
@ -95,9 +96,9 @@ export const Graph = ({
transform, transform,
}} }}
> >
<Edges /> <Edges edges={typebot?.edges ?? []} />
{typebot.blocks.allIds.map((blockId) => ( {typebot?.blocks.map((block, idx) => (
<BlockNode block={typebot.blocks.byId[blockId]} key={blockId} /> <BlockNode block={block} blockIndex={idx} key={block.id} />
))} ))}
{answersCounts?.map((answersCount) => ( {answersCounts?.map((answersCount) => (
<DropOffNode <DropOffNode

View File

@ -5,45 +5,44 @@ import {
Stack, Stack,
useEventListener, useEventListener,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import React, { useEffect, useMemo, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { Block } from 'models' import { Block } from 'models'
import { useGraph } from 'contexts/GraphContext' import { useGraph } from 'contexts/GraphContext'
import { useStepDnd } from 'contexts/StepDndContext' import { useStepDnd } from 'contexts/GraphDndContext'
import { StepNodesList } from '../StepNode/StepNodesList' import { StepNodesList } from '../StepNode/StepNodesList'
import { isNotDefined } from 'utils' import { isNotDefined } from 'utils'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { ContextMenu } from 'components/shared/ContextMenu' import { ContextMenu } from 'components/shared/ContextMenu'
import { BlockNodeContextMenu } from './BlockNodeContextMenu' import { BlockNodeContextMenu } from './BlockNodeContextMenu'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
import { setMultipleRefs } from 'services/utils'
type Props = { type Props = {
block: Block block: Block
blockIndex: number
} }
export const BlockNode = ({ block }: Props) => { export const BlockNode = ({ block, blockIndex }: Props) => {
const { const {
connectingIds, connectingIds,
setConnectingIds, setConnectingIds,
previewingEdgeId, previewingEdge,
blocksCoordinates, blocksCoordinates,
updateBlockCoordinates, updateBlockCoordinates,
isReadOnly, isReadOnly,
} = useGraph() } = useGraph()
const { typebot, updateBlock } = useTypebot() const { typebot, updateBlock } = useTypebot()
const { setMouseOverBlockId } = useStepDnd() const { setMouseOverBlock, mouseOverBlock } = useStepDnd()
const { draggedStep, draggedStepType } = useStepDnd()
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 =
if (!previewingEdgeId) return previewingEdge?.to.blockId === block.id ||
const edge = typebot?.edges.byId[previewingEdgeId] previewingEdge?.from.blockId === block.id
return edge?.to.blockId === block.id || edge?.from.blockId === block.id const isStartBlock =
}, [block.id, previewingEdgeId, typebot?.edges.byId]) block.steps.length === 1 && block.steps[0].type === 'start'
const blockCoordinates = useMemo( const blockCoordinates = blocksCoordinates[block.id]
() => blocksCoordinates?.byId[block.id], const blockRef = useRef<HTMLDivElement | null>(null)
[block.id, blocksCoordinates?.byId]
)
const [debouncedBlockPosition] = useDebounce(blockCoordinates, 100) const [debouncedBlockPosition] = useDebounce(blockCoordinates, 100)
useEffect(() => { useEffect(() => {
if (!debouncedBlockPosition || isReadOnly) return if (!debouncedBlockPosition || isReadOnly) return
@ -52,7 +51,7 @@ export const BlockNode = ({ block }: Props) => {
debouncedBlockPosition.y === block.graphCoordinates.y debouncedBlockPosition.y === block.graphCoordinates.y
) )
return return
updateBlock(block.id, { graphCoordinates: debouncedBlockPosition }) updateBlock(blockIndex, { graphCoordinates: debouncedBlockPosition })
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedBlockPosition]) }, [debouncedBlockPosition])
@ -63,7 +62,8 @@ export const BlockNode = ({ block }: Props) => {
) )
}, [block.id, connectingIds]) }, [block.id, connectingIds])
const handleTitleSubmit = (title: string) => updateBlock(block.id, { title }) const handleTitleSubmit = (title: string) =>
updateBlock(blockIndex, { title })
const handleMouseDown = () => { const handleMouseDown = () => {
setIsMouseDown(true) setIsMouseDown(true)
@ -87,24 +87,26 @@ export const BlockNode = ({ block }: Props) => {
useEventListener('mousemove', handleMouseMove) useEventListener('mousemove', handleMouseMove)
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (draggedStepType || draggedStep) setMouseOverBlockId(block.id) if (mouseOverBlock?.id !== block.id && !isStartBlock)
setMouseOverBlock({ id: block.id, ref: blockRef })
if (connectingIds) if (connectingIds)
setConnectingIds({ ...connectingIds, target: { blockId: block.id } }) setConnectingIds({ ...connectingIds, target: { blockId: block.id } })
} }
const handleMouseLeave = () => { const handleMouseLeave = () => {
setMouseOverBlockId(undefined) setMouseOverBlock(undefined)
if (connectingIds) setConnectingIds({ ...connectingIds, target: undefined }) if (connectingIds) setConnectingIds({ ...connectingIds, target: undefined })
} }
return ( return (
<ContextMenu<HTMLDivElement> <ContextMenu<HTMLDivElement>
renderMenu={() => <BlockNodeContextMenu blockId={block.id} />} renderMenu={() => <BlockNodeContextMenu blockIndex={blockIndex} />}
isDisabled={isReadOnly} isDisabled={isReadOnly}
> >
{(ref, isOpened) => ( {(ref, isOpened) => (
<Stack <Stack
ref={ref} ref={setMultipleRefs([ref, blockRef])}
data-testid="block"
p="4" p="4"
rounded="lg" rounded="lg"
bgColor="blue.50" bgColor="blue.50"
@ -142,7 +144,13 @@ export const BlockNode = ({ block }: Props) => {
<EditableInput minW="0" px="1" /> <EditableInput minW="0" px="1" />
</Editable> </Editable>
{typebot && ( {typebot && (
<StepNodesList blockId={block.id} stepIds={block.stepIds} /> <StepNodesList
blockId={block.id}
steps={block.steps}
blockIndex={blockIndex}
blockRef={ref}
isStartBlock={isStartBlock}
/>
)} )}
</Stack> </Stack>
)} )}

View File

@ -2,10 +2,14 @@ import { MenuList, MenuItem } from '@chakra-ui/react'
import { TrashIcon } from 'assets/icons' import { TrashIcon } from 'assets/icons'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
export const BlockNodeContextMenu = ({ blockId }: { blockId: string }) => { export const BlockNodeContextMenu = ({
blockIndex,
}: {
blockIndex: number
}) => {
const { deleteBlock } = useTypebot() const { deleteBlock } = useTypebot()
const handleDeleteClick = () => deleteBlock(blockId) const handleDeleteClick = () => deleteBlock(blockIndex)
return ( return (
<MenuList> <MenuList>

View File

@ -1,156 +0,0 @@
import {
EditablePreview,
EditableInput,
Editable,
useEventListener,
Flex,
Fade,
IconButton,
} from '@chakra-ui/react'
import { PlusIcon } from 'assets/icons'
import { ContextMenu } from 'components/shared/ContextMenu'
import { Coordinates } from 'contexts/GraphContext'
import { useTypebot } from 'contexts/TypebotContext'
import { ChoiceInputStep, ChoiceItem } from 'models'
import React, { useState } from 'react'
import { isNotDefined, isSingleChoiceInput } from 'utils'
import { SourceEndpoint } from '../../Endpoints/SourceEndpoint'
import { ButtonNodeContextMenu } from './ButtonNodeContextMenu'
type Props = {
item: ChoiceItem
onMouseMoveBottomOfElement?: () => void
onMouseMoveTopOfElement?: () => void
onMouseDown?: (
stepNodePosition: { absolute: Coordinates; relative: Coordinates },
item: ChoiceItem
) => void
}
export const ButtonNode = ({
item,
onMouseDown,
onMouseMoveBottomOfElement,
onMouseMoveTopOfElement,
}: Props) => {
const { deleteChoiceItem, updateChoiceItem, typebot, createChoiceItem } =
useTypebot()
const [mouseDownEvent, setMouseDownEvent] =
useState<{ absolute: Coordinates; relative: Coordinates }>()
const [isMouseOver, setIsMouseOver] = useState(false)
const handleMouseDown = (e: React.MouseEvent) => {
if (!onMouseDown) return
e.stopPropagation()
const element = e.currentTarget as HTMLDivElement
const rect = element.getBoundingClientRect()
const relativeX = e.clientX - rect.left
const relativeY = e.clientY - rect.top
setMouseDownEvent({
absolute: { x: e.clientX + relativeX, y: e.clientY + relativeY },
relative: { x: relativeX, y: relativeY },
})
}
const handleGlobalMouseUp = () => {
setMouseDownEvent(undefined)
}
useEventListener('mouseup', handleGlobalMouseUp)
const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
if (!onMouseMoveBottomOfElement || !onMouseMoveTopOfElement) return
const isMovingAndIsMouseDown =
mouseDownEvent &&
onMouseDown &&
(event.movementX > 0 || event.movementY > 0)
if (isMovingAndIsMouseDown) {
onMouseDown(mouseDownEvent, item)
deleteChoiceItem(item.id)
setMouseDownEvent(undefined)
}
const element = event.currentTarget as HTMLDivElement
const rect = element.getBoundingClientRect()
const y = event.clientY - rect.top
if (y > rect.height / 2) onMouseMoveBottomOfElement()
else onMouseMoveTopOfElement()
}
const handleInputSubmit = (content: string) =>
updateChoiceItem(item.id, { content: content === '' ? undefined : content })
const handlePlusClick = () => {
const nextIndex =
(
typebot?.steps.byId[item.stepId] as ChoiceInputStep
).options.itemIds.indexOf(item.id) + 1
createChoiceItem({ stepId: item.stepId }, nextIndex)
}
const handleMouseEnter = () => setIsMouseOver(true)
const handleMouseLeave = () => setIsMouseOver(false)
return (
<ContextMenu<HTMLDivElement>
renderMenu={() => <ButtonNodeContextMenu itemId={item.id} />}
>
{(ref, isOpened) => (
<Flex
ref={ref}
align="center"
pos="relative"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
justifyContent="center"
shadow="sm"
_hover={{ shadow: 'md' }}
transition="box-shadow 200ms"
borderWidth="1px"
rounded="md"
px="4"
py="2"
borderColor={isOpened ? 'blue.400' : 'gray.300'}
>
<Editable
defaultValue={item.content ?? 'Click to edit'}
flex="1"
startWithEditView={isNotDefined(item.content)}
onSubmit={handleInputSubmit}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
>
<EditablePreview
w="full"
color={item.content !== 'Click to edit' ? 'inherit' : 'gray.500'}
/>
<EditableInput />
</Editable>
{typebot && isSingleChoiceInput(typebot.steps.byId[item.stepId]) && (
<SourceEndpoint
source={{
blockId: typebot.steps.byId[item.stepId].blockId,
stepId: item.stepId,
buttonId: item.id,
}}
pos="absolute"
right="15px"
/>
)}
<Fade
in={isMouseOver}
style={{ position: 'absolute', bottom: '-15px', zIndex: 3 }}
unmountOnExit
>
<IconButton
aria-label="Add item"
icon={<PlusIcon />}
size="xs"
shadow="md"
colorScheme="blue"
onClick={handlePlusClick}
/>
</Fade>
</Flex>
)}
</ContextMenu>
)
}

View File

@ -1,154 +0,0 @@
import { Flex, Portal, Stack, Text, useEventListener } from '@chakra-ui/react'
import { useStepDnd } from 'contexts/StepDndContext'
import { Coordinates } from 'contexts/GraphContext'
import { useTypebot } from 'contexts/TypebotContext'
import { ChoiceInputStep, ChoiceItem } from 'models'
import React, { useMemo, useState } from 'react'
import { ButtonNode } from './ButtonNode'
import { SourceEndpoint } from '../../Endpoints'
import { ButtonNodeOverlay } from './ButtonNodeOverlay'
type ChoiceItemsListProps = {
step: ChoiceInputStep
}
export const ButtonNodesList = ({ step }: ChoiceItemsListProps) => {
const { typebot, createChoiceItem } = useTypebot()
const {
draggedChoiceItem,
mouseOverBlockId,
setDraggedChoiceItem,
setMouseOverBlockId,
} = useStepDnd()
const showSortPlaceholders = useMemo(
() => mouseOverBlockId === step.blockId && draggedChoiceItem,
[draggedChoiceItem, mouseOverBlockId, step.blockId]
)
const [position, setPosition] = useState({
x: 0,
y: 0,
})
const [relativeCoordinates, setRelativeCoordinates] = useState({ x: 0, y: 0 })
const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState<
number | undefined
>()
const handleStepMove = (event: MouseEvent) => {
if (!draggedChoiceItem) return
const { clientX, clientY } = event
setPosition({
...position,
x: clientX - relativeCoordinates.x,
y: clientY - relativeCoordinates.y,
})
}
useEventListener('mousemove', handleStepMove)
const handleMouseUp = (e: MouseEvent) => {
if (!draggedChoiceItem) return
if (expandedPlaceholderIndex !== -1) {
e.stopPropagation()
createChoiceItem(draggedChoiceItem, expandedPlaceholderIndex)
}
setMouseOverBlockId(undefined)
setExpandedPlaceholderIndex(undefined)
setDraggedChoiceItem(undefined)
}
useEventListener('mouseup', handleMouseUp, undefined, { capture: true })
const handleStepMouseDown = (
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
item: ChoiceItem
) => {
setPosition(absolute)
setRelativeCoordinates(relative)
setMouseOverBlockId(typebot?.steps.byId[item.stepId].blockId)
setDraggedChoiceItem(item)
}
const handleMouseOnTopOfStep = (stepIndex: number) => {
if (!draggedChoiceItem) return
setExpandedPlaceholderIndex(stepIndex === 0 ? 0 : stepIndex)
}
const handleMouseOnBottomOfStep = (stepIndex: number) => {
if (!draggedChoiceItem) return
setExpandedPlaceholderIndex(stepIndex + 1)
}
const stopPropagating = (e: React.MouseEvent) => e.stopPropagation()
return (
<Stack flex={1} spacing={1} onClick={stopPropagating}>
<Flex
h={expandedPlaceholderIndex === 0 ? '50px' : '2px'}
bgColor={'gray.300'}
visibility={showSortPlaceholders ? 'visible' : 'hidden'}
rounded="lg"
transition={showSortPlaceholders ? 'height 200ms' : 'none'}
/>
{step.options.itemIds.map((itemId, idx) => (
<Stack key={itemId} spacing={1}>
{typebot?.choiceItems.byId[itemId] && (
<ButtonNode
item={typebot?.choiceItems.byId[itemId]}
onMouseMoveTopOfElement={() => handleMouseOnTopOfStep(idx)}
onMouseMoveBottomOfElement={() => {
handleMouseOnBottomOfStep(idx)
}}
onMouseDown={handleStepMouseDown}
/>
)}
<Flex
h={
showSortPlaceholders && expandedPlaceholderIndex === idx + 1
? '50px'
: '2px'
}
bgColor={'gray.300'}
visibility={showSortPlaceholders ? 'visible' : 'hidden'}
rounded="lg"
transition={showSortPlaceholders ? 'height 200ms' : 'none'}
/>
</Stack>
))}
<Stack>
<Flex
px="4"
py="2"
borderWidth="1px"
borderColor="gray.300"
bgColor="gray.50"
rounded="md"
pos="relative"
align="center"
cursor="not-allowed"
>
<Text color="gray.500">Default</Text>
<SourceEndpoint
source={{
blockId: step.blockId,
stepId: step.id,
}}
pos="absolute"
right="15px"
/>
</Flex>
</Stack>
{draggedChoiceItem && draggedChoiceItem.stepId === step.id && (
<Portal>
<ButtonNodeOverlay
item={draggedChoiceItem}
pos="fixed"
top="0"
left="0"
style={{
transform: `translate(${position.x}px, ${position.y}px) rotate(-2deg)`,
}}
/>
</Portal>
)}
</Stack>
)
}

View File

@ -1 +0,0 @@
export { ButtonNodesList } from './ButtonNodesList'

View File

@ -3,7 +3,7 @@ import { useTypebot } from 'contexts/TypebotContext'
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' import { byId, isDefined } from 'utils'
type Props = { type Props = {
answersCounts: AnswersCount[] answersCounts: AnswersCount[]
@ -21,11 +21,10 @@ export const DropOffNode = ({ 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.edges.allIds const previousBlockIds = typebot.edges
.map((edgeId) => { .map((edge) =>
const edge = typebot.edges.byId[edgeId] edge.to.blockId === blockId ? edge.from.blockId : undefined
return edge.to.blockId === blockId ? edge.from.blockId : undefined )
})
.filter((blockId) => isDefined(blockId)) .filter((blockId) => isDefined(blockId))
const previousTotal = answersCounts const previousTotal = answersCounts
.filter((a) => previousBlockIds.includes(a.blockId)) .filter((a) => previousBlockIds.includes(a.blockId))
@ -41,12 +40,11 @@ export const DropOffNode = ({ answersCounts, blockId }: Props) => {
}, [answersCounts, blockId, totalAnswers, typebot]) }, [answersCounts, blockId, totalAnswers, typebot])
const labelCoordinates = useMemo(() => { const labelCoordinates = useMemo(() => {
if (!typebot) return { x: 0, y: 0 } const sourceBlock = typebot?.blocks.find(byId(blockId))
const sourceBlock = typebot?.blocks.byId[blockId]
if (!sourceBlock) return if (!sourceBlock) return
return computeSourceCoordinates( return computeSourceCoordinates(
sourceBlock?.graphCoordinates, sourceBlock?.graphCoordinates,
sourceBlock?.stepIds.length - 1 sourceBlock?.steps.length - 1
) )
}, [blockId, typebot]) }, [blockId, typebot])

View File

@ -0,0 +1,90 @@
import { Flex } from '@chakra-ui/react'
import { ContextMenu } from 'components/shared/ContextMenu'
import { Coordinates } from 'contexts/GraphContext'
import { NodePosition, useDragDistance } from 'contexts/GraphDndContext'
import { useTypebot } from 'contexts/TypebotContext'
import { ButtonItem, Item, ItemIndices, ItemType } from 'models'
import React, { useRef, useState } from 'react'
import { setMultipleRefs } from 'services/utils'
import { SourceEndpoint } from '../../Endpoints/SourceEndpoint'
import { ItemNodeContent } from './ItemNodeContent'
import { ItemNodeContextMenu } from './ItemNodeContextMenu'
type Props = {
item: Item
indices: ItemIndices
isReadOnly: boolean
isLastItem: boolean
onMouseDown?: (
stepNodePosition: { absolute: Coordinates; relative: Coordinates },
item: ButtonItem
) => void
}
export const ItemNode = ({
item,
indices,
isReadOnly,
isLastItem,
onMouseDown,
}: Props) => {
const { typebot } = useTypebot()
const [isMouseOver, setIsMouseOver] = useState(false)
const itemRef = useRef<HTMLDivElement | null>(null)
const onDrag = (position: NodePosition) => {
if (!onMouseDown || item.type !== ItemType.BUTTON) return
onMouseDown(position, item)
}
useDragDistance({
ref: itemRef,
onDrag,
isDisabled: !onMouseDown || item.type !== ItemType.BUTTON,
})
const handleMouseEnter = () => setIsMouseOver(true)
const handleMouseLeave = () => setIsMouseOver(false)
return (
<ContextMenu<HTMLDivElement>
renderMenu={() => <ItemNodeContextMenu indices={indices} />}
>
{(ref, isOpened) => (
<Flex
data-testid="item"
ref={setMultipleRefs([ref, itemRef])}
align="center"
pos="relative"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
shadow="sm"
_hover={isReadOnly ? {} : { shadow: 'md' }}
transition="box-shadow 200ms"
borderWidth="1px"
rounded="md"
borderColor={isOpened ? 'blue.400' : 'gray.300'}
pointerEvents={isReadOnly ? 'none' : 'all'}
w="full"
>
<ItemNodeContent
item={item}
isMouseOver={isMouseOver}
indices={indices}
isLastItem={isLastItem}
/>
{typebot && (
<SourceEndpoint
source={{
blockId: typebot.blocks[indices.blockIndex].id,
stepId: item.stepId,
itemId: item.id,
}}
pos="absolute"
right="15px"
pointerEvents="all"
/>
)}
</Flex>
)}
</ContextMenu>
)
}

View File

@ -0,0 +1,32 @@
import { Item, ItemIndices, ItemType } from 'models'
import React from 'react'
import { ButtonNodeContent } from './contents/ButtonNodeContent'
import { ConditionNodeContent } from './contents/ConditionNodeContent'
type Props = {
item: Item
indices: ItemIndices
isMouseOver: boolean
isLastItem: boolean
}
export const ItemNodeContent = ({
item,
indices,
isMouseOver,
isLastItem,
}: Props) => {
switch (item.type) {
case ItemType.BUTTON:
return (
<ButtonNodeContent
item={item}
isMouseOver={isMouseOver}
indices={indices}
isLastItem={isLastItem}
/>
)
case ItemType.CONDITION:
return <ConditionNodeContent item={item} />
}
}

View File

@ -0,0 +1,92 @@
import {
EditablePreview,
EditableInput,
Editable,
Fade,
IconButton,
Flex,
} from '@chakra-ui/react'
import { PlusIcon } from 'assets/icons'
import { useTypebot } from 'contexts/TypebotContext'
import { ButtonItem, ItemIndices, ItemType } from 'models'
import React, { useRef, useState } from 'react'
import { isNotDefined } from 'utils'
type Props = {
item: ButtonItem
indices: ItemIndices
isLastItem: boolean
isMouseOver: boolean
}
export const ButtonNodeContent = ({
item,
indices,
isMouseOver,
isLastItem,
}: Props) => {
const { deleteItem, updateItem, createItem } = useTypebot()
const [initialContent] = useState(item.content ?? '')
const [itemValue, setItemValue] = useState(item.content ?? 'Click to edit')
const editableRef = useRef<HTMLDivElement | null>(null)
const handleInputSubmit = () => {
if (itemValue === '') deleteItem(indices)
else
updateItem(indices, { content: itemValue === '' ? undefined : itemValue })
}
const handleKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Escape' && itemValue === 'Click to edit') deleteItem(indices)
if (
e.key === 'Enter' &&
itemValue !== '' &&
isLastItem &&
initialContent === ''
)
handlePlusClick()
}
const handlePlusClick = () => {
const itemIndex = indices.itemIndex + 1
createItem(
{ stepId: item.stepId, type: ItemType.BUTTON },
{ ...indices, itemIndex }
)
}
return (
<Flex px={4} py={2} justify="center" w="full">
<Editable
ref={editableRef}
flex="1"
startWithEditView={isNotDefined(item.content)}
value={itemValue}
onChange={setItemValue}
onSubmit={handleInputSubmit}
onKeyDownCapture={handleKeyPress}
>
<EditablePreview
w="full"
color={item.content !== 'Click to edit' ? 'inherit' : 'gray.500'}
cursor="pointer"
/>
<EditableInput />
</Editable>
<Fade
in={isMouseOver}
style={{ position: 'absolute', bottom: '-15px', zIndex: 3 }}
unmountOnExit
>
<IconButton
aria-label="Add item"
icon={<PlusIcon />}
size="xs"
shadow="md"
colorScheme="blue"
onClick={handlePlusClick}
/>
</Fade>
</Flex>
)
}

View File

@ -0,0 +1,73 @@
import { Stack, Tag, Text, Flex, Wrap } from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext'
import { Comparison, ConditionItem, ComparisonOperators } from 'models'
import React from 'react'
import { byId, isNotDefined } from 'utils'
type Props = {
item: ConditionItem
}
export const ConditionNodeContent = ({ item }: Props) => {
const { typebot } = useTypebot()
return (
<Flex px={2} py={2}>
{item.content.comparisons.length === 0 ||
comparisonIsEmpty(item.content.comparisons[0]) ? (
<Text color={'gray.500'}>Configure...</Text>
) : (
<Stack maxW="170px">
{item.content.comparisons.map((comparison, idx) => {
const variable = typebot?.variables.find(
byId(comparison.variableId)
)
return (
<Wrap key={comparison.id} spacing={1} isTruncated>
{idx > 0 && <Text>{item.content.logicalOperator ?? ''}</Text>}
{variable?.name && (
<Tag bgColor="orange.400" color="white">
{variable.name}
</Tag>
)}
{comparison.comparisonOperator && (
<Text>
{parseComparisonOperatorSymbol(
comparison.comparisonOperator
)}
</Text>
)}
{comparison?.value && (
<Tag bgColor={'gray.200'}>
<Text isTruncated>{comparison.value}</Text>
</Tag>
)}
</Wrap>
)
})}
</Stack>
)}
</Flex>
)
}
const comparisonIsEmpty = (comparison: Comparison) =>
isNotDefined(comparison.comparisonOperator) &&
isNotDefined(comparison.value) &&
isNotDefined(comparison.variableId)
const parseComparisonOperatorSymbol = (operator: ComparisonOperators) => {
switch (operator) {
case ComparisonOperators.CONTAINS:
return 'contains'
case ComparisonOperators.EQUAL:
return '='
case ComparisonOperators.GREATER:
return '>'
case ComparisonOperators.IS_SET:
return 'is set'
case ComparisonOperators.LESS:
return '<'
case ComparisonOperators.NOT_EQUAL:
return '!='
}
}

View File

@ -0,0 +1 @@
export { ItemNodeContent } from './ItemNodeContent'

View File

@ -1,11 +1,15 @@
import { MenuList, MenuItem } from '@chakra-ui/react' import { MenuList, MenuItem } from '@chakra-ui/react'
import { TrashIcon } from 'assets/icons' import { TrashIcon } from 'assets/icons'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { ItemIndices } from 'models'
export const ButtonNodeContextMenu = ({ itemId }: { itemId: string }) => { type Props = {
const { deleteChoiceItem } = useTypebot() indices: ItemIndices
}
export const ItemNodeContextMenu = ({ indices }: Props) => {
const { deleteItem } = useTypebot()
const handleDeleteClick = () => deleteChoiceItem(itemId) const handleDeleteClick = () => deleteItem(indices)
return ( return (
<MenuList> <MenuList>

View File

@ -1,12 +1,12 @@
import { Flex, FlexProps } from '@chakra-ui/react' import { Flex, FlexProps } from '@chakra-ui/react'
import { ChoiceItem } from 'models' import { Item } from 'models'
import React from 'react' import React from 'react'
type Props = { type Props = {
item: ChoiceItem item: Item
} & FlexProps } & FlexProps
export const ButtonNodeOverlay = ({ item, ...props }: Props) => { export const ItemNodeOverlay = ({ item, ...props }: Props) => {
return ( return (
<Flex <Flex
px="4" px="4"

View File

@ -0,0 +1,189 @@
import { Flex, Portal, Stack, Text, useEventListener } from '@chakra-ui/react'
import {
computeNearestPlaceholderIndex,
useStepDnd,
} from 'contexts/GraphDndContext'
import { Coordinates } from 'contexts/GraphContext'
import { useTypebot } from 'contexts/TypebotContext'
import { ButtonItem, StepIndices, StepWithItems } from 'models'
import React, { useEffect, useRef, useState } from 'react'
import { ItemNode } from './ItemNode'
import { SourceEndpoint } from '../../Endpoints'
import { ItemNodeOverlay } from './ItemNodeOverlay'
type Props = {
step: StepWithItems
indices: StepIndices
isReadOnly?: boolean
}
export const ItemNodesList = ({
step,
indices: { blockIndex, stepIndex },
isReadOnly = false,
}: Props) => {
const { typebot, createItem, deleteItem } = useTypebot()
const { draggedItem, setDraggedItem, mouseOverBlock } = useStepDnd()
const placeholderRefs = useRef<HTMLDivElement[]>([])
const blockId = typebot?.blocks[blockIndex].id
const isDraggingOnCurrentBlock =
(draggedItem && mouseOverBlock?.id === blockId) ?? false
const showPlaceholders = draggedItem && !isReadOnly
const [position, setPosition] = useState({
x: 0,
y: 0,
})
const [relativeCoordinates, setRelativeCoordinates] = useState({ x: 0, y: 0 })
const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState<
number | undefined
>()
const handleGlobalMouseMove = (event: MouseEvent) => {
if (!draggedItem || draggedItem.stepId !== step.id) return
const { clientX, clientY } = event
setPosition({
...position,
x: clientX - relativeCoordinates.x,
y: clientY - relativeCoordinates.y,
})
}
useEventListener('mousemove', handleGlobalMouseMove)
useEffect(() => {
if (mouseOverBlock?.id !== step.blockId)
setExpandedPlaceholderIndex(undefined)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mouseOverBlock?.id])
const handleMouseMoveOnBlock = (event: MouseEvent) => {
if (!isDraggingOnCurrentBlock || isReadOnly) return
const index = computeNearestPlaceholderIndex(event.pageY, placeholderRefs)
setExpandedPlaceholderIndex(index)
}
useEventListener(
'mousemove',
handleMouseMoveOnBlock,
mouseOverBlock?.ref.current
)
const handleMouseUpOnBlock = (e: MouseEvent) => {
setExpandedPlaceholderIndex(undefined)
if (!isDraggingOnCurrentBlock) return
const itemIndex = computeNearestPlaceholderIndex(e.pageY, placeholderRefs)
e.stopPropagation()
createItem(draggedItem as ButtonItem, {
blockIndex,
stepIndex,
itemIndex,
})
setDraggedItem(undefined)
}
useEventListener(
'mouseup',
handleMouseUpOnBlock,
mouseOverBlock?.ref.current,
{
capture: true,
}
)
const handleStepMouseDown =
(itemIndex: number) =>
(
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
item: ButtonItem
) => {
if (!typebot || isReadOnly) return
deleteItem({ blockIndex, stepIndex, itemIndex })
setPosition(absolute)
setRelativeCoordinates(relative)
setDraggedItem(item)
}
const stopPropagating = (e: React.MouseEvent) => e.stopPropagation()
const handlePushElementRef =
(idx: number) => (elem: HTMLDivElement | null) => {
elem && (placeholderRefs.current[idx] = elem)
}
return (
<Stack
flex={1}
spacing={1}
maxW="full"
onClick={stopPropagating}
pointerEvents={isReadOnly ? 'none' : 'all'}
>
<Flex
ref={handlePushElementRef(0)}
h={showPlaceholders && expandedPlaceholderIndex === 0 ? '50px' : '2px'}
bgColor={'gray.300'}
visibility={showPlaceholders ? 'visible' : 'hidden'}
rounded="lg"
transition={showPlaceholders ? 'height 200ms' : 'none'}
/>
{step.items.map((item, idx) => (
<Stack key={item.id} spacing={1}>
<ItemNode
item={item}
indices={{ blockIndex, stepIndex, itemIndex: idx }}
onMouseDown={handleStepMouseDown(idx)}
isReadOnly={isReadOnly}
isLastItem={idx === step.items.length - 1}
/>
<Flex
ref={handlePushElementRef(idx + 1)}
h={
showPlaceholders && expandedPlaceholderIndex === idx + 1
? '50px'
: '2px'
}
bgColor={'gray.300'}
visibility={showPlaceholders ? 'visible' : 'hidden'}
rounded="lg"
transition={showPlaceholders ? 'height 200ms' : 'none'}
/>
</Stack>
))}
<Stack>
<Flex
px="4"
py="2"
borderWidth="1px"
borderColor="gray.300"
bgColor={isReadOnly ? '' : 'gray.50'}
rounded="md"
pos="relative"
align="center"
cursor={isReadOnly ? 'pointer' : 'not-allowed'}
>
<Text color={isReadOnly ? 'inherit' : 'gray.500'}>Default</Text>
<SourceEndpoint
source={{
blockId: step.blockId,
stepId: step.id,
}}
pos="absolute"
right="15px"
/>
</Flex>
</Stack>
{draggedItem && draggedItem.stepId === step.id && (
<Portal>
<ItemNodeOverlay
item={draggedItem}
pos="fixed"
top="0"
left="0"
style={{
transform: `translate(${position.x}px, ${position.y}px) rotate(-2deg)`,
}}
/>
</Portal>
)}
</Stack>
)
}

View File

@ -0,0 +1 @@
export { ItemNodesList } from './ItemNodesList'

View File

@ -8,13 +8,17 @@ import {
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { ExpandIcon } from 'assets/icons' import { ExpandIcon } from 'assets/icons'
import { import {
ConditionItem,
ConditionStep,
InputStepType, InputStepType,
IntegrationStepType, IntegrationStepType,
LogicStepType, LogicStepType,
Step, Step,
StepIndices,
StepOptions, StepOptions,
TextBubbleStep, TextBubbleStep,
Webhook, Webhook,
WebhookStep,
} from 'models' } from 'models'
import { useRef } from 'react' import { useRef } from 'react'
import { import {
@ -37,9 +41,9 @@ type Props = {
step: Exclude<Step, TextBubbleStep> step: Exclude<Step, TextBubbleStep>
webhook?: Webhook webhook?: Webhook
onExpandClick: () => void onExpandClick: () => void
onOptionsChange: (options: StepOptions) => void onStepChange: (updates: Partial<Step>) => void
onWebhookChange: (updates: Partial<Webhook>) => void
onTestRequestClick: () => void onTestRequestClick: () => void
indices: StepIndices
} }
export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => { export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
@ -79,23 +83,35 @@ export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
export const StepSettings = ({ export const StepSettings = ({
step, step,
webhook, onStepChange,
onOptionsChange,
onWebhookChange,
onTestRequestClick, onTestRequestClick,
indices,
}: { }: {
step: Step step: Step
webhook?: Webhook webhook?: Webhook
onOptionsChange: (options: StepOptions) => void onStepChange: (step: Partial<Step>) => void
onWebhookChange: (updates: Partial<Webhook>) => void
onTestRequestClick: () => void onTestRequestClick: () => void
indices: StepIndices
}) => { }) => {
const handleOptionsChange = (options: StepOptions) => {
onStepChange({ options } as Partial<Step>)
}
const handleWebhookChange = (updates: Partial<Webhook>) => {
onStepChange({
webhook: { ...(step as WebhookStep).webhook, ...updates },
} as Partial<Step>)
}
const handleItemChange = (updates: Partial<ConditionItem>) => {
onStepChange({
items: [{ ...(step as ConditionStep).items[0], ...updates }],
} as Partial<Step>)
}
switch (step.type) { switch (step.type) {
case InputStepType.TEXT: { case InputStepType.TEXT: {
return ( return (
<TextInputSettingsBody <TextInputSettingsBody
options={step.options} options={step.options}
onOptionsChange={onOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
@ -103,7 +119,7 @@ export const StepSettings = ({
return ( return (
<NumberInputSettingsBody <NumberInputSettingsBody
options={step.options} options={step.options}
onOptionsChange={onOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
@ -111,7 +127,7 @@ export const StepSettings = ({
return ( return (
<EmailInputSettingsBody <EmailInputSettingsBody
options={step.options} options={step.options}
onOptionsChange={onOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
@ -119,7 +135,7 @@ export const StepSettings = ({
return ( return (
<UrlInputSettingsBody <UrlInputSettingsBody
options={step.options} options={step.options}
onOptionsChange={onOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
@ -127,7 +143,7 @@ export const StepSettings = ({
return ( return (
<DateInputSettingsBody <DateInputSettingsBody
options={step.options} options={step.options}
onOptionsChange={onOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
@ -135,7 +151,7 @@ export const StepSettings = ({
return ( return (
<PhoneNumberSettingsBody <PhoneNumberSettingsBody
options={step.options} options={step.options}
onOptionsChange={onOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
@ -143,7 +159,7 @@ export const StepSettings = ({
return ( return (
<ChoiceInputSettingsBody <ChoiceInputSettingsBody
options={step.options} options={step.options}
onOptionsChange={onOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
@ -151,23 +167,20 @@ export const StepSettings = ({
return ( return (
<SetVariableSettings <SetVariableSettings
options={step.options} options={step.options}
onOptionsChange={onOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
case LogicStepType.CONDITION: { case LogicStepType.CONDITION: {
return ( return (
<ConditionSettingsBody <ConditionSettingsBody step={step} onItemChange={handleItemChange} />
options={step.options}
onOptionsChange={onOptionsChange}
/>
) )
} }
case LogicStepType.REDIRECT: { case LogicStepType.REDIRECT: {
return ( return (
<RedirectSettings <RedirectSettings
options={step.options} options={step.options}
onOptionsChange={onOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
@ -175,7 +188,7 @@ export const StepSettings = ({
return ( return (
<GoogleSheetsSettingsBody <GoogleSheetsSettingsBody
options={step.options} options={step.options}
onOptionsChange={onOptionsChange} onOptionsChange={handleOptionsChange}
stepId={step.id} stepId={step.id}
/> />
) )
@ -184,18 +197,18 @@ export const StepSettings = ({
return ( return (
<GoogleAnalyticsSettings <GoogleAnalyticsSettings
options={step.options} options={step.options}
onOptionsChange={onOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
case IntegrationStepType.WEBHOOK: { case IntegrationStepType.WEBHOOK: {
return ( return (
<WebhookSettings <WebhookSettings
options={step.options} step={step}
webhook={webhook as Webhook} onOptionsChange={handleOptionsChange}
onOptionsChange={onOptionsChange} onWebhookChange={handleWebhookChange}
onWebhookChange={onWebhookChange}
onTestRequestClick={onTestRequestClick} onTestRequestClick={onTestRequestClick}
indices={indices}
/> />
) )
} }

View File

@ -1,33 +1,40 @@
import { Flex } from '@chakra-ui/react' import { Flex } from '@chakra-ui/react'
import { DropdownList } from 'components/shared/DropdownList' import { DropdownList } from 'components/shared/DropdownList'
import { TableList } from 'components/shared/TableList' import { TableList } from 'components/shared/TableList'
import { Comparison, ConditionOptions, LogicalOperator, Table } from 'models' import {
Comparison,
ConditionItem,
ConditionStep,
LogicalOperator,
} from 'models'
import React from 'react' import React from 'react'
import { ComparisonItem } from './ComparisonsItem' import { ComparisonItem } from './ComparisonsItem'
type ConditionSettingsBodyProps = { type ConditionSettingsBodyProps = {
options: ConditionOptions step: ConditionStep
onOptionsChange: (options: ConditionOptions) => void onItemChange: (updates: Partial<ConditionItem>) => void
} }
export const ConditionSettingsBody = ({ export const ConditionSettingsBody = ({
options, step,
onOptionsChange, onItemChange,
}: ConditionSettingsBodyProps) => { }: ConditionSettingsBodyProps) => {
const handleComparisonsChange = (comparisons: Table<Comparison>) => const itemContent = step.items[0].content
onOptionsChange({ ...options, comparisons })
const handleComparisonsChange = (comparisons: Comparison[]) =>
onItemChange({ content: { ...itemContent, comparisons } })
const handleLogicalOperatorChange = (logicalOperator: LogicalOperator) => const handleLogicalOperatorChange = (logicalOperator: LogicalOperator) =>
onOptionsChange({ ...options, logicalOperator }) onItemChange({ content: { ...itemContent, logicalOperator } })
return ( return (
<TableList<Comparison> <TableList<Comparison>
initialItems={options.comparisons} initialItems={itemContent.comparisons}
onItemsChange={handleComparisonsChange} onItemsChange={handleComparisonsChange}
Item={ComparisonItem} Item={ComparisonItem}
ComponentBetweenItems={() => ( ComponentBetweenItems={() => (
<Flex justify="center"> <Flex justify="center">
<DropdownList<LogicalOperator> <DropdownList<LogicalOperator>
currentItem={options.logicalOperator} currentItem={itemContent.logicalOperator}
onItemSelect={handleLogicalOperatorChange} onItemSelect={handleLogicalOperatorChange}
items={Object.values(LogicalOperator)} items={Object.values(LogicalOperator)}
/> />

View File

@ -6,14 +6,12 @@ import { useTypebot } from 'contexts/TypebotContext'
import { CredentialsType } from 'db' import { CredentialsType } from 'db'
import { import {
Cell, Cell,
defaultTable,
ExtractingCell, ExtractingCell,
GoogleSheetsAction, GoogleSheetsAction,
GoogleSheetsGetOptions, GoogleSheetsGetOptions,
GoogleSheetsInsertRowOptions, GoogleSheetsInsertRowOptions,
GoogleSheetsOptions, GoogleSheetsOptions,
GoogleSheetsUpdateRowOptions, GoogleSheetsUpdateRowOptions,
Table,
} from 'models' } from 'models'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { import {
@ -60,7 +58,7 @@ export const GoogleSheetsSettingsBody = ({
const newOptions: GoogleSheetsGetOptions = { const newOptions: GoogleSheetsGetOptions = {
...options, ...options,
action, action,
cellsToExtract: defaultTable, cellsToExtract: [],
} }
return onOptionsChange({ ...newOptions }) return onOptionsChange({ ...newOptions })
} }
@ -68,7 +66,7 @@ export const GoogleSheetsSettingsBody = ({
const newOptions: GoogleSheetsInsertRowOptions = { const newOptions: GoogleSheetsInsertRowOptions = {
...options, ...options,
action, action,
cellsToInsert: defaultTable, cellsToInsert: [],
} }
return onOptionsChange({ ...newOptions }) return onOptionsChange({ ...newOptions })
} }
@ -76,7 +74,7 @@ export const GoogleSheetsSettingsBody = ({
const newOptions: GoogleSheetsUpdateRowOptions = { const newOptions: GoogleSheetsUpdateRowOptions = {
...options, ...options,
action, action,
cellsToUpsert: defaultTable, cellsToUpsert: [],
} }
return onOptionsChange({ ...newOptions }) return onOptionsChange({ ...newOptions })
} }
@ -155,16 +153,16 @@ const ActionOptions = ({
sheet: Sheet sheet: Sheet
onOptionsChange: (options: GoogleSheetsOptions) => void onOptionsChange: (options: GoogleSheetsOptions) => void
}) => { }) => {
const handleInsertColumnsChange = (cellsToInsert: Table<Cell>) => const handleInsertColumnsChange = (cellsToInsert: Cell[]) =>
onOptionsChange({ ...options, cellsToInsert } as GoogleSheetsOptions) onOptionsChange({ ...options, cellsToInsert } as GoogleSheetsOptions)
const handleUpsertColumnsChange = (cellsToUpsert: Table<Cell>) => const handleUpsertColumnsChange = (cellsToUpsert: Cell[]) =>
onOptionsChange({ ...options, cellsToUpsert } as GoogleSheetsOptions) onOptionsChange({ ...options, cellsToUpsert } as GoogleSheetsOptions)
const handleReferenceCellChange = (referenceCell: Cell) => const handleReferenceCellChange = (referenceCell: Cell) =>
onOptionsChange({ ...options, referenceCell } as GoogleSheetsOptions) onOptionsChange({ ...options, referenceCell } as GoogleSheetsOptions)
const handleExtractingCellsChange = (cellsToExtract: Table<ExtractingCell>) => const handleExtractingCellsChange = (cellsToExtract: ExtractingCell[]) =>
onOptionsChange({ ...options, cellsToExtract } as GoogleSheetsOptions) onOptionsChange({ ...options, cellsToExtract } as GoogleSheetsOptions)
const UpdatingCellItem = useMemo( const UpdatingCellItem = useMemo(
@ -194,9 +192,8 @@ const ActionOptions = ({
<Stack> <Stack>
<Text>Row to select</Text> <Text>Row to select</Text>
<CellWithValueStack <CellWithValueStack
id={'reference'}
columns={sheet.columns} columns={sheet.columns}
item={options.referenceCell ?? {}} item={options.referenceCell ?? { id: 'reference' }}
onItemChange={handleReferenceCellChange} onItemChange={handleReferenceCellChange}
/> />
<Text>Cells to update</Text> <Text>Cells to update</Text>
@ -213,9 +210,8 @@ const ActionOptions = ({
<Stack> <Stack>
<Text>Row to select</Text> <Text>Row to select</Text>
<CellWithValueStack <CellWithValueStack
id={'reference'}
columns={sheet.columns} columns={sheet.columns}
item={options.referenceCell ?? {}} item={options.referenceCell ?? { id: 'reference' }}
onItemChange={handleReferenceCellChange} onItemChange={handleReferenceCellChange}
/> />
<Text>Cells to extract</Text> <Text>Cells to extract</Text>

View File

@ -1,5 +1,4 @@
import { FormLabel, Stack } from '@chakra-ui/react' import { FormLabel, Stack } from '@chakra-ui/react'
import { DebouncedInput } from 'components/shared/DebouncedInput'
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel' import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
import { InputWithVariableButton } from 'components/shared/TextboxWithVariableButton' import { InputWithVariableButton } from 'components/shared/TextboxWithVariableButton'
import { RedirectOptions } from 'models' import { RedirectOptions } from 'models'

View File

@ -20,7 +20,6 @@ export const HeadersInputs = (props: TableListItemProps<KeyValue>) => (
) )
export const KeyValueInputs = ({ export const KeyValueInputs = ({
id,
item, item,
onItemChange, onItemChange,
keyPlaceholder, keyPlaceholder,
@ -40,18 +39,18 @@ export const KeyValueInputs = ({
return ( return (
<Stack p="4" rounded="md" flex="1" borderWidth="1px"> <Stack p="4" rounded="md" flex="1" borderWidth="1px">
<FormControl> <FormControl>
<FormLabel htmlFor={'key' + id}>Key:</FormLabel> <FormLabel htmlFor={'key' + item.id}>Key:</FormLabel>
<InputWithVariableButton <InputWithVariableButton
id={'key' + id} id={'key' + item.id}
initialValue={item.key ?? ''} initialValue={item.key ?? ''}
onChange={handleKeyChange} onChange={handleKeyChange}
placeholder={keyPlaceholder} placeholder={keyPlaceholder}
/> />
</FormControl> </FormControl>
<FormControl> <FormControl>
<FormLabel htmlFor={'value' + id}>Value:</FormLabel> <FormLabel htmlFor={'value' + item.id}>Value:</FormLabel>
<InputWithVariableButton <InputWithVariableButton
id={'value' + id} id={'value' + item.id}
initialValue={item.value ?? ''} initialValue={item.value ?? ''}
onChange={handleValueChange} onChange={handleValueChange}
placeholder={valuePlaceholder} placeholder={valuePlaceholder}

View File

@ -5,7 +5,6 @@ import { VariableSearchInput } from 'components/shared/VariableSearchInput'
import { VariableForTest, Variable } from 'models' import { VariableForTest, Variable } from 'models'
export const VariableForTestInputs = ({ export const VariableForTestInputs = ({
id,
item, item,
onItemChange, onItemChange,
}: TableListItemProps<VariableForTest>) => { }: TableListItemProps<VariableForTest>) => {
@ -18,17 +17,17 @@ export const VariableForTestInputs = ({
return ( return (
<Stack p="4" rounded="md" flex="1" borderWidth="1px"> <Stack p="4" rounded="md" flex="1" borderWidth="1px">
<FormControl> <FormControl>
<FormLabel htmlFor={'name' + id}>Variable name:</FormLabel> <FormLabel htmlFor={'name' + item.id}>Variable name:</FormLabel>
<VariableSearchInput <VariableSearchInput
id={'name' + id} id={'name' + item.id}
initialVariableId={item.variableId} initialVariableId={item.variableId}
onSelectVariable={handleVariableSelect} onSelectVariable={handleVariableSelect}
/> />
</FormControl> </FormControl>
<FormControl> <FormControl>
<FormLabel htmlFor={'value' + id}>Test value:</FormLabel> <FormLabel htmlFor={'value' + item.id}>Test value:</FormLabel>
<DebouncedInput <DebouncedInput
id={'value' + id} id={'value' + item.id}
initialValue={item.value ?? ''} initialValue={item.value ?? ''}
onChange={handleValueChange} onChange={handleValueChange}
/> />

View File

@ -15,11 +15,12 @@ import { useTypebot } from 'contexts/TypebotContext'
import { import {
HttpMethod, HttpMethod,
KeyValue, KeyValue,
Table,
WebhookOptions, WebhookOptions,
VariableForTest, VariableForTest,
Webhook, Webhook,
ResponseVariableMapping, ResponseVariableMapping,
WebhookStep,
StepIndices,
} from 'models' } from 'models'
import { DropdownList } from 'components/shared/DropdownList' import { DropdownList } from 'components/shared/DropdownList'
import { TableList, TableListItemProps } from 'components/shared/TableList' import { TableList, TableListItemProps } from 'components/shared/TableList'
@ -34,19 +35,19 @@ import { VariableForTestInputs } from './VariableForTestInputs'
import { DataVariableInputs } from './ResponseMappingInputs' import { DataVariableInputs } from './ResponseMappingInputs'
type Props = { type Props = {
webhook: Webhook step: WebhookStep
options?: WebhookOptions
onOptionsChange: (options: WebhookOptions) => void onOptionsChange: (options: WebhookOptions) => void
onWebhookChange: (updates: Partial<Webhook>) => void onWebhookChange: (updates: Partial<Webhook>) => void
onTestRequestClick: () => void onTestRequestClick: () => void
indices: StepIndices
} }
export const WebhookSettings = ({ export const WebhookSettings = ({
options, step: { webhook, options },
onOptionsChange, onOptionsChange,
webhook,
onWebhookChange, onWebhookChange,
onTestRequestClick, onTestRequestClick,
indices,
}: Props) => { }: Props) => {
const { typebot, save } = useTypebot() const { typebot, save } = useTypebot()
const [isTestResponseLoading, setIsTestResponseLoading] = useState(false) const [isTestResponseLoading, setIsTestResponseLoading] = useState(false)
@ -62,23 +63,23 @@ export const WebhookSettings = ({
const handleMethodChange = (method: HttpMethod) => onWebhookChange({ method }) const handleMethodChange = (method: HttpMethod) => onWebhookChange({ method })
const handleQueryParamsChange = (queryParams: Table<KeyValue>) => const handleQueryParamsChange = (queryParams: KeyValue[]) =>
onWebhookChange({ queryParams }) onWebhookChange({ queryParams })
const handleHeadersChange = (headers: Table<KeyValue>) => const handleHeadersChange = (headers: KeyValue[]) =>
onWebhookChange({ headers }) onWebhookChange({ headers })
const handleBodyChange = (body: string) => onWebhookChange({ body }) const handleBodyChange = (body: string) => onWebhookChange({ body })
const handleVariablesChange = (variablesForTest: Table<VariableForTest>) => const handleVariablesChange = (variablesForTest: VariableForTest[]) =>
options && onOptionsChange({ ...options, variablesForTest }) onOptionsChange({ ...options, variablesForTest })
const handleResponseMappingChange = ( const handleResponseMappingChange = (
responseVariableMapping: Table<ResponseVariableMapping> responseVariableMapping: ResponseVariableMapping[]
) => options && onOptionsChange({ ...options, responseVariableMapping }) ) => onOptionsChange({ ...options, responseVariableMapping })
const handleTestRequestClick = async () => { const handleTestRequestClick = async () => {
if (!typebot || !webhook) return if (!typebot) return
setIsTestResponseLoading(true) setIsTestResponseLoading(true)
onTestRequestClick() onTestRequestClick()
await save() await save()
@ -86,9 +87,10 @@ export const WebhookSettings = ({
typebot.id, typebot.id,
webhook.id, webhook.id,
convertVariableForTestToVariables( convertVariableForTestToVariables(
options?.variablesForTest, options.variablesForTest,
typebot.variables typebot.variables
) ),
indices
) )
if (error) return toast({ title: error.name, description: error.message }) if (error) return toast({ title: error.name, description: error.message })
setTestResponse(JSON.stringify(data, undefined, 2)) setTestResponse(JSON.stringify(data, undefined, 2))
@ -196,9 +198,7 @@ export const WebhookSettings = ({
</AccordionButton> </AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="6"> <AccordionPanel pb={4} as={Stack} spacing="6">
<TableList<ResponseVariableMapping> <TableList<ResponseVariableMapping>
initialItems={ initialItems={options.responseVariableMapping}
options?.responseVariableMapping ?? { byId: {}, allIds: [] }
}
onItemsChange={handleResponseMappingChange} onItemsChange={handleResponseMappingChange}
Item={ResponseMappingInputs} Item={ResponseMappingInputs}
addLabel="Add an entry" addLabel="Add an entry"

View File

@ -4,21 +4,19 @@ import {
Popover, Popover,
PopoverTrigger, PopoverTrigger,
useDisclosure, useDisclosure,
useEventListener,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import React, { useEffect, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { import {
BubbleStep, BubbleStep,
BubbleStepContent, BubbleStepContent,
DraggableStep, DraggableStep,
Step, Step,
StepOptions, TextBubbleContent,
TextBubbleStep, TextBubbleStep,
Webhook,
} from 'models' } from 'models'
import { Coordinates, useGraph } from 'contexts/GraphContext' import { useGraph } from 'contexts/GraphContext'
import { StepIcon } from 'components/editor/StepsSideBar/StepIcon' import { StepIcon } from 'components/editor/StepsSideBar/StepIcon'
import { isBubbleStep, isTextBubbleStep, isWebhookStep } from 'utils' import { isBubbleStep, isTextBubbleStep, stepHasItems } from 'utils'
import { StepNodeContent } from './StepNodeContent/StepNodeContent' import { StepNodeContent } from './StepNodeContent/StepNodeContent'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { ContextMenu } from 'components/shared/ContextMenu' import { ContextMenu } from 'components/shared/ContextMenu'
@ -32,46 +30,41 @@ import { StepSettings } from './SettingsPopoverContent/SettingsPopoverContent'
import { TextBubbleEditor } from './TextBubbleEditor' import { TextBubbleEditor } from './TextBubbleEditor'
import { TargetEndpoint } from '../../Endpoints' import { TargetEndpoint } from '../../Endpoints'
import { MediaBubblePopoverContent } from './MediaBubblePopoverContent' import { MediaBubblePopoverContent } from './MediaBubblePopoverContent'
import { NodePosition, useDragDistance } from 'contexts/GraphDndContext'
import { setMultipleRefs } from 'services/utils'
export const StepNode = ({ export const StepNode = ({
step, step,
isConnectable, isConnectable,
onMouseMoveBottomOfElement, indices,
onMouseMoveTopOfElement,
onMouseDown, onMouseDown,
}: { }: {
step: Step step: Step
isConnectable: boolean isConnectable: boolean
onMouseMoveBottomOfElement?: () => void indices: { stepIndex: number; blockIndex: number }
onMouseMoveTopOfElement?: () => void onMouseDown?: (stepNodePosition: NodePosition, step: DraggableStep) => void
onMouseDown?: (
stepNodePosition: { absolute: Coordinates; relative: Coordinates },
step: DraggableStep
) => void
}) => { }) => {
const { query } = useRouter() const { query } = useRouter()
const { const { setConnectingIds, connectingIds, openedStepId, setOpenedStepId } =
setConnectingIds, useGraph()
connectingIds, const { updateStep } = useTypebot()
openedStepId,
setOpenedStepId,
blocksCoordinates,
} = useGraph()
const { detachStepFromBlock, updateStep, typebot, updateWebhook } =
useTypebot()
const [localStep, setLocalStep] = useState(step) const [localStep, setLocalStep] = useState(step)
const [localWebhook, setLocalWebhook] = useState(
isWebhookStep(step)
? typebot?.webhooks.byId[step.options.webhookId ?? '']
: undefined
)
const [isConnecting, setIsConnecting] = useState(false) const [isConnecting, setIsConnecting] = useState(false)
const [isPopoverOpened, setIsPopoverOpened] = useState( const [isPopoverOpened, setIsPopoverOpened] = useState(
openedStepId === step.id openedStepId === step.id
) )
const stepRef = useRef<HTMLDivElement | null>(null)
const onDrag = (position: NodePosition) => {
if (step.type === 'start' || !onMouseDown) return
onMouseDown(position, step)
}
useDragDistance({
ref: stepRef,
onDrag,
isDisabled: !onMouseDown || step.type === 'start',
})
const [mouseDownEvent, setMouseDownEvent] =
useState<{ absolute: Coordinates; relative: Coordinates }>()
const [isEditing, setIsEditing] = useState<boolean>( const [isEditing, setIsEditing] = useState<boolean>(
isTextBubbleStep(step) && step.content.plainText === '' isTextBubbleStep(step) && step.content.plainText === ''
) )
@ -98,15 +91,15 @@ export const StepNode = ({
}, [connectingIds, step.blockId, step.id]) }, [connectingIds, step.blockId, step.id])
const handleModalClose = () => { const handleModalClose = () => {
updateStep(localStep.id, { ...localStep }) updateStep(indices, { ...localStep })
onModalClose() onModalClose()
} }
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (connectingIds?.target) if (connectingIds)
setConnectingIds({ setConnectingIds({
...connectingIds, ...connectingIds,
target: { ...connectingIds.target, stepId: step.id }, target: { blockId: step.blockId, stepId: step.id },
}) })
} }
@ -118,54 +111,16 @@ export const StepNode = ({
}) })
} }
const handleMouseDown = (e: React.MouseEvent) => { const handleCloseEditor = (content: TextBubbleContent) => {
if (!onMouseDown) return const updatedStep = { ...localStep, content } as Step
e.stopPropagation() setLocalStep(updatedStep)
const element = e.currentTarget as HTMLDivElement updateStep(indices, updatedStep)
const rect = element.getBoundingClientRect()
const relativeX = e.clientX - rect.left
const relativeY = e.clientY - rect.top
setMouseDownEvent({
absolute: { x: e.clientX + relativeX, y: e.clientY + relativeY },
relative: { x: relativeX, y: relativeY },
})
}
const handleGlobalMouseUp = () => {
setMouseDownEvent(undefined)
}
useEventListener('mouseup', handleGlobalMouseUp)
const handleMouseUp = () => {
if (mouseDownEvent) {
setIsEditing(true)
}
}
const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
if (!onMouseMoveBottomOfElement || !onMouseMoveTopOfElement) return
const isMovingAndIsMouseDown =
mouseDownEvent &&
onMouseDown &&
(event.movementX > 0 || event.movementY > 0)
if (isMovingAndIsMouseDown && step.type !== 'start') {
onMouseDown(mouseDownEvent, step)
detachStepFromBlock(step.id)
setMouseDownEvent(undefined)
}
const element = event.currentTarget as HTMLDivElement
const rect = element.getBoundingClientRect()
const y = event.clientY - rect.top
if (y > rect.height / 2) onMouseMoveBottomOfElement()
else onMouseMoveTopOfElement()
}
const handleCloseEditor = () => {
setIsEditing(false) setIsEditing(false)
} }
const handleClick = (e: React.MouseEvent) => { const handleClick = (e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()
if (isTextBubbleStep(step)) setIsEditing(true)
setOpenedStepId(step.id) setOpenedStepId(step.id)
} }
@ -175,22 +130,16 @@ export const StepNode = ({
} }
const updateOptions = () => { const updateOptions = () => {
updateStep(localStep.id, { ...localStep }) updateStep(indices, { ...localStep })
if (localWebhook) updateWebhook(localWebhook.id, { ...localWebhook })
} }
const handleOptionsChange = (options: StepOptions) => { const handleStepChange = (updates: Partial<Step>) => {
setLocalStep({ ...localStep, options } as Step) setLocalStep({ ...localStep, ...updates } as Step)
} }
const handleContentChange = (content: BubbleStepContent) => const handleContentChange = (content: BubbleStepContent) =>
setLocalStep({ ...localStep, content } as Step) setLocalStep({ ...localStep, content } as Step)
const handleWebhookChange = (updates: Partial<Webhook>) => {
if (!localWebhook) return
setLocalWebhook({ ...localWebhook, ...updates })
}
useEffect(() => { useEffect(() => {
if (isPopoverOpened && openedStepId !== step.id) updateOptions() if (isPopoverOpened && openedStepId !== step.id) updateOptions()
setIsPopoverOpened(openedStepId === step.id) setIsPopoverOpened(openedStepId === step.id)
@ -199,13 +148,12 @@ export const StepNode = ({
return isEditing && isTextBubbleStep(localStep) ? ( return isEditing && isTextBubbleStep(localStep) ? (
<TextBubbleEditor <TextBubbleEditor
stepId={localStep.id}
initialValue={localStep.content.richText} initialValue={localStep.content.richText}
onClose={handleCloseEditor} onClose={handleCloseEditor}
/> />
) : ( ) : (
<ContextMenu<HTMLDivElement> <ContextMenu<HTMLDivElement>
renderMenu={() => <StepNodeContextMenu stepId={step.id} />} renderMenu={() => <StepNodeContextMenu indices={indices} />}
> >
{(ref, isOpened) => ( {(ref, isOpened) => (
<Popover <Popover
@ -217,14 +165,11 @@ export const StepNode = ({
<PopoverTrigger> <PopoverTrigger>
<Flex <Flex
pos="relative" pos="relative"
ref={ref} ref={setMultipleRefs([ref, stepRef])}
onMouseMove={handleMouseMove}
onMouseDown={handleMouseDown}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
onMouseUp={handleMouseUp}
onClick={handleClick} onClick={handleClick}
data-testid={`step-${step.id}`} data-testid={`step`}
w="full" w="full"
> >
<HStack <HStack
@ -244,37 +189,34 @@ export const StepNode = ({
mt="1" mt="1"
data-testid={`${localStep.id}-icon`} data-testid={`${localStep.id}-icon`}
/> />
<StepNodeContent step={localStep} /> <StepNodeContent step={localStep} indices={indices} />
<TargetEndpoint <TargetEndpoint
pos="absolute" pos="absolute"
left="-32px" left="-32px"
top="19px" top="19px"
stepId={localStep.id} stepId={localStep.id}
/> />
{blocksCoordinates && {isConnectable && hasDefaultConnector(localStep) && (
isConnectable && <SourceEndpoint
hasDefaultConnector(localStep) && ( source={{
<SourceEndpoint blockId: localStep.blockId,
source={{ stepId: localStep.id,
blockId: localStep.blockId, }}
stepId: localStep.id, pos="absolute"
}} right="15px"
pos="absolute" bottom="18px"
right="15px" />
bottom="18px" )}
/>
)}
</HStack> </HStack>
</Flex> </Flex>
</PopoverTrigger> </PopoverTrigger>
{hasSettingsPopover(localStep) && ( {hasSettingsPopover(localStep) && (
<SettingsPopoverContent <SettingsPopoverContent
step={localStep} step={localStep}
webhook={localWebhook}
onExpandClick={handleExpandClick} onExpandClick={handleExpandClick}
onOptionsChange={handleOptionsChange} onStepChange={handleStepChange}
onWebhookChange={handleWebhookChange}
onTestRequestClick={updateOptions} onTestRequestClick={updateOptions}
indices={indices}
/> />
)} )}
{isMediaBubbleStep(localStep) && ( {isMediaBubbleStep(localStep) && (
@ -286,10 +228,9 @@ export const StepNode = ({
<SettingsModal isOpen={isModalOpen} onClose={handleModalClose}> <SettingsModal isOpen={isModalOpen} onClose={handleModalClose}>
<StepSettings <StepSettings
step={localStep} step={localStep}
webhook={localWebhook} onStepChange={handleStepChange}
onOptionsChange={handleOptionsChange}
onWebhookChange={handleWebhookChange}
onTestRequestClick={updateOptions} onTestRequestClick={updateOptions}
indices={indices}
/> />
</SettingsModal> </SettingsModal>
</Popover> </Popover>

View File

@ -6,11 +6,11 @@ import {
InputStepType, InputStepType,
LogicStepType, LogicStepType,
IntegrationStepType, IntegrationStepType,
StepIndices,
} from 'models' } from 'models'
import { isInputStep } from 'utils' import { isInputStep } from 'utils'
import { ButtonNodesList } from '../../ButtonNode' import { ItemNodesList } from '../../ItemNode'
import { import {
ConditionContent,
SetVariableContent, SetVariableContent,
TextBubbleContent, TextBubbleContent,
VideoBubbleContent, VideoBubbleContent,
@ -23,9 +23,9 @@ import { PlaceholderContent } from './contents/PlaceholderContent'
type Props = { type Props = {
step: Step | StartStep step: Step | StartStep
isConnectable?: boolean indices: StepIndices
} }
export const StepNodeContent = ({ step }: Props) => { export const StepNodeContent = ({ step, indices }: Props) => {
if (isInputStep(step) && step.options.variableId) { if (isInputStep(step) && step.options.variableId) {
return <WithVariableContent step={step} /> return <WithVariableContent step={step} />
} }
@ -52,13 +52,13 @@ export const StepNodeContent = ({ step }: Props) => {
return <Text color={'gray.500'}>Pick a date...</Text> return <Text color={'gray.500'}>Pick a date...</Text>
} }
case InputStepType.CHOICE: { case InputStepType.CHOICE: {
return <ButtonNodesList step={step} /> return <ItemNodesList step={step} indices={indices} />
} }
case LogicStepType.SET_VARIABLE: { case LogicStepType.SET_VARIABLE: {
return <SetVariableContent step={step} /> return <SetVariableContent step={step} />
} }
case LogicStepType.CONDITION: { case LogicStepType.CONDITION: {
return <ConditionContent step={step} /> return <ItemNodesList step={step} indices={indices} isReadOnly />
} }
case LogicStepType.REDIRECT: { case LogicStepType.REDIRECT: {
return ( return (

View File

@ -1,57 +0,0 @@
import { Flex, Stack, HStack, Tag, Text } from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext'
import { ConditionStep } from 'models'
import { SourceEndpoint } from '../../../../Endpoints/SourceEndpoint'
export const ConditionContent = ({ step }: { step: ConditionStep }) => {
const { typebot } = useTypebot()
return (
<Flex>
{step.options?.comparisons.allIds.length === 0 ? (
<Text color={'gray.500'}>Configure...</Text>
) : (
<Stack>
{step.options?.comparisons.allIds.map((comparisonId, idx) => {
const comparison = step.options?.comparisons.byId[comparisonId]
const variable =
typebot?.variables.byId[comparison?.variableId ?? '']
return (
<HStack key={comparisonId} spacing={1}>
{idx > 0 && <Text>{step.options?.logicalOperator ?? ''}</Text>}
{variable?.name && (
<Tag bgColor="orange.400">{variable.name}</Tag>
)}
{comparison.comparisonOperator && (
<Text>{comparison?.comparisonOperator}</Text>
)}
{comparison?.value && (
<Tag bgColor={'green.400'}>{comparison.value}</Tag>
)}
</HStack>
)
})}
</Stack>
)}
<SourceEndpoint
source={{
blockId: step.blockId,
stepId: step.id,
conditionType: 'true',
}}
pos="absolute"
top="7px"
right="15px"
/>
<SourceEndpoint
source={{
blockId: step.blockId,
stepId: step.id,
conditionType: 'false',
}}
pos="absolute"
bottom="7px"
right="15px"
/>
</Flex>
)
}

View File

@ -1,12 +1,13 @@
import { Text } from '@chakra-ui/react' import { Text } from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { SetVariableStep } from 'models' import { SetVariableStep } from 'models'
import { byId } from 'utils'
export const SetVariableContent = ({ step }: { step: SetVariableStep }) => { export const SetVariableContent = ({ step }: { step: SetVariableStep }) => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
const variableName = const variableName =
typebot?.variables.byId[step.options?.variableId ?? '']?.name ?? '' typebot?.variables.find(byId(step.options.variableId))?.name ?? ''
const expression = step.options?.expressionToEvaluate ?? '' const expression = step.options.expressionToEvaluate ?? ''
return ( return (
<Text color={'gray.500'}> <Text color={'gray.500'}>
{variableName === '' && expression === '' {variableName === '' && expression === ''

View File

@ -10,6 +10,7 @@ type Props = {
export const TextBubbleContent = ({ step }: Props) => { export const TextBubbleContent = ({ step }: Props) => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
if (!typebot) return <></>
return ( return (
<Flex <Flex
flexDir={'column'} flexDir={'column'}

View File

@ -1,18 +1,11 @@
import { Text } from '@chakra-ui/react' import { Text } from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext'
import { WebhookStep } from 'models' import { WebhookStep } from 'models'
import { useMemo } from 'react'
type Props = { type Props = {
step: WebhookStep step: WebhookStep
} }
export const WebhookContent = ({ step }: Props) => { export const WebhookContent = ({ step: { webhook } }: Props) => {
const { typebot } = useTypebot()
const webhook = useMemo(
() => typebot?.webhooks.byId[step.options?.webhookId ?? ''],
[step.options?.webhookId, typebot?.webhooks.byId]
)
if (!webhook?.url) return <Text color="gray.500">Configure...</Text> if (!webhook?.url) return <Text color="gray.500">Configure...</Text>
return ( return (
<Text isTruncated pr="6"> <Text isTruncated pr="6">

View File

@ -2,6 +2,7 @@ import { InputStep } from 'models'
import { chakra, Text } from '@chakra-ui/react' import { chakra, Text } from '@chakra-ui/react'
import React from 'react' import React from 'react'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { byId } from 'utils'
type Props = { type Props = {
step: InputStep step: InputStep
@ -9,8 +10,10 @@ type Props = {
export const WithVariableContent = ({ step }: Props) => { export const WithVariableContent = ({ step }: Props) => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
const variableName = const variableName = typebot?.variables.find(
typebot?.variables.byId[step.options.variableId as string].name byId(step.options.variableId)
)?.name
return ( return (
<Text> <Text>
Collect{' '} Collect{' '}

View File

@ -1,4 +1,3 @@
export * from './ConditionContent'
export * from './SetVariableContent' export * from './SetVariableContent'
export * from './WithVariableContent' export * from './WithVariableContent'
export * from './VideoBubbleContent' export * from './VideoBubbleContent'

View File

@ -1,11 +1,13 @@
import { MenuList, MenuItem } from '@chakra-ui/react' import { MenuList, MenuItem } from '@chakra-ui/react'
import { TrashIcon } from 'assets/icons' import { TrashIcon } from 'assets/icons'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { StepIndices } from 'models'
export const StepNodeContextMenu = ({ stepId }: { stepId: string }) => { type Props = { indices: StepIndices }
export const StepNodeContextMenu = ({ indices }: Props) => {
const { deleteStep } = useTypebot() const { deleteStep } = useTypebot()
const handleDeleteClick = () => deleteStep(stepId) const handleDeleteClick = () => deleteStep(indices)
return ( return (
<MenuList> <MenuList>

View File

@ -1,12 +1,13 @@
import { StackProps, HStack } from '@chakra-ui/react' import { StackProps, HStack } from '@chakra-ui/react'
import { StartStep, Step } from 'models' import { StartStep, Step, StepIndices } from 'models'
import { StepIcon } from 'components/editor/StepsSideBar/StepIcon' import { StepIcon } from 'components/editor/StepsSideBar/StepIcon'
import { StepNodeContent } from './StepNodeContent/StepNodeContent' import { StepNodeContent } from './StepNodeContent/StepNodeContent'
export const StepNodeOverlay = ({ export const StepNodeOverlay = ({
step, step,
indices,
...props ...props
}: { step: Step | StartStep } & StackProps) => { }: { step: Step | StartStep; indices: StepIndices } & StackProps) => {
return ( return (
<HStack <HStack
p="3" p="3"
@ -20,7 +21,7 @@ export const StepNodeOverlay = ({
{...props} {...props}
> >
<StepIcon type={step.type} /> <StepIcon type={step.type} />
<StepNodeContent step={step} /> <StepNodeContent step={step} indices={indices} />
</HStack> </HStack>
) )
} }

View File

@ -1,105 +1,123 @@
import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react' import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react'
import { DraggableStep } from 'models' import { DraggableStep, DraggableStepType, Step } from 'models'
import { useStepDnd } from 'contexts/StepDndContext' import {
computeNearestPlaceholderIndex,
useStepDnd,
} from 'contexts/GraphDndContext'
import { Coordinates, useGraph } from 'contexts/GraphContext' import { Coordinates, useGraph } from 'contexts/GraphContext'
import { useMemo, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { StepNode } from './StepNode' import { StepNode } from './StepNode'
import { StepNodeOverlay } from './StepNodeOverlay' import { StepNodeOverlay } from './StepNodeOverlay'
type Props = {
blockId: string
steps: Step[]
blockIndex: number
blockRef: React.MutableRefObject<HTMLDivElement | null>
isStartBlock: boolean
}
export const StepNodesList = ({ export const StepNodesList = ({
blockId, blockId,
stepIds, steps,
}: { blockIndex,
blockId: string blockRef,
stepIds: string[] isStartBlock,
}) => { }: Props) => {
const { const {
draggedStep, draggedStep,
setDraggedStep, setDraggedStep,
draggedStepType, draggedStepType,
mouseOverBlockId, mouseOverBlock,
setDraggedStepType, setDraggedStepType,
setMouseOverBlockId,
} = useStepDnd() } = useStepDnd()
const { typebot, createStep } = useTypebot() const { typebot, createStep, detachStepFromBlock } = useTypebot()
const { isReadOnly } = useGraph() const { isReadOnly } = useGraph()
const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState< const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState<
number | undefined number | undefined
>() >()
const showSortPlaceholders = useMemo( const placeholderRefs = useRef<HTMLDivElement[]>([])
() => mouseOverBlockId === blockId && (draggedStep || draggedStepType),
[mouseOverBlockId, blockId, draggedStep, draggedStepType]
)
const [position, setPosition] = useState({ const [position, setPosition] = useState({
x: 0, x: 0,
y: 0, y: 0,
}) })
const [relativeCoordinates, setRelativeCoordinates] = useState({ x: 0, y: 0 }) const [mousePositionInElement, setMousePositionInElement] = useState({
x: 0,
y: 0,
})
const isDraggingOnCurrentBlock =
(draggedStep || draggedStepType) && mouseOverBlock?.id === blockId
const showSortPlaceholders = !isStartBlock && (draggedStep || draggedStepType)
const handleStepMove = (event: MouseEvent) => { useEffect(() => {
if (!draggedStep) return if (mouseOverBlock?.id !== blockId) setExpandedPlaceholderIndex(undefined)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mouseOverBlock?.id])
const handleMouseMoveGlobal = (event: MouseEvent) => {
if (!draggedStep || draggedStep.blockId !== blockId) return
const { clientX, clientY } = event const { clientX, clientY } = event
setPosition({ setPosition({
...position, x: clientX - mousePositionInElement.x,
x: clientX - relativeCoordinates.x, y: clientY - mousePositionInElement.y,
y: clientY - relativeCoordinates.y,
}) })
} }
useEventListener('mousemove', handleStepMove) useEventListener('mousemove', handleMouseMoveGlobal)
const handleMouseMove = (event: React.MouseEvent) => { const handleMouseMoveOnBlock = (event: MouseEvent) => {
if (!draggedStep) return if (!isDraggingOnCurrentBlock) return
const element = event.currentTarget as HTMLDivElement setExpandedPlaceholderIndex(
const rect = element.getBoundingClientRect() computeNearestPlaceholderIndex(event.pageY, placeholderRefs)
const y = event.clientY - rect.top )
if (y < 20) setExpandedPlaceholderIndex(0)
} }
useEventListener('mousemove', handleMouseMoveOnBlock, blockRef.current)
const handleMouseUp = (e: React.MouseEvent<HTMLDivElement>) => { const handleMouseUpOnBlock = (e: MouseEvent) => {
if (expandedPlaceholderIndex === undefined) return
e.stopPropagation()
setMouseOverBlockId(undefined)
setExpandedPlaceholderIndex(undefined) setExpandedPlaceholderIndex(undefined)
if (!draggedStep && !draggedStepType) return if (!isDraggingOnCurrentBlock) return
const stepIndex = computeNearestPlaceholderIndex(e.clientY, placeholderRefs)
createStep( createStep(
blockId, blockId,
draggedStep || draggedStepType, (draggedStep || draggedStepType) as DraggableStep | DraggableStepType,
expandedPlaceholderIndex {
blockIndex,
stepIndex,
}
) )
setDraggedStep(undefined) setDraggedStep(undefined)
setDraggedStepType(undefined) setDraggedStepType(undefined)
} }
useEventListener(
'mouseup',
handleMouseUpOnBlock,
mouseOverBlock?.ref.current,
{
capture: true,
}
)
const handleStepMouseDown = ( const handleStepMouseDown =
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates }, (stepIndex: number) =>
step: DraggableStep (
) => { { absolute, relative }: { absolute: Coordinates; relative: Coordinates },
if (isReadOnly) return step: DraggableStep
setPosition(absolute) ) => {
setRelativeCoordinates(relative) if (isReadOnly) return
setMouseOverBlockId(blockId) detachStepFromBlock({ blockIndex, stepIndex })
setDraggedStep(step) setPosition(absolute)
} setMousePositionInElement(relative)
setDraggedStep(step)
}
const handleMouseOnTopOfStep = (stepIndex: number) => () => { const handlePushElementRef =
if (!draggedStep && !draggedStepType) return (idx: number) => (elem: HTMLDivElement | null) => {
setExpandedPlaceholderIndex(stepIndex === 0 ? 0 : stepIndex) elem && (placeholderRefs.current[idx] = elem)
} }
const handleMouseOnBottomOfStep = (stepIndex: number) => () => {
if (!draggedStep && !draggedStepType) return
setExpandedPlaceholderIndex(stepIndex + 1)
}
return ( return (
<Stack <Stack spacing={1} transition="none">
spacing={1}
onMouseUpCapture={handleMouseUp}
onMouseMove={handleMouseMove}
transition="none"
>
<Flex <Flex
ref={handlePushElementRef(0)}
h={ h={
showSortPlaceholders && expandedPlaceholderIndex === 0 showSortPlaceholders && expandedPlaceholderIndex === 0
? '50px' ? '50px'
@ -111,17 +129,17 @@ export const StepNodesList = ({
transition={showSortPlaceholders ? 'height 200ms' : 'none'} transition={showSortPlaceholders ? 'height 200ms' : 'none'}
/> />
{typebot && {typebot &&
stepIds.map((stepId, idx) => ( steps.map((step, idx) => (
<Stack key={stepId} spacing={1}> <Stack key={step.id} spacing={1}>
<StepNode <StepNode
key={stepId} key={step.id}
step={typebot.steps.byId[stepId]} step={step}
isConnectable={!isReadOnly && stepIds.length - 1 === idx} indices={{ blockIndex, stepIndex: idx }}
onMouseMoveTopOfElement={handleMouseOnTopOfStep(idx)} isConnectable={!isReadOnly && steps.length - 1 === idx}
onMouseMoveBottomOfElement={handleMouseOnBottomOfStep(idx)} onMouseDown={handleStepMouseDown(idx)}
onMouseDown={handleStepMouseDown}
/> />
<Flex <Flex
ref={handlePushElementRef(idx + 1)}
h={ h={
showSortPlaceholders && expandedPlaceholderIndex === idx + 1 showSortPlaceholders && expandedPlaceholderIndex === idx + 1
? '50px' ? '50px'
@ -138,6 +156,7 @@ export const StepNodesList = ({
<Portal> <Portal>
<StepNodeOverlay <StepNodeOverlay
step={draggedStep} step={draggedStep}
indices={{ blockIndex, stepIndex: 0 }}
pos="fixed" pos="fixed"
top="0" top="0"
left="0" left="0"

View File

@ -8,21 +8,19 @@ import {
withPlate, withPlate,
} from '@udecode/plate-core' } from '@udecode/plate-core'
import { editorStyle, platePlugins } from 'libs/plate' import { editorStyle, platePlugins } from 'libs/plate'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { BaseSelection, createEditor, Transforms } from 'slate' import { BaseSelection, createEditor, Transforms } from 'slate'
import { ToolBar } from './ToolBar' import { ToolBar } from './ToolBar'
import { parseHtmlStringToPlainText } from 'services/utils' import { parseHtmlStringToPlainText } from 'services/utils'
import { TextBubbleStep, Variable } from 'models' import { defaultTextBubbleContent, TextBubbleContent, Variable } from 'models'
import { VariableSearchInput } from 'components/shared/VariableSearchInput' import { VariableSearchInput } from 'components/shared/VariableSearchInput'
import { ReactEditor } from 'slate-react' import { ReactEditor } from 'slate-react'
type Props = { type Props = {
stepId: string
initialValue: TDescendant[] initialValue: TDescendant[]
onClose: () => void onClose: (newContent: TextBubbleContent) => void
} }
export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => { export const TextBubbleEditor = ({ initialValue, onClose }: Props) => {
const randomEditorId = useMemo(() => Math.random().toString(), []) const randomEditorId = useMemo(() => Math.random().toString(), [])
const editor = useMemo( const editor = useMemo(
() => () =>
@ -30,7 +28,6 @@ export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[] []
) )
const { updateStep } = useTypebot()
const [value, setValue] = useState(initialValue) const [value, setValue] = useState(initialValue)
const varDropdownRef = useRef<HTMLDivElement | null>(null) const varDropdownRef = useRef<HTMLDivElement | null>(null)
const rememberedSelection = useRef<BaseSelection | null>(null) const rememberedSelection = useRef<BaseSelection | null>(null)
@ -38,12 +35,11 @@ export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => {
const textEditorRef = useRef<HTMLDivElement>(null) const textEditorRef = useRef<HTMLDivElement>(null)
const closeEditor = () => onClose(convertValueToStepContent(value))
useOutsideClick({ useOutsideClick({
ref: textEditorRef, ref: textEditorRef,
handler: () => { handler: closeEditor,
save(value)
onClose()
},
}) })
useEffect(() => { useEffect(() => {
@ -69,18 +65,16 @@ export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => {
} }
} }
const save = (value: unknown[]) => { const convertValueToStepContent = (value: unknown[]): TextBubbleContent => {
if (value.length === 0) return if (value.length === 0) defaultTextBubbleContent
const html = serializeHtml(editor, { const html = serializeHtml(editor, {
nodes: value, nodes: value,
}) })
updateStep(stepId, { return {
content: { html,
html, richText: value,
richText: value, plainText: parseHtmlStringToPlainText(html),
plainText: parseHtmlStringToPlainText(html), }
},
} as TextBubbleStep)
} }
const handleMouseDown = (e: React.MouseEvent) => { const handleMouseDown = (e: React.MouseEvent) => {
@ -99,6 +93,11 @@ export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => {
setValue(val) setValue(val)
setIsVariableDropdownOpen(false) setIsVariableDropdownOpen(false)
} }
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.shiftKey) return
if (e.key === 'Enter') closeEditor()
}
return ( return (
<Stack <Stack
flex="1" flex="1"
@ -126,6 +125,7 @@ export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => {
onBlur: () => { onBlur: () => {
rememberedSelection.current = editor.selection rememberedSelection.current = editor.selection
}, },
onKeyDown: handleKeyDown,
}} }}
initialValue={ initialValue={
initialValue.length === 0 initialValue.length === 0

View File

@ -2,20 +2,20 @@ import { Box, Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react'
import { TrashIcon, PlusIcon } from 'assets/icons' import { TrashIcon, PlusIcon } from 'assets/icons'
import { deepEqual } from 'fast-equals' import { deepEqual } from 'fast-equals'
import { Draft } from 'immer' import { Draft } from 'immer'
import { Table } from 'models'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { generate } from 'short-uuid' import { generate } from 'short-uuid'
import { useImmer } from 'use-immer' import { useImmer } from 'use-immer'
type ItemWithId<T> = T & { id: string }
export type TableListItemProps<T> = { export type TableListItemProps<T> = {
id: string
item: T item: T
onItemChange: (item: T) => void onItemChange: (item: T) => void
} }
type Props<T> = { type Props<T> = {
initialItems: Table<T> initialItems: ItemWithId<T>[]
onItemsChange: (items: Table<T>) => void onItemsChange: (items: ItemWithId<T>[]) => void
addLabel?: string addLabel?: string
Item: (props: TableListItemProps<T>) => JSX.Element Item: (props: TableListItemProps<T>) => JSX.Element
ComponentBetweenItems?: (props: unknown) => JSX.Element ComponentBetweenItems?: (props: unknown) => JSX.Element
@ -29,7 +29,7 @@ export const TableList = <T,>({
ComponentBetweenItems = () => <></>, ComponentBetweenItems = () => <></>,
}: Props<T>) => { }: Props<T>) => {
const [items, setItems] = useImmer(initialItems) const [items, setItems] = useImmer(initialItems)
const [showDeleteId, setShowDeleteId] = useState<string | undefined>() const [showDeleteIndex, setShowDeleteIndex] = useState<number | null>(null)
useEffect(() => { useEffect(() => {
if (deepEqual(items, initialItems)) return if (deepEqual(items, initialItems)) return
@ -40,55 +40,47 @@ export const TableList = <T,>({
const createItem = () => { const createItem = () => {
setItems((items) => { setItems((items) => {
const id = generate() const id = generate()
items.byId[id] = { id } as unknown as Draft<T> const newItem = { id } as Draft<ItemWithId<T>>
items.allIds.push(id) items.push(newItem)
}) })
} }
const updateItem = (itemId: string, updates: Partial<T>) => const updateItem = (itemIndex: number, updates: Partial<T>) =>
setItems((items) => { setItems((items) => {
items.byId[itemId] = { items[itemIndex] = { ...items[itemIndex], ...updates }
...items.byId[itemId],
...updates,
}
}) })
const deleteItem = (itemId: string) => () => { const deleteItem = (itemIndex: number) => () => {
setItems((items) => { setItems((items) => {
delete items.byId[itemId] items.splice(itemIndex, 1)
const index = items.allIds.indexOf(itemId)
if (index !== -1) items.allIds.splice(index, 1)
}) })
} }
const handleMouseEnter = (itemId: string) => () => setShowDeleteId(itemId) const handleMouseEnter = (itemIndex: number) => () =>
setShowDeleteIndex(itemIndex)
const handleCellChange = (itemId: string) => (item: T) => const handleCellChange = (itemIndex: number) => (item: T) =>
updateItem(itemId, item) updateItem(itemIndex, item)
const handleMouseLeave = () => setShowDeleteId(undefined) const handleMouseLeave = () => setShowDeleteIndex(null)
return ( return (
<Stack spacing="4"> <Stack spacing="4">
{items.allIds.map((itemId, idx) => ( {items.map((item, itemIndex) => (
<Box key={itemId}> <Box key={item.id}>
{idx !== 0 && <ComponentBetweenItems />} {itemIndex !== 0 && <ComponentBetweenItems />}
<Flex <Flex
pos="relative" pos="relative"
onMouseEnter={handleMouseEnter(itemId)} onMouseEnter={handleMouseEnter(itemIndex)}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
mt={idx !== 0 && ComponentBetweenItems ? 4 : 0} mt={itemIndex !== 0 && ComponentBetweenItems ? 4 : 0}
> >
<Item <Item item={item} onItemChange={handleCellChange(itemIndex)} />
id={itemId} <Fade in={showDeleteIndex === itemIndex}>
item={items.byId[itemId]}
onItemChange={handleCellChange(itemId)}
/>
<Fade in={showDeleteId === itemId}>
<IconButton <IconButton
icon={<TrashIcon />} icon={<TrashIcon />}
aria-label="Remove cell" aria-label="Remove cell"
onClick={deleteItem(itemId)} onClick={deleteItem(itemIndex)}
pos="absolute" pos="absolute"
left="-15px" left="-15px"
top="-15px" top="-15px"

View File

@ -13,10 +13,10 @@ import {
import { PlusIcon, TrashIcon } from 'assets/icons' import { PlusIcon, TrashIcon } from 'assets/icons'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { Variable } from 'models' import { Variable } from 'models'
import React, { useState, useRef, ChangeEvent, useMemo, useEffect } from 'react' import React, { useState, useRef, ChangeEvent, useEffect } from 'react'
import { generate } from 'short-uuid' import { generate } from 'short-uuid'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
import { isNotDefined } from 'utils' import { byId, isNotDefined } from 'utils'
type Props = { type Props = {
initialVariableId?: string initialVariableId?: string
@ -34,16 +34,14 @@ export const VariableSearchInput = ({
}: Props) => { }: Props) => {
const { onOpen, onClose, isOpen } = useDisclosure() const { onOpen, onClose, isOpen } = useDisclosure()
const { typebot, createVariable, deleteVariable } = useTypebot() const { typebot, createVariable, deleteVariable } = useTypebot()
const variables = useMemo( const variables = typebot?.variables ?? []
() =>
typebot?.variables.allIds.map((id) => typebot.variables.byId[id]) ?? [],
[typebot?.variables]
)
const [inputValue, setInputValue] = useState( const [inputValue, setInputValue] = useState(
typebot?.variables.byId[initialVariableId ?? '']?.name ?? '' variables.find(byId(initialVariableId))?.name ?? ''
) )
const [debouncedInputValue] = useDebounce(inputValue, 200) const [debouncedInputValue] = useDebounce(inputValue, 200)
const [filteredItems, setFilteredItems] = useState<Variable[]>(variables) const [filteredItems, setFilteredItems] = useState<Variable[]>(
variables ?? []
)
const dropdownRef = useRef(null) const dropdownRef = useRef(null)
const inputRef = useRef(null) const inputRef = useRef(null)

View File

@ -15,7 +15,7 @@ export const ButtonsTheme = ({ buttons, onButtonsChange }: Props) => {
onButtonsChange({ ...buttons, color }) onButtonsChange({ ...buttons, color })
return ( return (
<Stack> <Stack data-testid="buttons-theme">
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Text>Background:</Text> <Text>Background:</Text>
<ColorPicker <ColorPicker

View File

@ -15,7 +15,7 @@ export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
onGuestBubblesChange({ ...guestBubbles, color }) onGuestBubblesChange({ ...guestBubbles, color })
return ( return (
<Stack> <Stack data-testid="guest-bubbles-theme">
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Text>Background:</Text> <Text>Background:</Text>
<ColorPicker <ColorPicker

View File

@ -15,7 +15,7 @@ export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
onHostBubblesChange({ ...hostBubbles, color }) onHostBubblesChange({ ...hostBubbles, color })
return ( return (
<Stack> <Stack data-testid="host-bubbles-theme">
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Text>Background:</Text> <Text>Background:</Text>
<ColorPicker <ColorPicker

View File

@ -17,7 +17,7 @@ export const InputsTheme = ({ inputs, onInputsChange }: Props) => {
onInputsChange({ ...inputs, placeholderColor }) onInputsChange({ ...inputs, placeholderColor })
return ( return (
<Stack> <Stack data-testid="inputs-theme">
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Text>Background:</Text> <Text>Background:</Text>
<ColorPicker <ColorPicker

View File

@ -41,6 +41,8 @@ export const ColorPicker = ({ initialColor, onColorChange }: Props) => {
const handleColorChange = (e: ChangeEvent<HTMLInputElement>) => const handleColorChange = (e: ChangeEvent<HTMLInputElement>) =>
setColor(e.target.value) setColor(e.target.value)
const handleClick = (color: string) => () => setColor(color)
return ( return (
<Popover variant="picker" placement="right" isLazy> <Popover variant="picker" placement="right" isLazy>
<PopoverTrigger> <PopoverTrigger>
@ -79,10 +81,8 @@ export const ColorPicker = ({ initialColor, onColorChange }: Props) => {
minWidth="unset" minWidth="unset"
borderRadius={3} borderRadius={3}
_hover={{ background: c }} _hover={{ background: c }}
onClick={() => { onClick={handleClick(c)}
setColor(c) />
}}
></Button>
))} ))}
</SimpleGrid> </SimpleGrid>
<Input <Input

View File

@ -1,4 +1,4 @@
import { Block, Source, Step, Table, Target, Typebot } from 'models' import { Block, Edge, IdMap, Source, Step, Target } from 'models'
import { import {
createContext, createContext,
Dispatch, Dispatch,
@ -9,7 +9,6 @@ import {
useEffect, useEffect,
useState, useState,
} from 'react' } from 'react'
import { useImmer } from 'use-immer'
export const stubLength = 20 export const stubLength = 20
export const blockWidth = 300 export const blockWidth = 300
@ -56,20 +55,20 @@ export type Endpoint = {
ref: MutableRefObject<HTMLDivElement | null> ref: MutableRefObject<HTMLDivElement | null>
} }
export type BlocksCoordinates = { byId: { [key: string]: Coordinates } } export type BlocksCoordinates = IdMap<Coordinates>
const graphContext = createContext<{ const graphContext = createContext<{
blocksCoordinates?: BlocksCoordinates blocksCoordinates: BlocksCoordinates
updateBlockCoordinates: (blockId: string, newCoord: Coordinates) => void updateBlockCoordinates: (blockId: string, newCoord: Coordinates) => void
graphPosition: Position graphPosition: Position
setGraphPosition: Dispatch<SetStateAction<Position>> setGraphPosition: Dispatch<SetStateAction<Position>>
connectingIds: ConnectingIds | null connectingIds: ConnectingIds | null
setConnectingIds: Dispatch<SetStateAction<ConnectingIds | null>> setConnectingIds: Dispatch<SetStateAction<ConnectingIds | null>>
previewingEdgeId?: string previewingEdge?: Edge
setPreviewingEdgeId: Dispatch<SetStateAction<string | undefined>> setPreviewingEdge: Dispatch<SetStateAction<Edge | undefined>>
sourceEndpoints: Table<Endpoint> sourceEndpoints: IdMap<Endpoint>
addSourceEndpoint: (endpoint: Endpoint) => void addSourceEndpoint: (endpoint: Endpoint) => void
targetEndpoints: Table<Endpoint> targetEndpoints: IdMap<Endpoint>
addTargetEndpoint: (endpoint: Endpoint) => void addTargetEndpoint: (endpoint: Endpoint) => void
openedStepId?: string openedStepId?: string
setOpenedStepId: Dispatch<SetStateAction<string | undefined>> setOpenedStepId: Dispatch<SetStateAction<string | undefined>>
@ -83,63 +82,55 @@ const graphContext = createContext<{
export const GraphProvider = ({ export const GraphProvider = ({
children, children,
typebot, blocks,
isReadOnly = false, isReadOnly = false,
}: { }: {
children: ReactNode children: ReactNode
typebot?: Typebot blocks: Block[]
isReadOnly?: boolean isReadOnly?: boolean
}) => { }) => {
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 [previewingEdgeId, setPreviewingEdgeId] = useState<string>() const [previewingEdge, setPreviewingEdge] = useState<Edge>()
const [sourceEndpoints, setSourceEndpoints] = useState<Table<Endpoint>>({ const [sourceEndpoints, setSourceEndpoints] = useState<IdMap<Endpoint>>({})
byId: {}, const [targetEndpoints, setTargetEndpoints] = useState<IdMap<Endpoint>>({})
allIds: [],
})
const [targetEndpoints, setTargetEndpoints] = useState<Table<Endpoint>>({
byId: {},
allIds: [],
})
const [openedStepId, setOpenedStepId] = useState<string>() const [openedStepId, setOpenedStepId] = useState<string>()
const [blocksCoordinates, setBlocksCoordinates] = useImmer< const [blocksCoordinates, setBlocksCoordinates] = useState<BlocksCoordinates>(
BlocksCoordinates | undefined {}
>(undefined) )
useEffect(() => { useEffect(() => {
setBlocksCoordinates( setBlocksCoordinates(
typebot?.blocks.allIds.reduce( blocks.reduce(
(coords, blockId) => ({ (coords, block) => ({
byId: { ...coords,
...coords.byId, [block.id]: block.graphCoordinates,
[blockId]: typebot.blocks.byId[blockId].graphCoordinates,
},
}), }),
{ byId: {} } {}
) )
) )
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [typebot?.blocks]) }, [blocks])
const addSourceEndpoint = (endpoint: Endpoint) => { const addSourceEndpoint = (endpoint: Endpoint) => {
setSourceEndpoints((endpoints) => ({ setSourceEndpoints((endpoints) => ({
byId: { ...endpoints.byId, [endpoint.id]: endpoint }, ...endpoints,
allIds: [...endpoints.allIds, endpoint.id], [endpoint.id]: endpoint,
})) }))
} }
const addTargetEndpoint = (endpoint: Endpoint) => { const addTargetEndpoint = (endpoint: Endpoint) => {
setTargetEndpoints((endpoints) => ({ setTargetEndpoints((endpoints) => ({
byId: { ...endpoints.byId, [endpoint.id]: endpoint }, ...endpoints,
allIds: [...endpoints.allIds, endpoint.id], [endpoint.id]: endpoint,
})) }))
} }
const updateBlockCoordinates = (blockId: string, newCoord: Coordinates) => const updateBlockCoordinates = (blockId: string, newCoord: Coordinates) =>
setBlocksCoordinates((blocksCoordinates) => { setBlocksCoordinates((blocksCoordinates) => ({
if (!blocksCoordinates) return ...blocksCoordinates,
blocksCoordinates.byId[blockId] = newCoord [blockId]: newCoord,
}) }))
return ( return (
<graphContext.Provider <graphContext.Provider
@ -148,8 +139,8 @@ export const GraphProvider = ({
setGraphPosition, setGraphPosition,
connectingIds, connectingIds,
setConnectingIds, setConnectingIds,
previewingEdgeId, previewingEdge,
setPreviewingEdgeId, setPreviewingEdge,
sourceEndpoints, sourceEndpoints,
targetEndpoints, targetEndpoints,
addSourceEndpoint, addSourceEndpoint,

View File

@ -0,0 +1,125 @@
import { useEventListener } from '@chakra-ui/react'
import { ButtonItem, DraggableStep, DraggableStepType } from 'models'
import {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useContext,
useRef,
useState,
} from 'react'
import { Coordinates } from './GraphContext'
type BlockInfo = {
id: string
ref: React.MutableRefObject<HTMLDivElement | null>
}
const graphDndContext = createContext<{
draggedStepType?: DraggableStepType
setDraggedStepType: Dispatch<SetStateAction<DraggableStepType | undefined>>
draggedStep?: DraggableStep
setDraggedStep: Dispatch<SetStateAction<DraggableStep | undefined>>
draggedItem?: ButtonItem
setDraggedItem: Dispatch<SetStateAction<ButtonItem | undefined>>
mouseOverBlock?: BlockInfo
setMouseOverBlock: Dispatch<SetStateAction<BlockInfo | undefined>>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})
export type NodePosition = { absolute: Coordinates; relative: Coordinates }
export const GraphDndContext = ({ children }: { children: ReactNode }) => {
const [draggedStep, setDraggedStep] = useState<DraggableStep>()
const [draggedStepType, setDraggedStepType] = useState<
DraggableStepType | undefined
>()
const [draggedItem, setDraggedItem] = useState<ButtonItem | undefined>()
const [mouseOverBlock, setMouseOverBlock] = useState<BlockInfo>()
return (
<graphDndContext.Provider
value={{
draggedStep,
setDraggedStep,
draggedStepType,
setDraggedStepType,
draggedItem,
setDraggedItem,
mouseOverBlock,
setMouseOverBlock,
}}
>
{children}
</graphDndContext.Provider>
)
}
export const useDragDistance = ({
ref,
onDrag,
distanceTolerance = 20,
isDisabled = false,
}: {
ref: React.MutableRefObject<HTMLDivElement | null>
onDrag: (position: { absolute: Coordinates; relative: Coordinates }) => void
distanceTolerance?: number
isDisabled: boolean
}) => {
const mouseDownPosition =
useRef<{ absolute: Coordinates; relative: Coordinates }>()
const handleMouseUp = () => {
if (mouseDownPosition) mouseDownPosition.current = undefined
}
useEventListener('mouseup', handleMouseUp)
const handleMouseDown = (e: MouseEvent) => {
if (isDisabled || !ref.current) return
e.stopPropagation()
const { top, left } = ref.current.getBoundingClientRect()
mouseDownPosition.current = {
absolute: { x: e.clientX, y: e.clientY },
relative: {
x: e.clientX - left,
y: e.clientY - top,
},
}
}
useEventListener('mousedown', handleMouseDown, ref.current)
const handleMouseMove = (e: MouseEvent) => {
if (!mouseDownPosition.current) return
const { clientX, clientY } = e
if (
Math.abs(mouseDownPosition.current.absolute.x - clientX) >
distanceTolerance ||
Math.abs(mouseDownPosition.current.absolute.y - clientY) >
distanceTolerance
) {
onDrag(mouseDownPosition.current)
}
}
useEventListener('mousemove', handleMouseMove)
}
export const computeNearestPlaceholderIndex = (
offsetY: number,
placeholderRefs: React.MutableRefObject<HTMLDivElement[]>
) => {
const { closestIndex } = placeholderRefs.current.reduce(
(prev, elem, index) => {
const elementTop = elem.getBoundingClientRect().top
const mouseDistanceFromPlaceholder = Math.abs(offsetY - elementTop)
return mouseDistanceFromPlaceholder < prev.value
? { closestIndex: index, value: mouseDistanceFromPlaceholder }
: prev
},
{ closestIndex: 0, value: 100 }
)
return closestIndex
}
export const useStepDnd = () => useContext(graphDndContext)

View File

@ -1,52 +0,0 @@
import { ChoiceItem, DraggableStep, DraggableStepType } from 'models'
import {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useContext,
useState,
} from 'react'
const stepDndContext = createContext<{
draggedStepType?: DraggableStepType
setDraggedStepType: Dispatch<SetStateAction<DraggableStepType | undefined>>
draggedStep?: DraggableStep
setDraggedStep: Dispatch<SetStateAction<DraggableStep | undefined>>
draggedChoiceItem?: ChoiceItem
setDraggedChoiceItem: Dispatch<SetStateAction<ChoiceItem | undefined>>
mouseOverBlockId?: string
setMouseOverBlockId: Dispatch<SetStateAction<string | undefined>>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})
export const StepDndContext = ({ children }: { children: ReactNode }) => {
const [draggedStep, setDraggedStep] = useState<DraggableStep>()
const [draggedStepType, setDraggedStepType] = useState<
DraggableStepType | undefined
>()
const [draggedChoiceItem, setDraggedChoiceItem] = useState<
ChoiceItem | undefined
>()
const [mouseOverBlockId, setMouseOverBlockId] = useState<string>()
return (
<stepDndContext.Provider
value={{
draggedStep,
setDraggedStep,
draggedStepType,
setDraggedStepType,
draggedChoiceItem,
setDraggedChoiceItem,
mouseOverBlockId,
setMouseOverBlockId,
}}
>
{children}
</stepDndContext.Provider>
)
}
export const useStepDnd = () => useContext(stepDndContext)

View File

@ -25,13 +25,12 @@ import useSWR from 'swr'
import { isDefined } from 'utils' import { isDefined } from 'utils'
import { BlocksActions, blocksActions } from './actions/blocks' import { BlocksActions, blocksActions } from './actions/blocks'
import { stepsAction, StepsActions } from './actions/steps' import { stepsAction, StepsActions } from './actions/steps'
import { choiceItemsAction, ChoiceItemsActions } from './actions/choiceItems'
import { variablesAction, VariablesActions } from './actions/variables' import { variablesAction, VariablesActions } from './actions/variables'
import { edgesAction, EdgesActions } from './actions/edges' import { edgesAction, EdgesActions } from './actions/edges'
import { webhooksAction, WebhooksAction } from './actions/webhooks'
import { useRegisterActions } from 'kbar' import { useRegisterActions } from 'kbar'
import useUndo from 'services/utils/useUndo' import useUndo from 'services/utils/useUndo'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
import { itemsAction, ItemsActions } from './actions/items'
const autoSaveTimeout = 40000 const autoSaveTimeout = 40000
type UpdateTypebotPayload = Partial<{ type UpdateTypebotPayload = Partial<{
@ -59,10 +58,9 @@ const typebotContext = createContext<
publishTypebot: () => void publishTypebot: () => void
} & BlocksActions & } & BlocksActions &
StepsActions & StepsActions &
ChoiceItemsActions & ItemsActions &
VariablesActions & VariablesActions &
EdgesActions & EdgesActions
WebhooksAction
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
>({}) >({})
@ -72,7 +70,7 @@ export const TypebotContext = ({
typebotId, typebotId,
}: { }: {
children: ReactNode children: ReactNode
typebotId?: string typebotId: string
}) => { }) => {
const router = useRouter() const router = useRouter()
const toast = useToast({ const toast = useToast({
@ -237,10 +235,9 @@ export const TypebotContext = ({
updateTypebot: updateLocalTypebot, updateTypebot: updateLocalTypebot,
...blocksActions(localTypebot as Typebot, setLocalTypebot), ...blocksActions(localTypebot as Typebot, setLocalTypebot),
...stepsAction(localTypebot as Typebot, setLocalTypebot), ...stepsAction(localTypebot as Typebot, setLocalTypebot),
...choiceItemsAction(localTypebot as Typebot, setLocalTypebot),
...variablesAction(localTypebot as Typebot, setLocalTypebot), ...variablesAction(localTypebot as Typebot, setLocalTypebot),
...edgesAction(localTypebot as Typebot, setLocalTypebot), ...edgesAction(localTypebot as Typebot, setLocalTypebot),
...webhooksAction(localTypebot as Typebot, setLocalTypebot), ...itemsAction(localTypebot as Typebot, setLocalTypebot),
}} }}
> >
{children} {children}
@ -254,13 +251,13 @@ export const useFetchedTypebot = ({
typebotId, typebotId,
onError, onError,
}: { }: {
typebotId?: string typebotId: string
onError: (error: Error) => void onError: (error: Error) => void
}) => { }) => {
const { data, error, mutate } = useSWR< const { data, error, mutate } = useSWR<
{ typebot: Typebot; publishedTypebot?: PublicTypebot }, { typebot: Typebot; publishedTypebot?: PublicTypebot },
Error Error
>(typebotId ? `/api/typebots/${typebotId}` : null, fetcher) >(`/api/typebots/${typebotId}`, fetcher)
if (error) onError(error) if (error) onError(error)
return { return {
typebot: data?.typebot, typebot: data?.typebot,

View File

@ -1,96 +1,86 @@
import { Coordinates } from 'contexts/GraphContext' import { Coordinates } from 'contexts/GraphContext'
import { produce } from 'immer' import { produce } from 'immer'
import { WritableDraft } from 'immer/dist/internal' import { WritableDraft } from 'immer/dist/internal'
import { Block, DraggableStep, DraggableStepType, Typebot } from 'models' import {
Block,
DraggableStep,
DraggableStepType,
StepIndices,
Typebot,
} from 'models'
import { SetTypebot } from '../TypebotContext' import { SetTypebot } from '../TypebotContext'
import { deleteEdgeDraft } from './edges' import { cleanUpEdgeDraft } from './edges'
import { createStepDraft, deleteStepDraft } from './steps' import { createStepDraft } from './steps'
export type BlocksActions = { export type BlocksActions = {
createBlock: ( createBlock: (
props: Coordinates & { props: Coordinates & {
id: string id: string
step: DraggableStep | DraggableStepType step: DraggableStep | DraggableStepType
indices: StepIndices
} }
) => void ) => void
updateBlock: (blockId: string, updates: Partial<Omit<Block, 'id'>>) => void updateBlock: (blockIndex: number, updates: Partial<Omit<Block, 'id'>>) => void
deleteBlock: (blockId: string) => void deleteBlock: (blockIndex: number) => void
} }
export const blocksActions = ( const blocksActions = (
typebot: Typebot, typebot: Typebot,
setTypebot: SetTypebot setTypebot: SetTypebot
): BlocksActions => ({ ): BlocksActions => ({
createBlock: ({ createBlock: ({
id, id,
step, step,
indices,
...graphCoordinates ...graphCoordinates
}: Coordinates & { }: Coordinates & {
id: string id: string
step: DraggableStep | DraggableStepType step: DraggableStep | DraggableStepType
indices: StepIndices
}) => { }) => {
setTypebot( setTypebot(
produce(typebot, (typebot) => { produce(typebot, (typebot) => {
const newBlock: Block = { const newBlock: Block = {
id, id,
graphCoordinates, graphCoordinates,
title: `Block ${typebot.blocks.allIds.length}`, title: `Block #${typebot.blocks.length}`,
stepIds: [], steps: [],
} }
typebot.blocks.byId[newBlock.id] = newBlock typebot.blocks.push(newBlock)
typebot.blocks.allIds.push(newBlock.id) createStepDraft(typebot, step, newBlock.id, indices)
createStepDraft(typebot, step, newBlock.id)
removeEmptyBlocks(typebot)
}) })
) )
}, },
updateBlock: (blockId: string, updates: Partial<Omit<Block, 'id'>>) => updateBlock: (blockIndex: number, updates: Partial<Omit<Block, 'id'>>) =>
setTypebot( setTypebot(
produce(typebot, (typebot) => { produce(typebot, (typebot) => {
typebot.blocks.byId[blockId] = { const block = typebot.blocks[blockIndex]
...typebot.blocks.byId[blockId], typebot.blocks[blockIndex] = { ...block, ...updates }
...updates,
}
}) })
), ),
deleteBlock: (blockId: string) => deleteBlock: (blockIndex: number) =>
setTypebot( setTypebot(
produce(typebot, (typebot) => { produce(typebot, (typebot) => {
deleteStepsInsideBlock(typebot, blockId) deleteBlockDraft(typebot)(blockIndex)
deleteAssociatedEdges(typebot, blockId)
deleteBlockDraft(typebot)(blockId)
}) })
), ),
}) })
export const removeEmptyBlocks = (typebot: WritableDraft<Typebot>) => { const deleteBlockDraft =
const emptyBlockIds = typebot.blocks.allIds.filter( (typebot: WritableDraft<Typebot>) => (blockIndex: number) => {
(blockId) => typebot.blocks.byId[blockId].stepIds.length === 0 cleanUpEdgeDraft(typebot, typebot.blocks[blockIndex].id)
) typebot.blocks.splice(blockIndex, 1)
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 = (
typebot: WritableDraft<Typebot>,
blockId: string
) => {
const block = typebot.blocks.byId[blockId]
block.stepIds.forEach((stepId) => deleteStepDraft(stepId)(typebot))
}
export const deleteBlockDraft =
(typebot: WritableDraft<Typebot>) => (blockId: string) => {
delete typebot.blocks.byId[blockId]
const index = typebot.blocks.allIds.indexOf(blockId)
if (index !== -1) typebot.blocks.allIds.splice(index, 1)
} }
const removeEmptyBlocks = (typebot: WritableDraft<Typebot>) => {
const emptyBlocksIndices = typebot.blocks.reduce<number[]>(
(arr, block, idx) => {
block.steps.length === 0 && arr.push(idx)
return arr
},
[]
)
emptyBlocksIndices.forEach(deleteBlockDraft(typebot))
}
export { blocksActions, removeEmptyBlocks }

View File

@ -1,85 +0,0 @@
import { ChoiceItem, InputStepType, Typebot } from 'models'
import { generate } from 'short-uuid'
import assert from 'assert'
import { SetTypebot } from '../TypebotContext'
import { produce } from 'immer'
import { WritableDraft } from 'immer/dist/internal'
export type ChoiceItemsActions = {
createChoiceItem: (
item: ChoiceItem | Pick<ChoiceItem, 'stepId'>,
index?: number
) => void
updateChoiceItem: (
itemId: string,
updates: Partial<Omit<ChoiceItem, 'id'>>
) => void
deleteChoiceItem: (itemId: string) => void
}
export const choiceItemsAction = (
typebot: Typebot,
setTypebot: SetTypebot
): ChoiceItemsActions => ({
createChoiceItem: (
item: ChoiceItem | Pick<ChoiceItem, 'stepId'>,
index?: number
) => {
setTypebot(
produce(typebot, (typebot) => {
createChoiceItemDraft(typebot, item, index)
})
)
},
updateChoiceItem: (
itemId: string,
updates: Partial<Omit<ChoiceItem, 'id'>>
) =>
setTypebot(
produce(typebot, (typebot) => {
typebot.choiceItems.byId[itemId] = {
...typebot.choiceItems.byId[itemId],
...updates,
}
})
),
deleteChoiceItem: (itemId: string) => {
setTypebot(
produce(typebot, (typebot) => {
removeChoiceItemFromStep(typebot, itemId)
deleteChoiceItemDraft(typebot, itemId)
})
)
},
})
const removeChoiceItemFromStep = (
typebot: WritableDraft<Typebot>,
itemId: string
) => {
const containerStepId = typebot.choiceItems.byId[itemId].stepId
const step = typebot.steps.byId[containerStepId]
assert(step.type === InputStepType.CHOICE)
step.options?.itemIds.splice(step.options.itemIds.indexOf(itemId), 1)
}
export const deleteChoiceItemDraft = (typebot: Typebot, itemId: string) => {
delete typebot.choiceItems.byId[itemId]
const index = typebot.choiceItems.allIds.indexOf(itemId)
if (index !== -1) typebot.choiceItems.allIds.splice(index, 1)
}
export const createChoiceItemDraft = (
typebot: Typebot,
item: ChoiceItem | Pick<ChoiceItem, 'stepId'>,
index?: number
) => {
const step = typebot.steps.byId[item.stepId]
assert(step.type === InputStepType.CHOICE)
const newItem: ChoiceItem =
'id' in item ? { ...item } : { id: generate(), stepId: item.stepId }
typebot.choiceItems.byId[newItem.id] = newItem
typebot.choiceItems.allIds.push(newItem.id)
if (step.options.itemIds.indexOf(newItem.id) !== -1) return
step.options.itemIds.splice(index ?? 0, 0, newItem.id)
}

View File

@ -1,12 +1,13 @@
import { Typebot, Edge, ConditionStep } from 'models' import { Typebot, Edge, StepWithItems, StepIndices, ItemIndices } from 'models'
import { WritableDraft } from 'immer/dist/types/types-external' import { WritableDraft } from 'immer/dist/types/types-external'
import { generate } from 'short-uuid' import { generate } from 'short-uuid'
import { SetTypebot } from '../TypebotContext' import { SetTypebot } from '../TypebotContext'
import { produce } from 'immer' import { produce } from 'immer'
import { byId, isDefined, isNotDefined } from 'utils'
export type EdgesActions = { export type EdgesActions = {
createEdge: (edge: Omit<Edge, 'id'>) => void createEdge: (edge: Omit<Edge, 'id'>) => void
updateEdge: (edgeId: string, updates: Partial<Omit<Edge, 'id'>>) => void updateEdge: (edgeIndex: number, updates: Partial<Omit<Edge, 'id'>>) => void
deleteEdge: (edgeId: string) => void deleteEdge: (edgeId: string) => void
} }
@ -21,40 +22,37 @@ export const edgesAction = (
...edge, ...edge,
id: generate(), id: generate(),
} }
if (edge.from.buttonId) { removeExistingEdge(typebot, edge)
deleteEdgeDraft( typebot.edges.push(newEdge)
typebot, const blockIndex = typebot.blocks.findIndex(byId(edge.from.blockId))
typebot.choiceItems.byId[edge.from.buttonId].edgeId const stepIndex = typebot.blocks[blockIndex].steps.findIndex(
) byId(edge.from.stepId)
typebot.choiceItems.byId[edge.from.buttonId].edgeId = newEdge.id )
} else if (edge.from.conditionType === 'true') { const itemIndex = edge.from.itemId
deleteEdgeDraft( ? (
typebot, typebot.blocks[blockIndex].steps[stepIndex] as StepWithItems
(typebot.steps.byId[edge.from.stepId] as ConditionStep).trueEdgeId ).items.findIndex(byId(edge.from.itemId))
) : null
;(typebot.steps.byId[edge.from.stepId] as ConditionStep).trueEdgeId =
newEdge.id isDefined(itemIndex)
} else if (edge.from.conditionType === 'false') { ? addEdgeIdToItem(typebot, newEdge.id, {
deleteEdgeDraft( blockIndex,
typebot, stepIndex,
(typebot.steps.byId[edge.from.stepId] as ConditionStep).falseEdgeId itemIndex,
) })
;(typebot.steps.byId[edge.from.stepId] as ConditionStep).falseEdgeId = : addEdgeIdToStep(typebot, newEdge.id, {
newEdge.id blockIndex,
} else { stepIndex,
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'>>) => updateEdge: (edgeIndex: number, updates: Partial<Omit<Edge, 'id'>>) =>
setTypebot( setTypebot(
produce(typebot, (typebot) => { produce(typebot, (typebot) => {
typebot.edges.byId[edgeId] = { const currentEdge = typebot.edges[edgeIndex]
...typebot.edges.byId[edgeId], typebot.edges[edgeIndex] = {
...currentEdge,
...updates, ...updates,
} }
}) })
@ -68,12 +66,55 @@ export const edgesAction = (
}, },
}) })
const addEdgeIdToStep = (
typebot: WritableDraft<Typebot>,
edgeId: string,
{ blockIndex, stepIndex }: StepIndices
) => {
typebot.blocks[blockIndex].steps[stepIndex].outgoingEdgeId = edgeId
}
const addEdgeIdToItem = (
typebot: WritableDraft<Typebot>,
edgeId: string,
{ blockIndex, stepIndex, itemIndex }: ItemIndices
) => {
;(typebot.blocks[blockIndex].steps[stepIndex] as StepWithItems).items[
itemIndex
].outgoingEdgeId = edgeId
}
export const deleteEdgeDraft = ( export const deleteEdgeDraft = (
typebot: WritableDraft<Typebot>, typebot: WritableDraft<Typebot>,
edgeId?: string edgeId: string
) => { ) => {
if (!edgeId) return const edgeIndex = typebot.edges.findIndex(byId(edgeId))
delete typebot.edges.byId[edgeId] typebot.edges.splice(edgeIndex, 1)
const index = typebot.edges.allIds.indexOf(edgeId) }
if (index !== -1) typebot.edges.allIds.splice(index, 1)
export const cleanUpEdgeDraft = (
typebot: WritableDraft<Typebot>,
deletedNodeId: string
) => {
typebot.edges = typebot.edges.filter(
(edge) =>
![
edge.from.blockId,
edge.from.stepId,
edge.from.itemId,
edge.to.blockId,
edge.to.stepId,
].includes(deletedNodeId)
)
}
const removeExistingEdge = (
typebot: WritableDraft<Typebot>,
edge: Omit<Edge, 'id'>
) => {
typebot.edges = typebot.edges.filter((e) =>
edge.from.itemId
? e.from.itemId !== edge.from.itemId
: isDefined(e.from.itemId) || e.from.stepId !== edge.from.stepId
)
} }

View File

@ -0,0 +1,71 @@
import {
Typebot,
ItemIndices,
Item,
InputStepType,
StepWithItems,
ButtonItem,
} from 'models'
import { SetTypebot } from '../TypebotContext'
import produce from 'immer'
import { cleanUpEdgeDraft } from './edges'
import { stepHasItems } from 'utils'
import { generate } from 'short-uuid'
export type ItemsActions = {
createItem: (item: Omit<ButtonItem, 'id'>, indices: ItemIndices) => void
updateItem: (indices: ItemIndices, updates: Partial<Omit<Item, 'id'>>) => void
deleteItem: (indices: ItemIndices) => void
}
const itemsAction = (
typebot: Typebot,
setTypebot: SetTypebot
): ItemsActions => ({
createItem: (
item: Omit<ButtonItem, 'id'>,
{ blockIndex, stepIndex, itemIndex }: ItemIndices
) => {
setTypebot(
produce(typebot, (typebot) => {
const step = typebot.blocks[blockIndex].steps[stepIndex]
if (step.type !== InputStepType.CHOICE) return
step.items.splice(itemIndex, 0, {
...item,
stepId: step.id,
id: generate(),
})
})
)
},
updateItem: (
{ blockIndex, stepIndex, itemIndex }: ItemIndices,
updates: Partial<Omit<Item, 'id'>>
) =>
setTypebot(
produce(typebot, (typebot) => {
const step = typebot.blocks[blockIndex].steps[stepIndex]
if (!stepHasItems(step)) return
;(typebot.blocks[blockIndex].steps[stepIndex] as StepWithItems).items[
itemIndex
] = {
...step.items[itemIndex],
...updates,
} as Item
})
),
deleteItem: ({ blockIndex, stepIndex, itemIndex }: ItemIndices) => {
setTypebot(
produce(typebot, (typebot) => {
const step = typebot.blocks[blockIndex].steps[
stepIndex
] as StepWithItems
const removingItem = step.items[itemIndex]
step.items.splice(itemIndex, 1)
cleanUpEdgeDraft(typebot, removingItem.id)
})
)
},
})
export { itemsAction }

View File

@ -1,142 +1,108 @@
import { import {
ChoiceInputStep,
Step, Step,
Typebot, Typebot,
DraggableStep, DraggableStep,
DraggableStepType, DraggableStepType,
defaultWebhookAttributes, StepIndices,
} from 'models' } from 'models'
import { parseNewStep } from 'services/typebots' import { parseNewStep } from 'services/typebots'
import { removeEmptyBlocks } from './blocks' 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 { isChoiceInput, isWebhookStep } from 'utils'
import { deleteEdgeDraft } from './edges'
import { createWebhookDraft, deleteWebhookDraft } from './webhooks'
import { SetTypebot } from '../TypebotContext' import { SetTypebot } from '../TypebotContext'
import produce from 'immer' import produce from 'immer'
import { cleanUpEdgeDraft } from './edges'
export type StepsActions = { export type StepsActions = {
createStep: ( createStep: (
blockId: string, blockId: string,
step?: DraggableStep | DraggableStepType, step: DraggableStep | DraggableStepType,
index?: number indices: StepIndices
) => void ) => void
updateStep: ( updateStep: (
stepId: string, indices: StepIndices,
updates: Partial<Omit<Step, 'id' | 'type'>> updates: Partial<Omit<Step, 'id' | 'type'>>
) => void ) => void
detachStepFromBlock: (stepId: string) => void detachStepFromBlock: (indices: StepIndices) => void
deleteStep: (stepId: string) => void deleteStep: (indices: StepIndices) => void
} }
export const stepsAction = ( const stepsAction = (
typebot: Typebot, typebot: Typebot,
setTypebot: SetTypebot setTypebot: SetTypebot
): StepsActions => ({ ): StepsActions => ({
createStep: ( createStep: (
blockId: string, blockId: string,
step?: DraggableStep | DraggableStepType, step: DraggableStep | DraggableStepType,
index?: number indices: StepIndices
) => { ) => {
if (!step) return
setTypebot( setTypebot(
produce(typebot, (typebot) => { produce(typebot, (typebot) => {
createStepDraft(typebot, step, blockId, index) createStepDraft(typebot, step, blockId, indices)
})
)
},
updateStep: (
{ blockIndex, stepIndex }: StepIndices,
updates: Partial<Omit<Step, 'id' | 'type'>>
) =>
setTypebot(
produce(typebot, (typebot) => {
const step = typebot.blocks[blockIndex].steps[stepIndex]
typebot.blocks[blockIndex].steps[stepIndex] = { ...step, ...updates }
})
),
detachStepFromBlock: (indices: StepIndices) => {
setTypebot(produce(typebot, removeStepFromBlock(indices)))
},
deleteStep: (indices: StepIndices) => {
setTypebot(
produce(typebot, (typebot) => {
removeStepFromBlock(indices)(typebot)
removeEmptyBlocks(typebot) removeEmptyBlocks(typebot)
}) })
) )
}, },
updateStep: (stepId: string, updates: Partial<Omit<Step, 'id' | 'type'>>) =>
setTypebot(
produce(typebot, (typebot) => {
typebot.steps.byId[stepId] = {
...typebot.steps.byId[stepId],
...updates,
}
})
),
detachStepFromBlock: (stepId: string) => {
setTypebot(
produce(typebot, (typebot) => {
removeStepIdFromBlock(typebot, stepId)
})
)
},
deleteStep: (stepId: string) => {
setTypebot(produce(typebot, deleteStepDraft(stepId)))
},
}) })
const removeStepIdFromBlock = ( const removeStepFromBlock =
typebot: WritableDraft<Typebot>, ({ blockIndex, stepIndex }: StepIndices) =>
stepId: string (typebot: WritableDraft<Typebot>) => {
) => { const removingStep = typebot.blocks[blockIndex].steps[stepIndex]
const containerBlock = typebot.blocks.byId[typebot.steps.byId[stepId].blockId] cleanUpEdgeDraft(typebot, removingStep.id)
containerBlock.stepIds.splice(containerBlock.stepIds.indexOf(stepId), 1) typebot.blocks[blockIndex].steps.splice(stepIndex, 1)
}
export const deleteStepDraft =
(stepId: string) => (typebot: WritableDraft<Typebot>) => {
const step = typebot.steps.byId[stepId]
if (isChoiceInput(step)) deleteChoiceItemsInsideStep(typebot, step)
if (isWebhookStep(step))
deleteWebhookDraft(step.options?.webhookId)(typebot)
deleteEdgeDraft(typebot, step.edgeId)
removeStepIdFromBlock(typebot, stepId)
delete typebot.steps.byId[stepId]
const index = typebot.steps.allIds.indexOf(stepId)
if (index !== -1) typebot.steps.allIds.splice(index, 1)
removeEmptyBlocks(typebot)
} }
export const createStepDraft = ( const createStepDraft = (
typebot: WritableDraft<Typebot>, typebot: WritableDraft<Typebot>,
step: DraggableStep | DraggableStepType, step: DraggableStep | DraggableStepType,
blockId: string, blockId: string,
index?: number indices: StepIndices
) => ) => {
typeof step === 'string' typeof step === 'string'
? createNewStep(typebot, step, blockId, index) ? createNewStep(typebot, step, blockId, indices)
: moveStepToBlock(typebot, step, blockId, index) : moveStepToBlock(typebot, step, blockId, indices)
removeEmptyBlocks(typebot)
}
const createNewStep = ( const createNewStep = (
typebot: WritableDraft<Typebot>, typebot: WritableDraft<Typebot>,
type: DraggableStepType, type: DraggableStepType,
blockId: string, blockId: string,
index?: number { blockIndex, stepIndex }: StepIndices
) => { ) => {
const newStep = parseNewStep(type, blockId) const newStep = parseNewStep(type, blockId)
typebot.steps.byId[newStep.id] = newStep typebot.blocks[blockIndex].steps.splice(stepIndex ?? 0, 0, newStep)
if (isChoiceInput(newStep)) {
createChoiceItemDraft(typebot, {
id: newStep.options.itemIds[0],
stepId: newStep.id,
})
} else if (isWebhookStep(newStep)) {
createWebhookDraft({
id: newStep.options.webhookId,
...defaultWebhookAttributes,
})(typebot)
}
typebot.steps.allIds.push(newStep.id)
typebot.blocks.byId[blockId].stepIds.splice(index ?? 0, 0, newStep.id)
} }
const moveStepToBlock = ( const moveStepToBlock = (
typebot: WritableDraft<Typebot>, typebot: WritableDraft<Typebot>,
step: DraggableStep, step: DraggableStep,
blockId: string, blockId: string,
index?: number { blockIndex, stepIndex }: StepIndices
) => {
typebot.steps.byId[step.id].blockId = blockId
typebot.blocks.byId[blockId].stepIds.splice(index ?? 0, 0, step.id)
}
const deleteChoiceItemsInsideStep = (
typebot: WritableDraft<Typebot>,
step: ChoiceInputStep
) => ) =>
step.options?.itemIds.forEach((itemId) => typebot.blocks[blockIndex].steps.splice(stepIndex ?? 0, 0, {
deleteChoiceItemDraft(typebot, itemId) ...step,
) blockId,
})
export { stepsAction, createStepDraft }

View File

@ -19,8 +19,7 @@ export const variablesAction = (
createVariable: (newVariable: Variable) => { createVariable: (newVariable: Variable) => {
setTypebot( setTypebot(
produce(typebot, (typebot) => { produce(typebot, (typebot) => {
typebot.variables.byId[newVariable.id] = newVariable typebot.variables.push(newVariable)
typebot.variables.allIds.push(newVariable.id)
}) })
) )
}, },
@ -30,10 +29,9 @@ export const variablesAction = (
) => ) =>
setTypebot( setTypebot(
produce(typebot, (typebot) => { produce(typebot, (typebot) => {
typebot.variables.byId[variableId] = { typebot.variables.map((v) =>
...typebot.variables.byId[variableId], v.id === variableId ? { ...v, ...updates } : v
...updates, )
}
}) })
), ),
deleteVariable: (itemId: string) => { deleteVariable: (itemId: string) => {
@ -49,7 +47,6 @@ export const deleteVariableDraft = (
typebot: WritableDraft<Typebot>, typebot: WritableDraft<Typebot>,
variableId: string variableId: string
) => { ) => {
delete typebot.variables.byId[variableId] const index = typebot.variables.findIndex((v) => v.id === variableId)
const index = typebot.variables.allIds.indexOf(variableId) typebot.variables.splice(index, 1)
if (index !== -1) typebot.variables.allIds.splice(index, 1)
} }

View File

@ -1,48 +0,0 @@
import { Typebot, Webhook } from 'models'
import { WritableDraft } from 'immer/dist/internal'
import { SetTypebot } from '../TypebotContext'
import { produce } from 'immer'
export type WebhooksAction = {
createWebhook: (webook: Webhook) => void
updateWebhook: (
webhookId: string,
updates: Partial<Omit<Webhook, 'id'>>
) => void
deleteWebhook: (variableId: string) => void
}
export const webhooksAction = (
typebot: Typebot,
setTypebot: SetTypebot
): WebhooksAction => ({
createWebhook: (newWebhook: Webhook) => {
setTypebot(produce(typebot, createWebhookDraft(newWebhook)))
},
updateWebhook: (webhookId: string, updates: Partial<Omit<Webhook, 'id'>>) =>
setTypebot(
produce(typebot, (typebot) => {
typebot.webhooks.byId[webhookId] = {
...typebot.webhooks.byId[webhookId],
...updates,
}
})
),
deleteWebhook: (webhookId: string) => {
setTypebot(produce(typebot, deleteWebhookDraft(webhookId)))
},
})
export const createWebhookDraft =
(newWebhook: Webhook) => (typebot: WritableDraft<Typebot>) => {
typebot.webhooks.byId[newWebhook.id] = newWebhook
typebot.webhooks.allIds.push(newWebhook.id)
}
export const deleteWebhookDraft =
(webhookId?: string) => (typebot: WritableDraft<Typebot>) => {
if (!webhookId) return
delete typebot.webhooks.byId[webhookId]
const index = typebot.webhooks.allIds.indexOf(webhookId)
if (index !== -1) typebot.webhooks.allIds.splice(index, 1)
}

View File

@ -1,6 +1,6 @@
import { Flex } from '@chakra-ui/react' import { Flex } from '@chakra-ui/react'
import React from 'react' import React from 'react'
import { StepDndContext } from 'contexts/StepDndContext' import { GraphDndContext } from 'contexts/GraphDndContext'
import { StepsSideBar } from '../../components/editor/StepsSideBar' import { StepsSideBar } from '../../components/editor/StepsSideBar'
import { PreviewDrawer } from '../../components/editor/preview/PreviewDrawer' import { PreviewDrawer } from '../../components/editor/preview/PreviewDrawer'
import { RightPanel, useEditor } from 'contexts/EditorContext' import { RightPanel, useEditor } from 'contexts/EditorContext'
@ -15,14 +15,14 @@ export const Board = () => {
return ( return (
<Flex flex="1" pos="relative" bgColor="gray.50" h="full"> <Flex flex="1" pos="relative" bgColor="gray.50" h="full">
<StepDndContext> <GraphDndContext>
<StepsSideBar /> <StepsSideBar />
<GraphProvider typebot={typebot}> <GraphProvider blocks={typebot?.blocks ?? []}>
<Graph flex="1" /> <Graph flex="1" />
<BoardMenuButton pos="absolute" right="40px" top="20px" /> <BoardMenuButton pos="absolute" right="40px" top="20px" />
{rightPanel === RightPanel.PREVIEW && <PreviewDrawer />} {rightPanel === RightPanel.PREVIEW && <PreviewDrawer />}
</GraphProvider> </GraphProvider>
</StepDndContext> </GraphDndContext>
</Flex> </Flex>
) )
} }

View File

@ -1,17 +1,13 @@
import { Flex } from '@chakra-ui/react' import { Flex } from '@chakra-ui/react'
import { TypebotViewer } from 'bot-engine' import { TypebotViewer } from 'bot-engine'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import React, { useMemo } from 'react' import React from 'react'
import { parseTypebotToPublicTypebot } from 'services/publicTypebot' import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
import { ThemeSideMenu } from '../../components/theme/ThemeSideMenu' import { ThemeSideMenu } from '../../components/theme/ThemeSideMenu'
export const ThemeContent = () => { export const ThemeContent = () => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
const publicTypebot = useMemo( const publicTypebot = typebot && parseTypebotToPublicTypebot(typebot)
() => (typebot ? parseTypebotToPublicTypebot(typebot) : undefined),
// eslint-disable-next-line react-hooks/exhaustive-deps
[typebot?.theme]
)
return ( return (
<Flex h="full" w="full"> <Flex h="full" w="full">
<ThemeSideMenu /> <ThemeSideMenu />

View File

@ -20,14 +20,19 @@ const App = ({ Component, pageProps }: AppProps) => {
useRouterProgressBar() useRouterProgressBar()
const { query } = useRouter() const { query } = useRouter()
const typebotId = query.typebotId?.toString()
return ( return (
<ChakraProvider theme={customTheme}> <ChakraProvider theme={customTheme}>
<KBarProvider actions={actions}> <KBarProvider actions={actions}>
<SessionProvider> <SessionProvider>
<UserContext> <UserContext>
<TypebotContext typebotId={query.typebotId?.toString()}> {typebotId ? (
<TypebotContext typebotId={typebotId}>
<Component />
</TypebotContext>
) : (
<Component {...pageProps} /> <Component {...pageProps} />
</TypebotContext> )}
</UserContext> </UserContext>
</SessionProvider> </SessionProvider>
</KBarProvider> </KBarProvider>

View File

@ -1,12 +1,5 @@
import prisma from 'libs/prisma' import prisma from 'libs/prisma'
import { import { KeyValue, Typebot, Variable, Webhook, WebhookResponse } from 'models'
KeyValue,
Table,
Typebot,
Variable,
Webhook,
WebhookResponse,
} from 'models'
import { parseVariables } from 'bot-engine' import { parseVariables } from 'bot-engine'
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import got, { Method, Headers, HTTPError } from 'got' import got, { Method, Headers, HTTPError } from 'got'
@ -16,13 +9,21 @@ import { stringify } from 'qs'
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'POST') { if (req.method === 'POST') {
const typebotId = req.query.typebotId.toString() const typebotId = req.query.typebotId.toString()
const webhookId = req.query.id.toString() const blockIndex = Number(req.query.blockIndex)
const variables = JSON.parse(req.body).variables as Table<Variable> const stepIndex = Number(req.query.stepIndex)
const variables = JSON.parse(req.body).variables as Variable[]
const typebot = await prisma.typebot.findUnique({ const typebot = await prisma.typebot.findUnique({
where: { id: typebotId }, where: { id: typebotId },
}) })
const webhook = (typebot as Typebot).webhooks.byId[webhookId] const step = (typebot as unknown as Typebot).blocks[blockIndex].steps[
const result = await executeWebhook(webhook, variables) stepIndex
]
if (!('webhook' in step))
return {
statusCode: 400,
data: { message: `Couldn't find webhook` },
}
const result = await executeWebhook(step.webhook, variables)
return res.status(200).send(result) return res.status(200).send(result)
} }
return methodNotAllowed(res) return methodNotAllowed(res)
@ -30,7 +31,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const executeWebhook = async ( const executeWebhook = async (
webhook: Webhook, webhook: Webhook,
variables: Table<Variable> variables: Variable[]
): Promise<WebhookResponse> => { ): Promise<WebhookResponse> => {
if (!webhook.url || !webhook.method) if (!webhook.url || !webhook.method)
return { return {
@ -87,12 +88,11 @@ const parseBody = (body: string) => {
} }
const convertKeyValueTableToObject = ( const convertKeyValueTableToObject = (
keyValues: Table<KeyValue> | undefined, keyValues: KeyValue[] | undefined,
variables: Table<Variable> variables: Variable[]
) => { ) => {
if (!keyValues) return if (!keyValues) return
return keyValues.allIds.reduce((object, id) => { return keyValues.reduce((object, item) => {
const item = keyValues.byId[id]
if (!item.key) return {} if (!item.key) return {}
return { return {
...object, ...object,

View File

@ -24,14 +24,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const answersCounts: { blockId: string; totalAnswers: number }[] = const answersCounts: { blockId: string; totalAnswers: number }[] =
await Promise.all( await Promise.all(
( (typebot.publishedTypebot as unknown as PublicTypebot).blocks.map(
typebot.publishedTypebot as unknown as PublicTypebot async (block) => {
).blocks.allIds.map(async (blockId) => { const totalAnswers = await prisma.answer.count({
const totalAnswers = await prisma.answer.count({ where: { blockId: block.id },
where: { blockId }, })
}) return { blockId: block.id, totalAnswers }
return { blockId, totalAnswers } }
}) )
) )
return res.status(200).send({ answersCounts }) return res.status(200).send({ answersCounts })
} }

View File

@ -8,8 +8,8 @@ const config: PlaywrightTestConfig = {
expect: { expect: {
timeout: 5000, timeout: 5000,
}, },
retries: 2, retries: process.env.NO_RETRIES ? 0 : 2,
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : 3,
reporter: 'html', reporter: 'html',
maxFailures: process.env.CI ? 10 : undefined, maxFailures: process.env.CI ? 10 : undefined,
use: { use: {

View File

@ -0,0 +1,185 @@
{
"id": "ckz84wbbj2095no1ali9kzfz4",
"createdAt": "2022-02-04T08:16:59.215Z",
"updatedAt": "2022-02-04T08:16:59.215Z",
"name": "My typebot",
"ownerId": "ckz6t9iep0006k31a22j05fwq",
"publishedTypebotId": null,
"folderId": null,
"blocks": [
{
"id": "de8iZbvNxMxyhvLrnPBpt8",
"steps": [
{
"id": "hqgG9FuPDWrkrdHXFnSy9G",
"type": "start",
"label": "Start",
"blockId": "de8iZbvNxMxyhvLrnPBpt8",
"outgoingEdgeId": "41aa19ih9WQQQEurwdjmVJ"
}
],
"title": "Start",
"graphCoordinates": { "x": 0, "y": 0 }
},
{
"id": "vmDTsAC7aLeqanVVtJ9yQx",
"graphCoordinates": { "x": 85, "y": 220 },
"title": "Block #1",
"steps": [
{
"id": "sqUp2x8SXx8JBC8a9XuKGL9",
"blockId": "vmDTsAC7aLeqanVVtJ9yQx",
"type": "text",
"content": {
"html": "<div>Hello!</div>",
"richText": [{ "type": "p", "children": [{ "text": "Hello!" }] }],
"plainText": "Hello!"
}
},
{
"id": "suRXuWyuJ7kpsdLUYKA6VqM",
"blockId": "vmDTsAC7aLeqanVVtJ9yQx",
"type": "text",
"content": {
"html": "<div>How are you?</div>",
"richText": [
{ "type": "p", "children": [{ "text": "How are you?" }] }
],
"plainText": "How are you?"
}
},
{
"id": "ssxDdzVUkgZYPPoPnQK4dCo",
"blockId": "vmDTsAC7aLeqanVVtJ9yQx",
"type": "choice input",
"options": { "buttonLabel": "Send", "isMultipleChoice": false },
"items": [
{
"id": "e7dy3bH2py8fFcak2jUJjr",
"stepId": "ssxDdzVUkgZYPPoPnQK4dCo",
"type": 0,
"content": "Item 1",
"outgoingEdgeId": "8Ty7noiTJAP3jtaWXLsNwy"
},
{
"stepId": "ssxDdzVUkgZYPPoPnQK4dCo",
"type": 0,
"id": "mBJkTavGHAygmPTjiLMQyC",
"content": "Item 2"
},
{
"stepId": "ssxDdzVUkgZYPPoPnQK4dCo",
"type": 0,
"id": "vVfToFyNLyGgRYyB8jYLkn",
"content": "Item 3",
"outgoingEdgeId": "tprSzPvt6A5kTFf7iUNaeR"
}
]
}
]
},
{
"id": "hB4p8rwA1dUSq9A5ctTLqh",
"graphCoordinates": { "x": 513, "y": 152 },
"title": "Block #2",
"steps": [
{
"id": "suHw7fjcMD9KjDRpbB413jn",
"blockId": "hB4p8rwA1dUSq9A5ctTLqh",
"type": "Condition",
"items": [
{
"id": "jvGN6sfftqJgfYYVRUYMuJ",
"stepId": "suHw7fjcMD9KjDRpbB413jn",
"type": 1,
"content": { "comparisons": [], "logicalOperator": "AND" }
}
]
}
]
},
{
"id": "t7g44CwVvCg6mN16KHVAWv",
"graphCoordinates": { "x": 509, "y": 489 },
"title": "Block #3",
"steps": [
{
"id": "suHztCMVss4kTAtgShANxjU",
"blockId": "t7g44CwVvCg6mN16KHVAWv",
"type": "choice input",
"options": { "buttonLabel": "Send", "isMultipleChoice": false },
"items": [
{
"id": "buQjAL2M3cBUVK2ofnxKW3",
"stepId": "suHztCMVss4kTAtgShANxjU",
"type": 0,
"content": "Item 2-1"
},
{
"stepId": "suHztCMVss4kTAtgShANxjU",
"type": 0,
"id": "4gQe9XK1vyQUHXVzFErW4t",
"content": "Item 2-2"
},
{
"stepId": "suHztCMVss4kTAtgShANxjU",
"type": 0,
"id": "uttagH8w5XWzibkKsW23oi",
"content": "Item 2-3"
}
]
}
]
}
],
"variables": [],
"edges": [
{
"from": {
"blockId": "de8iZbvNxMxyhvLrnPBpt8",
"stepId": "hqgG9FuPDWrkrdHXFnSy9G"
},
"to": { "blockId": "vmDTsAC7aLeqanVVtJ9yQx" },
"id": "41aa19ih9WQQQEurwdjmVJ"
},
{
"from": {
"blockId": "vmDTsAC7aLeqanVVtJ9yQx",
"stepId": "ssxDdzVUkgZYPPoPnQK4dCo",
"itemId": "e7dy3bH2py8fFcak2jUJjr"
},
"to": { "blockId": "hB4p8rwA1dUSq9A5ctTLqh" },
"id": "8Ty7noiTJAP3jtaWXLsNwy"
},
{
"from": {
"blockId": "vmDTsAC7aLeqanVVtJ9yQx",
"stepId": "ssxDdzVUkgZYPPoPnQK4dCo",
"itemId": "vVfToFyNLyGgRYyB8jYLkn"
},
"to": { "blockId": "t7g44CwVvCg6mN16KHVAWv" },
"id": "tprSzPvt6A5kTFf7iUNaeR"
}
],
"theme": {
"chat": {
"inputs": {
"color": "#303235",
"backgroundColor": "#FFFFFF",
"placeholderColor": "#9095A0"
},
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
},
"general": { "font": "Open Sans", "background": { "type": "None" } }
},
"settings": {
"general": { "isBrandingEnabled": true },
"metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
},
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
},
"publicId": null
}

View File

@ -1,98 +1,76 @@
{ {
"id": "ckylszb9z0354z31a623dg7ji", "id": "ckz8gg4n39573no1aa5rsfyp1",
"createdAt": "2022-01-19T17:12:27.863Z", "createdAt": "2022-02-04T13:40:19.455Z",
"updatedAt": "2022-01-19T17:12:27.863Z", "updatedAt": "2022-02-04T13:40:19.455Z",
"name": "My typebot", "name": "My typebot",
"ownerId": "ckylsz8yy0335z31amvq0jwtt", "ownerId": "ckz6t9iep0006k31a22j05fwq",
"publishedTypebotId": null, "publishedTypebotId": null,
"folderId": null, "folderId": null,
"webhooks": { "byId": {}, "allIds": [] }, "blocks": [
"blocks": { {
"byId": { "id": "bSHn2HQZ1sKji5pd9Nmejf",
"j24wz82YG3rjXMgrmCiTLy": { "steps": [
"id": "j24wz82YG3rjXMgrmCiTLy", {
"title": "Start", "id": "qYmbSFBxCvGCgTvQTg9HeH",
"stepIds": ["1NdXPCiRicqDA8k4JfnXfi"], "type": "start",
"graphCoordinates": { "x": 0, "y": 0 } "label": "Start",
}, "blockId": "bSHn2HQZ1sKji5pd9Nmejf",
"bmaKTUXkT2cc3wtKfK7ra71": { "outgoingEdgeId": "jdQnqJK4b559rTJzHHhjcz"
"id": "bmaKTUXkT2cc3wtKfK7ra71", }
"title": "Block #2", ],
"graphCoordinates": { "x": 175, "y": 197 }, "title": "Start",
"stepIds": ["spHxPWbSqkVZW9gqH86ovC5"] "graphCoordinates": { "x": 0, "y": 0 }
},
"bnt8fM5Wgc8gBg2iSmUcfJu": {
"id": "bnt8fM5Wgc8gBg2iSmUcfJu",
"title": "Block #3",
"graphCoordinates": { "x": 504, "y": 347 },
"stepIds": ["siPoEE9H27hVHqykth3a7Kj"]
}
}, },
"allIds": [ {
"j24wz82YG3rjXMgrmCiTLy", "id": "mSvUFogQH16bQDN1iGWF66",
"bmaKTUXkT2cc3wtKfK7ra71", "graphCoordinates": { "x": 324, "y": 209 },
"bnt8fM5Wgc8gBg2iSmUcfJu" "title": "Block #1",
] "steps": [
}, {
"steps": { "id": "spDLmDCZfNJu4DrZ1MUg84c",
"byId": { "blockId": "mSvUFogQH16bQDN1iGWF66",
"1NdXPCiRicqDA8k4JfnXfi": { "type": "email input",
"id": "1NdXPCiRicqDA8k4JfnXfi", "options": {
"type": "start", "labels": { "button": "Send", "placeholder": "Type your email..." },
"label": "Start", "variableId": "qyLW6xD1AyLeedso2tHmhw"
"blockId": "j24wz82YG3rjXMgrmCiTLy", },
"edgeId": "benDCcLMUWNvi6Fg6CXE9H" "outgoingEdgeId": "4yg9V76fdDntpDEw6H3tvU"
}, }
"spHxPWbSqkVZW9gqH86ovC5": { ]
"id": "spHxPWbSqkVZW9gqH86ovC5",
"blockId": "bmaKTUXkT2cc3wtKfK7ra71",
"type": "email input",
"options": { "variableId": "oexLr4sJQNVdSnYCGgGRB3" },
"edgeId": "6Tax9rw7L8kmRn9JRD2Mrg"
},
"siPoEE9H27hVHqykth3a7Kj": {
"id": "siPoEE9H27hVHqykth3a7Kj",
"blockId": "bnt8fM5Wgc8gBg2iSmUcfJu",
"type": "Google Sheets"
}
}, },
"allIds": [ {
"1NdXPCiRicqDA8k4JfnXfi", "id": "jd4S6BQsUQ2RuKieHXYKs9",
"spHxPWbSqkVZW9gqH86ovC5", "graphCoordinates": { "x": 655, "y": 363 },
"siPoEE9H27hVHqykth3a7Kj" "title": "Block #2",
] "steps": [
}, {
"choiceItems": { "byId": {}, "allIds": [] }, "id": "s1ZvUqWxV6b8UgFGYWh39pV",
"variables": { "blockId": "jd4S6BQsUQ2RuKieHXYKs9",
"byId": { "type": "Google Sheets",
"oexLr4sJQNVdSnYCGgGRB3": { "options": {}
"id": "oexLr4sJQNVdSnYCGgGRB3", }
"name": "Email" ]
} }
}, ],
"allIds": ["oexLr4sJQNVdSnYCGgGRB3"] "variables": [{ "id": "qyLW6xD1AyLeedso2tHmhw", "name": "Email" }],
}, "edges": [
"edges": { {
"byId": { "from": {
"benDCcLMUWNvi6Fg6CXE9H": { "blockId": "bSHn2HQZ1sKji5pd9Nmejf",
"from": { "stepId": "qYmbSFBxCvGCgTvQTg9HeH"
"blockId": "j24wz82YG3rjXMgrmCiTLy",
"stepId": "1NdXPCiRicqDA8k4JfnXfi"
},
"to": { "blockId": "bmaKTUXkT2cc3wtKfK7ra71" },
"id": "benDCcLMUWNvi6Fg6CXE9H"
}, },
"6Tax9rw7L8kmRn9JRD2Mrg": { "to": { "blockId": "mSvUFogQH16bQDN1iGWF66" },
"from": { "id": "jdQnqJK4b559rTJzHHhjcz"
"blockId": "bmaKTUXkT2cc3wtKfK7ra71",
"stepId": "spHxPWbSqkVZW9gqH86ovC5"
},
"to": { "blockId": "bnt8fM5Wgc8gBg2iSmUcfJu" },
"id": "6Tax9rw7L8kmRn9JRD2Mrg"
}
}, },
"allIds": ["benDCcLMUWNvi6Fg6CXE9H", "6Tax9rw7L8kmRn9JRD2Mrg"] {
}, "from": {
"blockId": "mSvUFogQH16bQDN1iGWF66",
"stepId": "spDLmDCZfNJu4DrZ1MUg84c"
},
"to": { "blockId": "jd4S6BQsUQ2RuKieHXYKs9" },
"id": "4yg9V76fdDntpDEw6H3tvU"
}
],
"theme": { "theme": {
"chat": { "chat": {
"inputs": { "inputs": {

View File

@ -1,136 +1,109 @@
{ {
"id": "ckyltevlb0559z31an8cmkyrp", "id": "ckz8gg4n39573no1aa5rsfyp1",
"createdAt": "2022-01-19T17:24:34.031Z", "createdAt": "2022-02-04T13:40:19.455Z",
"updatedAt": "2022-01-19T17:24:34.031Z", "updatedAt": "2022-02-04T13:40:19.455Z",
"name": "My typebot", "name": "My typebot",
"ownerId": "ckyltekzq0533z31ad8opmacz", "ownerId": "ckz6t9iep0006k31a22j05fwq",
"publishedTypebotId": null, "publishedTypebotId": null,
"folderId": null, "folderId": null,
"webhooks": { "byId": {}, "allIds": [] }, "blocks": [
"blocks": { {
"byId": { "id": "bSHn2HQZ1sKji5pd9Nmejf",
"kPupUcEn7TcBGKHUpgK2Q5": { "steps": [
"id": "kPupUcEn7TcBGKHUpgK2Q5", {
"title": "Start", "id": "qYmbSFBxCvGCgTvQTg9HeH",
"stepIds": ["nP5oWm7PxigMupyWpPLq24"], "type": "start",
"graphCoordinates": { "x": 0, "y": 0 } "label": "Start",
}, "blockId": "bSHn2HQZ1sKji5pd9Nmejf",
"bi4J5fv9DFn1zPSqGf8eRht": { "outgoingEdgeId": "jdQnqJK4b559rTJzHHhjcz"
"id": "bi4J5fv9DFn1zPSqGf8eRht",
"title": "Block #2",
"graphCoordinates": { "x": 104, "y": 201 },
"stepIds": ["scH9qdXwFfAScoavj6UzQNT"]
},
"bwqGhUsa2SKaxXSrKtapVc8": {
"id": "bwqGhUsa2SKaxXSrKtapVc8",
"title": "Block #3",
"graphCoordinates": { "x": 458, "y": 292 },
"stepIds": ["shZdc8Qw48domEbS7vLW5eN"]
},
"bqmgS9hLUu2RA2oxVv7hMka": {
"id": "bqmgS9hLUu2RA2oxVv7hMka",
"title": "Block #4",
"graphCoordinates": { "x": 102, "y": 432 },
"stepIds": ["s4z6G3MGAyhXChU9jakQWer"]
}
},
"allIds": [
"kPupUcEn7TcBGKHUpgK2Q5",
"bi4J5fv9DFn1zPSqGf8eRht",
"bwqGhUsa2SKaxXSrKtapVc8",
"bqmgS9hLUu2RA2oxVv7hMka"
]
},
"steps": {
"byId": {
"nP5oWm7PxigMupyWpPLq24": {
"id": "nP5oWm7PxigMupyWpPLq24",
"type": "start",
"label": "Start",
"blockId": "kPupUcEn7TcBGKHUpgK2Q5",
"edgeId": "kCLXGLpiM2F6pn4wFYc1f5"
},
"scH9qdXwFfAScoavj6UzQNT": {
"id": "scH9qdXwFfAScoavj6UzQNT",
"blockId": "bi4J5fv9DFn1zPSqGf8eRht",
"type": "email input",
"options": { "variableId": "ifXp66N1meAtoUDcbqWxuD" },
"edgeId": "7Czn5hJFUfpkRGtwGnKxtt"
},
"shZdc8Qw48domEbS7vLW5eN": {
"id": "shZdc8Qw48domEbS7vLW5eN",
"blockId": "bwqGhUsa2SKaxXSrKtapVc8",
"type": "Google Sheets",
"edgeId": "eMhGokwHMDRDrynSvpiRje"
},
"s4z6G3MGAyhXChU9jakQWer": {
"id": "s4z6G3MGAyhXChU9jakQWer",
"blockId": "bqmgS9hLUu2RA2oxVv7hMka",
"type": "text",
"content": {
"html": "<div>Your name is: {{First name}} {{Last name}}</div>",
"richText": [
{
"type": "p",
"children": [
{ "text": "Your name is: {{First name}} {{Last name}}" }
]
}
],
"plainText": "Your name is: {{First name}} {{Last name}}"
} }
} ],
"title": "Start",
"graphCoordinates": { "x": 0, "y": 0 }
}, },
"allIds": [ {
"nP5oWm7PxigMupyWpPLq24", "id": "mSvUFogQH16bQDN1iGWF66",
"scH9qdXwFfAScoavj6UzQNT", "graphCoordinates": { "x": 324, "y": 209 },
"shZdc8Qw48domEbS7vLW5eN", "title": "Block #1",
"s4z6G3MGAyhXChU9jakQWer" "steps": [
] {
}, "id": "spDLmDCZfNJu4DrZ1MUg84c",
"choiceItems": { "byId": {}, "allIds": [] }, "blockId": "mSvUFogQH16bQDN1iGWF66",
"variables": { "type": "email input",
"byId": { "options": {
"ifXp66N1meAtoUDcbqWxuD": { "labels": { "button": "Send", "placeholder": "Type your email..." },
"id": "ifXp66N1meAtoUDcbqWxuD", "variableId": "qyLW6xD1AyLeedso2tHmhw"
"name": "Email" },
} "outgoingEdgeId": "4yg9V76fdDntpDEw6H3tvU"
}
]
}, },
"allIds": ["ifXp66N1meAtoUDcbqWxuD"] {
}, "id": "jd4S6BQsUQ2RuKieHXYKs9",
"edges": { "graphCoordinates": { "x": 655, "y": 363 },
"byId": { "title": "Block #2",
"kCLXGLpiM2F6pn4wFYc1f5": { "steps": [
"from": { {
"blockId": "kPupUcEn7TcBGKHUpgK2Q5", "id": "s1ZvUqWxV6b8UgFGYWh39pV",
"stepId": "nP5oWm7PxigMupyWpPLq24" "blockId": "jd4S6BQsUQ2RuKieHXYKs9",
}, "type": "Google Sheets",
"to": { "blockId": "bi4J5fv9DFn1zPSqGf8eRht" }, "options": {},
"id": "kCLXGLpiM2F6pn4wFYc1f5" "outgoingEdgeId": "tBsPNYzMW1mMSvFMHZpmx8"
}
]
},
{
"id": "pd3PECJqHB9xHMfc52SbrZ",
"graphCoordinates": { "x": 292, "y": 509 },
"title": "Block #3",
"steps": [
{
"id": "sdECvSYszxBaZHH5TuWm11h",
"blockId": "pd3PECJqHB9xHMfc52SbrZ",
"type": "text",
"content": {
"html": "<div>Your name is: {{First name}} {{Last name}}</div>",
"richText": [
{
"type": "p",
"children": [
{ "text": "Your name is: {{First name}} {{Last name}}" }
]
}
],
"plainText": "Your name is: {{First name}} {{Last name}}"
}
}
]
}
],
"variables": [{ "id": "qyLW6xD1AyLeedso2tHmhw", "name": "Email" }],
"edges": [
{
"from": {
"blockId": "bSHn2HQZ1sKji5pd9Nmejf",
"stepId": "qYmbSFBxCvGCgTvQTg9HeH"
}, },
"7Czn5hJFUfpkRGtwGnKxtt": { "to": { "blockId": "mSvUFogQH16bQDN1iGWF66" },
"from": { "id": "jdQnqJK4b559rTJzHHhjcz"
"blockId": "bi4J5fv9DFn1zPSqGf8eRht",
"stepId": "scH9qdXwFfAScoavj6UzQNT"
},
"to": { "blockId": "bwqGhUsa2SKaxXSrKtapVc8" },
"id": "7Czn5hJFUfpkRGtwGnKxtt"
},
"eMhGokwHMDRDrynSvpiRje": {
"from": {
"blockId": "bwqGhUsa2SKaxXSrKtapVc8",
"stepId": "shZdc8Qw48domEbS7vLW5eN"
},
"to": { "blockId": "bqmgS9hLUu2RA2oxVv7hMka" },
"id": "eMhGokwHMDRDrynSvpiRje"
}
}, },
"allIds": [ {
"kCLXGLpiM2F6pn4wFYc1f5", "from": {
"7Czn5hJFUfpkRGtwGnKxtt", "blockId": "mSvUFogQH16bQDN1iGWF66",
"eMhGokwHMDRDrynSvpiRje" "stepId": "spDLmDCZfNJu4DrZ1MUg84c"
] },
}, "to": { "blockId": "jd4S6BQsUQ2RuKieHXYKs9" },
"id": "4yg9V76fdDntpDEw6H3tvU"
},
{
"from": {
"blockId": "jd4S6BQsUQ2RuKieHXYKs9",
"stepId": "s1ZvUqWxV6b8UgFGYWh39pV"
},
"to": { "blockId": "pd3PECJqHB9xHMfc52SbrZ" },
"id": "tBsPNYzMW1mMSvFMHZpmx8"
}
],
"theme": { "theme": {
"chat": { "chat": {
"inputs": { "inputs": {

View File

@ -1,155 +1,103 @@
{ {
"id": "ckz478ggv1144eo1a5euf9twl", "id": "ckz8gli9e9842no1afuppdn0z",
"createdAt": "2022-02-01T14:11:20.287Z", "createdAt": "2022-02-04T13:44:30.386Z",
"updatedAt": "2022-02-01T14:11:20.286Z", "updatedAt": "2022-02-04T13:44:30.386Z",
"name": "Webhook", "name": "My typebot",
"ownerId": "ckz478eaj1091eo1amyo1me1z", "ownerId": "ckz6t9iep0006k31a22j05fwq",
"publishedTypebotId": null, "publishedTypebotId": null,
"folderId": null, "folderId": null,
"blocks": { "blocks": [
"byId": { {
"q7gjzJu7wBFycca5dNvZek": { "id": "p6GeeRXHgwiJeoJRBkKaMJ",
"id": "q7gjzJu7wBFycca5dNvZek", "steps": [
"title": "Start", {
"stepIds": ["da1KErxMzczHwaM25vQtFP"], "id": "iDS7jFemUsQ7Sp3eu3xg3w",
"graphCoordinates": { "x": 0, "y": 0 } "type": "start",
}, "label": "Start",
"bUUqjxyAFZkKzjByqEaEzV": { "blockId": "p6GeeRXHgwiJeoJRBkKaMJ",
"id": "bUUqjxyAFZkKzjByqEaEzV", "outgoingEdgeId": "cyEJPaLU7AchnBSaeWoyiS"
"graphCoordinates": { "x": 248, "y": 247 },
"title": "Block 1",
"stepIds": ["siAj9x5LZ8W5cviqznX82T3", "s5GToHCtqZhwpygDuTb3tu4"]
},
"ifpYvoBnYU2X3B3RgwfeNJ": {
"id": "ifpYvoBnYU2X3B3RgwfeNJ",
"graphCoordinates": { "x": 690, "y": 504 },
"title": "Block 2",
"stepIds": ["sjDhaBWVLd2Ep7N3WryJGQJ"]
}
},
"allIds": [
"q7gjzJu7wBFycca5dNvZek",
"bUUqjxyAFZkKzjByqEaEzV",
"ifpYvoBnYU2X3B3RgwfeNJ"
]
},
"steps": {
"byId": {
"da1KErxMzczHwaM25vQtFP": {
"id": "da1KErxMzczHwaM25vQtFP",
"type": "start",
"label": "Start",
"blockId": "q7gjzJu7wBFycca5dNvZek",
"edgeId": "mcxdssnDkbvJBZ6d51XDey"
},
"s5GToHCtqZhwpygDuTb3tu4": {
"id": "s5GToHCtqZhwpygDuTb3tu4",
"blockId": "bUUqjxyAFZkKzjByqEaEzV",
"type": "choice input",
"options": {
"buttonLabel": "Send",
"isMultipleChoice": false,
"itemIds": ["ddSjZkft27gQnZAEeXtQny"]
} }
}, ],
"siAj9x5LZ8W5cviqznX82T3": { "title": "Start",
"id": "siAj9x5LZ8W5cviqznX82T3", "graphCoordinates": { "x": 0, "y": 0 }
"blockId": "bUUqjxyAFZkKzjByqEaEzV",
"type": "text",
"content": {
"html": "<div>Ready?</div>",
"richText": [{ "type": "p", "children": [{ "text": "Ready?" }] }],
"plainText": "Ready?"
}
},
"sjDhaBWVLd2Ep7N3WryJGQJ": {
"id": "sjDhaBWVLd2Ep7N3WryJGQJ",
"blockId": "ifpYvoBnYU2X3B3RgwfeNJ",
"type": "Webhook",
"options": {
"responseVariableMapping": { "byId": {}, "allIds": [] },
"variablesForTest": { "byId": {}, "allIds": [] },
"webhookId": "3nxQGoMMXpA6K5iuhGFW5S"
}
}
}, },
"allIds": [ {
"da1KErxMzczHwaM25vQtFP", "id": "kBneEpKdMYrF65XxUQ5GS7",
"s5GToHCtqZhwpygDuTb3tu4", "graphCoordinates": { "x": 260, "y": 186 },
"siAj9x5LZ8W5cviqznX82T3", "title": "Block #1",
"sjDhaBWVLd2Ep7N3WryJGQJ" "steps": [
] {
}, "id": "skSkZ4PNP7m1gYvu9Ew6ngM",
"choiceItems": { "blockId": "kBneEpKdMYrF65XxUQ5GS7",
"byId": { "type": "text",
"ddSjZkft27gQnZAEeXtQny": { "content": {
"id": "ddSjZkft27gQnZAEeXtQny", "html": "<div>Ready?</div>",
"stepId": "s5GToHCtqZhwpygDuTb3tu4", "richText": [{ "type": "p", "children": [{ "text": "Ready?" }] }],
"content": "Go", "plainText": "Ready?"
"edgeId": "x6cbRGrLAVYy4ymAg5tfp9" }
}
},
"allIds": ["ddSjZkft27gQnZAEeXtQny"]
},
"variables": {
"byId": {
"oASkBtoLqkYNqeakcjZH4L": {
"id": "oASkBtoLqkYNqeakcjZH4L",
"name": "secret 1"
},
"4tvkRmf32wiTsXrYoqyhfr": {
"id": "4tvkRmf32wiTsXrYoqyhfr",
"name": "secret 2"
},
"jEg1FvkCU5S5owNAxXFsHL": {
"id": "jEg1FvkCU5S5owNAxXFsHL",
"name": "secret 3"
},
"rEoE1ehHzgx8X3d3UPGDHg": {
"id": "rEoE1ehHzgx8X3d3UPGDHg",
"name": "secret 4"
}
},
"allIds": [
"oASkBtoLqkYNqeakcjZH4L",
"4tvkRmf32wiTsXrYoqyhfr",
"jEg1FvkCU5S5owNAxXFsHL",
"rEoE1ehHzgx8X3d3UPGDHg"
]
},
"webhooks": {
"byId": {
"3nxQGoMMXpA6K5iuhGFW5S": {
"id": "3nxQGoMMXpA6K5iuhGFW5S",
"method": "GET",
"headers": { "byId": {}, "allIds": [] },
"queryParams": { "byId": {}, "allIds": [] }
}
},
"allIds": ["3nxQGoMMXpA6K5iuhGFW5S"]
},
"edges": {
"byId": {
"mcxdssnDkbvJBZ6d51XDey": {
"from": {
"blockId": "q7gjzJu7wBFycca5dNvZek",
"stepId": "da1KErxMzczHwaM25vQtFP"
}, },
"to": { "blockId": "bUUqjxyAFZkKzjByqEaEzV" }, {
"id": "mcxdssnDkbvJBZ6d51XDey" "id": "sh6ZVRA3o72y6BEiNKVcoma",
}, "blockId": "kBneEpKdMYrF65XxUQ5GS7",
"x6cbRGrLAVYy4ymAg5tfp9": { "type": "choice input",
"from": { "options": { "buttonLabel": "Send", "isMultipleChoice": false },
"blockId": "bUUqjxyAFZkKzjByqEaEzV", "items": [
"stepId": "s5GToHCtqZhwpygDuTb3tu4", {
"buttonId": "ddSjZkft27gQnZAEeXtQny" "id": "rr5mKKBPq73ZrfXZ3uuupz",
}, "stepId": "sh6ZVRA3o72y6BEiNKVcoma",
"to": { "blockId": "ifpYvoBnYU2X3B3RgwfeNJ" }, "type": 0,
"id": "x6cbRGrLAVYy4ymAg5tfp9" "content": "Go",
} "outgoingEdgeId": "1sLicz8gq2QxytFTwBd8ac"
}
]
}
]
}, },
"allIds": ["mcxdssnDkbvJBZ6d51XDey", "x6cbRGrLAVYy4ymAg5tfp9"] {
}, "id": "8XnDM1QsqPms4LQHh8q3Jo",
"graphCoordinates": { "x": 646, "y": 511 },
"title": "Block #2",
"steps": [
{
"id": "soSmiE7zyb3WF77GxFxAjYX",
"blockId": "8XnDM1QsqPms4LQHh8q3Jo",
"type": "Webhook",
"options": { "responseVariableMapping": [], "variablesForTest": [] },
"webhook": {
"id": "2L9mPYsLAXdXwcnGVK6pv9",
"method": "GET",
"headers": [],
"queryParams": []
}
}
]
}
],
"variables": [
{ "id": "var1", "name": "secret 1" },
{ "id": "var2", "name": "secret 2" },
{ "id": "var3", "name": "secret 3" },
{ "id": "var4", "name": "secret 4" }
],
"edges": [
{
"from": {
"blockId": "p6GeeRXHgwiJeoJRBkKaMJ",
"stepId": "iDS7jFemUsQ7Sp3eu3xg3w"
},
"to": { "blockId": "kBneEpKdMYrF65XxUQ5GS7" },
"id": "cyEJPaLU7AchnBSaeWoyiS"
},
{
"from": {
"blockId": "kBneEpKdMYrF65XxUQ5GS7",
"stepId": "sh6ZVRA3o72y6BEiNKVcoma",
"itemId": "rr5mKKBPq73ZrfXZ3uuupz"
},
"to": { "blockId": "8XnDM1QsqPms4LQHh8q3Jo" },
"id": "1sLicz8gq2QxytFTwBd8ac"
}
],
"theme": { "theme": {
"chat": { "chat": {
"inputs": { "inputs": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -1,275 +0,0 @@
{
"id": "bdFW2HHjMoEFmqHtFre9Xi8",
"createdAt": "2022-01-21T07:55:14.727Z",
"updatedAt": "2022-01-21T07:55:14.727Z",
"name": "My typebot",
"ownerId": "user2",
"publishedTypebotId": null,
"folderId": null,
"blocks": {
"byId": {
"3kH2sUjVThQDWmqdoKnGk5": {
"id": "3kH2sUjVThQDWmqdoKnGk5",
"title": "Start",
"stepIds": ["oxTsU2C1RX5QHuyY8qjHAM"],
"graphCoordinates": { "x": 42, "y": 13 }
},
"b9mSgu7RKmK4xuiTVQP5Me8": {
"id": "b9mSgu7RKmK4xuiTVQP5Me8",
"title": "Block #3",
"stepIds": ["ssLd2wjExS9qWRur4tZDU1Z"],
"graphCoordinates": { "x": 300, "y": 550 }
},
"bdFW2HHjMoEFmqHtFre9Xi8": {
"id": "bdFW2HHjMoEFmqHtFre9Xi8",
"title": "Block #2",
"stepIds": ["sgkADMK25y9P9V3vjwjBaac", "ssEiEECKSFkA44dGDceHxKw"],
"graphCoordinates": { "x": 121, "y": 227 }
},
"bmz4rc8r19H2C6b7soxzby4": {
"id": "bmz4rc8r19H2C6b7soxzby4",
"title": "Block #4",
"graphCoordinates": { "x": 632, "y": 279 },
"stepIds": ["sgTWsRM1qF2YoYLuGo3Z3pU"]
}
},
"allIds": [
"3kH2sUjVThQDWmqdoKnGk5",
"bdFW2HHjMoEFmqHtFre9Xi8",
"b9mSgu7RKmK4xuiTVQP5Me8",
"bmz4rc8r19H2C6b7soxzby4"
]
},
"steps": {
"byId": {
"oxTsU2C1RX5QHuyY8qjHAM": {
"id": "oxTsU2C1RX5QHuyY8qjHAM",
"type": "start",
"label": "Start",
"edgeId": "25yX9DnQgdafpdAjfAu5Fp",
"blockId": "3kH2sUjVThQDWmqdoKnGk5"
},
"sgkADMK25y9P9V3vjwjBaac": {
"id": "sgkADMK25y9P9V3vjwjBaac",
"type": "text",
"blockId": "bdFW2HHjMoEFmqHtFre9Xi8",
"content": {
"html": "<div>Ready?</div>",
"richText": [{ "type": "p", "children": [{ "text": "Ready?" }] }],
"plainText": "Ready?"
}
},
"ssEiEECKSFkA44dGDceHxKw": {
"id": "ssEiEECKSFkA44dGDceHxKw",
"type": "choice input",
"edgeId": "oxEEtym3NfDf34NCipzjRQ",
"blockId": "bdFW2HHjMoEFmqHtFre9Xi8",
"options": { "itemIds": ["q69Ex7LacPrH9QUMeosRnB"] }
},
"ssLd2wjExS9qWRur4tZDU1Z": {
"id": "ssLd2wjExS9qWRur4tZDU1Z",
"type": "Webhook",
"blockId": "b9mSgu7RKmK4xuiTVQP5Me8",
"options": {
"webhookId": "4h4Kk3Q1qGy7gFzpZtWVpU",
"variablesForTest": {
"byId": {
"6pMn1xm1y3xWVSdJetMAJH": {
"id": "6pMn1xm1y3xWVSdJetMAJH",
"variableId": "oASkBtoLqkYNqeakcjZH4L",
"value": "secret1"
},
"ettAiB75uoFWnJyPS7gn5k": {
"id": "ettAiB75uoFWnJyPS7gn5k",
"variableId": "4tvkRmf32wiTsXrYoqyhfr",
"value": "secret2"
},
"kKpD3Q4YvFQ7CGWiZxJF4s": {
"id": "kKpD3Q4YvFQ7CGWiZxJF4s",
"variableId": "jEg1FvkCU5S5owNAxXFsHL",
"value": "secret3"
},
"xjUC5Q3msXCw9fwqpNdoSx": {
"id": "xjUC5Q3msXCw9fwqpNdoSx",
"variableId": "rEoE1ehHzgx8X3d3UPGDHg",
"value": "secret4"
}
},
"allIds": [
"6pMn1xm1y3xWVSdJetMAJH",
"ettAiB75uoFWnJyPS7gn5k",
"kKpD3Q4YvFQ7CGWiZxJF4s",
"xjUC5Q3msXCw9fwqpNdoSx"
]
},
"responseVariableMapping": {
"byId": {
"o53h6M1sgHJfDTY5C3YEaT": {
"id": "o53h6M1sgHJfDTY5C3YEaT",
"bodyPath": "data[0].name",
"variableId": "4kVx5uf8W1XP6WsfJEvt8v"
}
},
"allIds": ["o53h6M1sgHJfDTY5C3YEaT"]
}
},
"edgeId": "81SjKnxuUgrPmXvvJJihHM"
},
"sgTWsRM1qF2YoYLuGo3Z3pU": {
"id": "sgTWsRM1qF2YoYLuGo3Z3pU",
"blockId": "bmz4rc8r19H2C6b7soxzby4",
"type": "text",
"content": {
"html": "<div>His name is {{Name}}</div>",
"richText": [
{ "type": "p", "children": [{ "text": "His name is {{Name}}" }] }
],
"plainText": "His name is {{Name}}"
}
}
},
"allIds": [
"oxTsU2C1RX5QHuyY8qjHAM",
"sgkADMK25y9P9V3vjwjBaac",
"ssEiEECKSFkA44dGDceHxKw",
"ssLd2wjExS9qWRur4tZDU1Z",
"sgTWsRM1qF2YoYLuGo3Z3pU"
]
},
"choiceItems": {
"byId": {
"q69Ex7LacPrH9QUMeosRnB": {
"id": "q69Ex7LacPrH9QUMeosRnB",
"stepId": "ssEiEECKSFkA44dGDceHxKw",
"content": "Go"
}
},
"allIds": ["q69Ex7LacPrH9QUMeosRnB"]
},
"webhooks": { "byId": {}, "allIds": [] },
"variables": {
"byId": {
"4tvkRmf32wiTsXrYoqyhfr": {
"id": "4tvkRmf32wiTsXrYoqyhfr",
"name": "secret 2",
"value": "secret2"
},
"jEg1FvkCU5S5owNAxXFsHL": {
"id": "jEg1FvkCU5S5owNAxXFsHL",
"name": "secret 3",
"value": "secret3"
},
"oASkBtoLqkYNqeakcjZH4L": {
"id": "oASkBtoLqkYNqeakcjZH4L",
"name": "secret 1",
"value": "secret1"
},
"rEoE1ehHzgx8X3d3UPGDHg": {
"id": "rEoE1ehHzgx8X3d3UPGDHg",
"name": "secret 4",
"value": "secret4"
},
"4kVx5uf8W1XP6WsfJEvt8v": {
"id": "4kVx5uf8W1XP6WsfJEvt8v",
"name": "Name"
}
},
"allIds": [
"oASkBtoLqkYNqeakcjZH4L",
"4tvkRmf32wiTsXrYoqyhfr",
"jEg1FvkCU5S5owNAxXFsHL",
"rEoE1ehHzgx8X3d3UPGDHg",
"4kVx5uf8W1XP6WsfJEvt8v"
]
},
"webhooks": {
"byId": {
"4h4Kk3Q1qGy7gFzpZtWVpU": {
"id": "4h4Kk3Q1qGy7gFzpZtWVpU",
"url": "http://localhost:3000/api/mock/webhook",
"queryParams": {
"byId": {
"hwGB11cA7RaYnaqH7gYyuQ": {
"id": "hwGB11cA7RaYnaqH7gYyuQ",
"key": "firstParam",
"value": "{{secret 1}}"
},
"6ux2FZjhNc4vfqNUDuCkxn": {
"id": "6ux2FZjhNc4vfqNUDuCkxn",
"key": "secondParam",
"value": "{{secret 2}}"
}
},
"allIds": ["hwGB11cA7RaYnaqH7gYyuQ", "6ux2FZjhNc4vfqNUDuCkxn"]
},
"headers": {
"byId": {
"ayTB2cFRKMo6oH9t9KS8SA": {
"id": "ayTB2cFRKMo6oH9t9KS8SA",
"key": "Custom-Typebot",
"value": "{{secret 3}}"
}
},
"allIds": ["ayTB2cFRKMo6oH9t9KS8SA"]
},
"method": "POST",
"body": "{ \"customField\": \"{{secret 4}}\" }"
}
},
"allIds": ["4h4Kk3Q1qGy7gFzpZtWVpU"]
},
"edges": {
"byId": {
"25yX9DnQgdafpdAjfAu5Fp": {
"id": "25yX9DnQgdafpdAjfAu5Fp",
"to": { "blockId": "bdFW2HHjMoEFmqHtFre9Xi8" },
"from": {
"stepId": "oxTsU2C1RX5QHuyY8qjHAM",
"blockId": "3kH2sUjVThQDWmqdoKnGk5"
}
},
"oxEEtym3NfDf34NCipzjRQ": {
"id": "oxEEtym3NfDf34NCipzjRQ",
"to": { "blockId": "b9mSgu7RKmK4xuiTVQP5Me8" },
"from": {
"stepId": "ssEiEECKSFkA44dGDceHxKw",
"blockId": "bdFW2HHjMoEFmqHtFre9Xi8"
}
},
"81SjKnxuUgrPmXvvJJihHM": {
"from": {
"blockId": "b9mSgu7RKmK4xuiTVQP5Me8",
"stepId": "ssLd2wjExS9qWRur4tZDU1Z"
},
"to": { "blockId": "bmz4rc8r19H2C6b7soxzby4" },
"id": "81SjKnxuUgrPmXvvJJihHM"
}
},
"allIds": [
"25yX9DnQgdafpdAjfAu5Fp",
"oxEEtym3NfDf34NCipzjRQ",
"81SjKnxuUgrPmXvvJJihHM"
]
},
"theme": {
"chat": {
"inputs": {
"color": "#303235",
"backgroundColor": "#FFFFFF",
"placeholderColor": "#9095A0"
},
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
},
"general": { "font": "Open Sans", "background": { "type": "None" } }
},
"settings": {
"general": { "isBrandingEnabled": true },
"metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
},
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
},
"publicId": null
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

View File

@ -1,228 +1,217 @@
{ {
"id": "ckylsd52p0114z31aobllswmu", "id": "ckz8gpmgr10008no1a1mq4q1l2",
"createdAt": "2022-01-19T16:55:13.393Z", "createdAt": "2022-02-04T13:47:42.459Z",
"updatedAt": "2022-01-19T16:55:13.393Z", "updatedAt": "2022-02-04T13:47:42.459Z",
"name": "My typebot", "name": "My typebot",
"ownerId": "ckylsbdf60088z31ayqytest6", "ownerId": "ckz6t9iep0006k31a22j05fwq",
"publishedTypebotId": null, "publishedTypebotId": null,
"folderId": null, "folderId": null,
"webhooks": { "byId": {}, "allIds": [] }, "blocks": [
"blocks": { {
"byId": { "id": "cN46uqNAR3ohjrS8jHJ6xT",
"2x83WHtEBkiv7pk7KgqJwZ": { "steps": [
"id": "2x83WHtEBkiv7pk7KgqJwZ", {
"title": "Start", "id": "nzijwLtLTAZfNNCN7kEpn5",
"stepIds": ["1A76iZBgXG7hvkG2koCxe4"], "type": "start",
"graphCoordinates": { "x": 0, "y": 0 } "label": "Start",
}, "blockId": "cN46uqNAR3ohjrS8jHJ6xT",
"bwga7RwqQWbowdHph27DM1N": { "outgoingEdgeId": "7wxB76VK81JsXMX9jU9dbQ"
"id": "bwga7RwqQWbowdHph27DM1N",
"title": "Block #2",
"graphCoordinates": { "x": 78, "y": 224 },
"stepIds": ["srwUKaUFFmehppJ2ZDqp4xG", "sxvzuo48GHi3AcAfmiFyYC1"]
},
"bu8whx817bJBG37FQrtD5dD": {
"id": "bu8whx817bJBG37FQrtD5dD",
"title": "Block #3",
"graphCoordinates": { "x": 430, "y": 287 },
"stepIds": ["ituVWW1AvQeVdFHTwsiVao", "5SLc4whZooZVUfr1bmTNSC"]
},
"b59jwmEdwZUvJszV394x44u": {
"id": "b59jwmEdwZUvJszV394x44u",
"title": "Block #4",
"graphCoordinates": { "x": 844, "y": 185 },
"stepIds": ["sm1YcKTL9cQMCGywzo1wyBB"]
},
"baVF9HqhuSnLDZqY9eRPpcp": {
"id": "baVF9HqhuSnLDZqY9eRPpcp",
"title": "Block #5",
"graphCoordinates": { "x": 841, "y": 356 },
"stepIds": ["sb3o6J8Fybg6u8KuayKviJq"]
},
"b9aEH46RHuZWTdQwZJ6KBWR": {
"id": "b9aEH46RHuZWTdQwZJ6KBWR",
"title": "Block #6",
"graphCoordinates": { "x": 839, "y": 523 },
"stepIds": ["scKogEJSTq4kPeHRhwTTjit"]
}
},
"allIds": [
"2x83WHtEBkiv7pk7KgqJwZ",
"bwga7RwqQWbowdHph27DM1N",
"bu8whx817bJBG37FQrtD5dD",
"b59jwmEdwZUvJszV394x44u",
"baVF9HqhuSnLDZqY9eRPpcp",
"b9aEH46RHuZWTdQwZJ6KBWR"
]
},
"steps": {
"byId": {
"1A76iZBgXG7hvkG2koCxe4": {
"id": "1A76iZBgXG7hvkG2koCxe4",
"type": "start",
"label": "Start",
"blockId": "2x83WHtEBkiv7pk7KgqJwZ",
"edgeId": "jjNy2hYgrQgPS9EBMKA7MH"
},
"srwUKaUFFmehppJ2ZDqp4xG": {
"id": "srwUKaUFFmehppJ2ZDqp4xG",
"blockId": "bwga7RwqQWbowdHph27DM1N",
"type": "text",
"content": {
"html": "<div>How old are you?</div>",
"richText": [
{ "type": "p", "children": [{ "text": "How old are you?" }] }
],
"plainText": "How old are you?"
} }
}, ],
"sxvzuo48GHi3AcAfmiFyYC1": { "title": "Start",
"id": "sxvzuo48GHi3AcAfmiFyYC1", "graphCoordinates": { "x": 0, "y": 0 }
"blockId": "bwga7RwqQWbowdHph27DM1N", },
"type": "number input", {
"options": { "variableId": "dEz689uVm8AxUM8TrbQd2t" }, "id": "eh2ohNATnGg6RTdjG9h5kb",
"edgeId": "7mcWaWohM9zGtLX8ZSnqFy" "steps": [
}, {
"ituVWW1AvQeVdFHTwsiVao": { "id": "sb6xdkJRr6P6BxtTM3ku5JD",
"id": "ituVWW1AvQeVdFHTwsiVao", "type": "text",
"blockId": "bu8whx817bJBG37FQrtD5dD", "blockId": "eh2ohNATnGg6RTdjG9h5kb",
"type": "Condition", "content": {
"options": { "html": "<div>How old are you?</div>",
"comparisons": { "richText": [
"byId": {}, { "type": "p", "children": [{ "text": "How old are you?" }] }
"allIds": [] ],
"plainText": "How old are you?"
}
},
{
"id": "ssyBKZve7bihSxUASYTruZA",
"type": "number input",
"blockId": "eh2ohNATnGg6RTdjG9h5kb",
"options": {
"labels": { "button": "Send", "placeholder": "Type a number..." },
"variableId": "iDJzzyzAY2jrapm3NwhGMz"
}, },
"logicalOperator": "AND" "outgoingEdgeId": "r8LX7iuEXxjF5SW5dbS6qT"
},
"trueEdgeId": "iBPsFyBsPv6Rbdfo2QdJyi"
},
"5SLc4whZooZVUfr1bmTNSC": {
"id": "5SLc4whZooZVUfr1bmTNSC",
"blockId": "bu8whx817bJBG37FQrtD5dD",
"type": "Condition",
"options": {
"comparisons": {
"byId": {},
"allIds": []
},
"logicalOperator": "AND"
},
"trueEdgeId": "354PJ2jD5U3J2APqLsPJrp",
"falseEdgeId": "94bmeCLigEUUpWYw2xsAVB"
},
"sm1YcKTL9cQMCGywzo1wyBB": {
"id": "sm1YcKTL9cQMCGywzo1wyBB",
"blockId": "b59jwmEdwZUvJszV394x44u",
"type": "text",
"content": {
"html": "<div>You are older than 80</div>",
"richText": [
{ "type": "p", "children": [{ "text": "You are older than 80" }] }
],
"plainText": "You are older than 80"
} }
}, ],
"sb3o6J8Fybg6u8KuayKviJq": { "title": "Block #1",
"id": "sb3o6J8Fybg6u8KuayKviJq", "graphCoordinates": { "x": 159, "y": 224 }
"blockId": "baVF9HqhuSnLDZqY9eRPpcp", },
"type": "text", {
"content": { "id": "eMk84KvFM53sBxchTeackR",
"html": "<div>You are older than 20</div>", "steps": [
"richText": [ {
{ "type": "p", "children": [{ "text": "You are older than 20" }] } "id": "s5hz7HQki66cwELvk2738MJ",
"blockId": "eMk84KvFM53sBxchTeackR",
"type": "Condition",
"items": [
{
"id": "56s4R9THRKeoJtBdKzEGLn",
"stepId": "s5hz7HQki66cwELvk2738MJ",
"type": 1,
"content": {
"comparisons": [
{
"id": "wxWqL7JuZhy9uwCZNPDJjM"
}
],
"logicalOperator": "AND"
},
"outgoingEdgeId": "nDjMjM11xPQF7c9Be6ukdY"
}
]
},
{
"id": "sv8uvEXgYWQNMfZWcdbfyCs",
"type": "Condition",
"items": [
{
"id": "ijYfW38tGhCMRrCtmR3bcr",
"type": 1,
"stepId": "sv8uvEXgYWQNMfZWcdbfyCs",
"content": {
"comparisons": [
{
"id": "3di6P3ypWf3XusgBLLHYBE"
}
],
"logicalOperator": "AND"
},
"outgoingEdgeId": "s1uVbzP1zMMJjsCguAfhGV"
}
], ],
"plainText": "You are older than 20" "blockId": "eMk84KvFM53sBxchTeackR",
"outgoingEdgeId": "hVScFHNzToiEsMfYVv5AWZ"
} }
}, ],
"scKogEJSTq4kPeHRhwTTjit": { "title": "Block #2",
"id": "scKogEJSTq4kPeHRhwTTjit", "graphCoordinates": { "x": 561, "y": 295 }
"blockId": "b9aEH46RHuZWTdQwZJ6KBWR", },
"type": "text", {
"content": { "id": "fGrzjahWecA8hoNMRrLSwn",
"html": "<div>You are younger than 20</div>", "steps": [
"richText": [ {
{ "type": "p", "children": [{ "text": "You are younger than 20" }] } "id": "soZqPdPUjYAht9nHmVpba1Q",
], "type": "text",
"plainText": "You are younger than 20" "blockId": "fGrzjahWecA8hoNMRrLSwn",
"content": {
"html": "<div>You are older than 80</div>",
"richText": [
{
"type": "p",
"children": [{ "text": "You are older than 80" }]
}
],
"plainText": "You are older than 80"
}
} }
} ],
"title": "Block #3",
"graphCoordinates": { "x": 936, "y": 63 }
}, },
"allIds": [ {
"1A76iZBgXG7hvkG2koCxe4", "id": "49Jv45UJi9R3U4FuWS8R2c",
"srwUKaUFFmehppJ2ZDqp4xG", "steps": [
"sxvzuo48GHi3AcAfmiFyYC1", {
"ituVWW1AvQeVdFHTwsiVao", "id": "svipUacs1sDk9KDxzaGhnsG",
"5SLc4whZooZVUfr1bmTNSC", "type": "text",
"sxvzuo48GHi3AcAfmiFyYC1", "blockId": "49Jv45UJi9R3U4FuWS8R2c",
"sm1YcKTL9cQMCGywzo1wyBB", "content": {
"sb3o6J8Fybg6u8KuayKviJq", "html": "<div>You are older than 20</div>",
"scKogEJSTq4kPeHRhwTTjit" "richText": [
] {
}, "type": "p",
"choiceItems": { "byId": {}, "allIds": [] }, "children": [{ "text": "You are older than 20" }]
"variables": { }
"byId": { ],
"dEz689uVm8AxUM8TrbQd2t": { "plainText": "You are older than 20"
"id": "dEz689uVm8AxUM8TrbQd2t", }
"name": "Age" }
} ],
"title": "Block #4",
"graphCoordinates": { "x": 950, "y": 298 }
}, },
"allIds": ["dEz689uVm8AxUM8TrbQd2t"] {
}, "id": "fD28kefdySKK7XA7SyTozC",
"edges": { "steps": [
"byId": { {
"jjNy2hYgrQgPS9EBMKA7MH": { "id": "spHJ7v9bDdVrFfuF2zg3YNR",
"from": { "type": "text",
"blockId": "2x83WHtEBkiv7pk7KgqJwZ", "blockId": "fD28kefdySKK7XA7SyTozC",
"stepId": "1A76iZBgXG7hvkG2koCxe4" "content": {
}, "html": "<div>You are younger than 20</div>",
"to": { "blockId": "bwga7RwqQWbowdHph27DM1N" }, "richText": [
"id": "jjNy2hYgrQgPS9EBMKA7MH" {
"type": "p",
"children": [{ "text": "You are younger than 20" }]
}
],
"plainText": "You are younger than 20"
}
}
],
"title": "Block #5",
"graphCoordinates": { "x": 985, "y": 537 }
}
],
"variables": [{ "id": "iDJzzyzAY2jrapm3NwhGMz", "name": "Age" }],
"edges": [
{
"from": {
"blockId": "eMk84KvFM53sBxchTeackR",
"stepId": "s5hz7HQki66cwELvk2738MJ",
"itemId": "56s4R9THRKeoJtBdKzEGLn"
}, },
"iBPsFyBsPv6Rbdfo2QdJyi": { "to": { "blockId": "fGrzjahWecA8hoNMRrLSwn" },
"from": { "id": "nDjMjM11xPQF7c9Be6ukdY"
"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", "from": {
"iBPsFyBsPv6Rbdfo2QdJyi", "blockId": "eMk84KvFM53sBxchTeackR",
"354PJ2jD5U3J2APqLsPJrp", "stepId": "sv8uvEXgYWQNMfZWcdbfyCs",
"94bmeCLigEUUpWYw2xsAVB", "itemId": "ijYfW38tGhCMRrCtmR3bcr"
"7mcWaWohM9zGtLX8ZSnqFy" },
] "to": { "blockId": "49Jv45UJi9R3U4FuWS8R2c" },
}, "id": "s1uVbzP1zMMJjsCguAfhGV"
},
{
"from": {
"blockId": "eMk84KvFM53sBxchTeackR",
"stepId": "sv8uvEXgYWQNMfZWcdbfyCs"
},
"to": { "blockId": "fD28kefdySKK7XA7SyTozC" },
"id": "hVScFHNzToiEsMfYVv5AWZ"
},
{
"from": {
"blockId": "eh2ohNATnGg6RTdjG9h5kb",
"stepId": "ssyBKZve7bihSxUASYTruZA"
},
"to": { "blockId": "eMk84KvFM53sBxchTeackR" },
"id": "r8LX7iuEXxjF5SW5dbS6qT"
},
{
"from": {
"blockId": "cN46uqNAR3ohjrS8jHJ6xT",
"stepId": "nzijwLtLTAZfNNCN7kEpn5"
},
"to": { "blockId": "eh2ohNATnGg6RTdjG9h5kb" },
"id": "7wxB76VK81JsXMX9jU9dbQ"
}
],
"theme": { "theme": {
"chat": { "chat": {
"inputs": { "inputs": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -1,101 +1,82 @@
{ {
"id": "ckymkfh1e00562z1a3fjoua3e", "id": "ckz8hnw7m10833no1ar12eov20",
"createdAt": "2022-01-20T06:00:51.458Z", "createdAt": "2022-02-04T14:14:21.394Z",
"updatedAt": "2022-01-20T06:00:51.458Z", "updatedAt": "2022-02-04T14:14:21.394Z",
"name": "My typebot", "name": "My typebot",
"ownerId": "ckymkff1100362z1a85juyoa8", "ownerId": "ckz6t9iep0006k31a22j05fwq",
"publishedTypebotId": null, "publishedTypebotId": null,
"folderId": null, "folderId": null,
"webhooks": { "byId": {}, "allIds": [] }, "blocks": [
"blocks": { {
"byId": { "id": "tdN9VXcdBWpuh6Gpaz3w4u",
"bsVJfEW7EZrUnAi9s5ev17": { "steps": [
"id": "bsVJfEW7EZrUnAi9s5ev17", {
"title": "Start", "id": "cVRL5EuVruTK31SAaVCvNE",
"stepIds": ["9Ck2yveNjZNHhjyc4HCJAL"], "type": "start",
"graphCoordinates": { "x": 0, "y": 0 } "label": "Start",
}, "blockId": "tdN9VXcdBWpuh6Gpaz3w4u",
"bmdnpyvzopZ8YVfqsJY7Q8K": { "outgoingEdgeId": "jqZYCYGxaL8svJbM2h1QAn"
"id": "bmdnpyvzopZ8YVfqsJY7Q8K", }
"title": "Block #2", ],
"graphCoordinates": { "x": 68, "y": 229 }, "title": "Start",
"stepIds": ["sas16Qqf4TmZEXSexmYpmSd"] "graphCoordinates": { "x": 0, "y": 0 }
},
"bnsxmer7DD2R9DogoXTsvHJ": {
"id": "bnsxmer7DD2R9DogoXTsvHJ",
"title": "Block #3",
"graphCoordinates": { "x": 491, "y": 239 },
"stepIds": ["sqNGop2aYkXRvJqb9nGtFbD"]
}
}, },
"allIds": [ {
"bsVJfEW7EZrUnAi9s5ev17", "id": "vymPUjL9AcWpkg9PkUXovk",
"bmdnpyvzopZ8YVfqsJY7Q8K", "graphCoordinates": { "x": 685, "y": 194 },
"bnsxmer7DD2R9DogoXTsvHJ" "title": "Block #1",
] "steps": [
}, {
"steps": { "id": "sa8WhnrMyMjYCBMeozfYRoi",
"byId": { "blockId": "vymPUjL9AcWpkg9PkUXovk",
"9Ck2yveNjZNHhjyc4HCJAL": { "type": "Redirect",
"id": "9Ck2yveNjZNHhjyc4HCJAL", "options": { "isNewTab": false }
"type": "start", }
"label": "Start", ]
"blockId": "bsVJfEW7EZrUnAi9s5ev17",
"edgeId": "totLsWG6AQfcFT39CsZwDy"
},
"sas16Qqf4TmZEXSexmYpmSd": {
"id": "sas16Qqf4TmZEXSexmYpmSd",
"blockId": "bmdnpyvzopZ8YVfqsJY7Q8K",
"type": "choice input",
"options": { "itemIds": ["mAgynXh3zmkmWzNyPGVAcf"] }
},
"sqNGop2aYkXRvJqb9nGtFbD": {
"id": "sqNGop2aYkXRvJqb9nGtFbD",
"blockId": "bnsxmer7DD2R9DogoXTsvHJ",
"type": "Redirect",
"options": { "isNewTab": false }
}
}, },
"allIds": [ {
"9Ck2yveNjZNHhjyc4HCJAL", "id": "rEJ3PhFQc7diJ23jdoF6w7",
"sas16Qqf4TmZEXSexmYpmSd", "graphCoordinates": { "x": 294, "y": 201 },
"sqNGop2aYkXRvJqb9nGtFbD" "title": "Block #2",
] "steps": [
}, {
"choiceItems": { "id": "s7QRApVZmVFZgS53CNruBRz",
"byId": { "blockId": "rEJ3PhFQc7diJ23jdoF6w7",
"mAgynXh3zmkmWzNyPGVAcf": { "type": "choice input",
"id": "mAgynXh3zmkmWzNyPGVAcf", "options": { "buttonLabel": "Send", "isMultipleChoice": false },
"stepId": "sas16Qqf4TmZEXSexmYpmSd", "items": [
"content": "Go to URL", {
"edgeId": "7KgqWB88ufzhDwzvwHuEbN" "id": "5rWR3enRg6jZyFhtmgbPYo",
} "stepId": "s7QRApVZmVFZgS53CNruBRz",
}, "type": 0,
"allIds": ["mAgynXh3zmkmWzNyPGVAcf"] "content": "Go to URL",
}, "outgoingEdgeId": "6aVDkPMEsadze2vf4mLiYt"
"variables": { "byId": {}, "allIds": [] }, }
"edges": { ]
"byId": { }
"totLsWG6AQfcFT39CsZwDy": { ]
"from": { }
"blockId": "bsVJfEW7EZrUnAi9s5ev17", ],
"stepId": "9Ck2yveNjZNHhjyc4HCJAL" "variables": [],
}, "edges": [
"to": { "blockId": "bmdnpyvzopZ8YVfqsJY7Q8K" }, {
"id": "totLsWG6AQfcFT39CsZwDy" "from": {
"blockId": "tdN9VXcdBWpuh6Gpaz3w4u",
"stepId": "cVRL5EuVruTK31SAaVCvNE"
}, },
"7KgqWB88ufzhDwzvwHuEbN": { "to": { "blockId": "rEJ3PhFQc7diJ23jdoF6w7" },
"from": { "id": "jqZYCYGxaL8svJbM2h1QAn"
"blockId": "bmdnpyvzopZ8YVfqsJY7Q8K",
"stepId": "sas16Qqf4TmZEXSexmYpmSd",
"nodeId": "mAgynXh3zmkmWzNyPGVAcf"
},
"to": { "blockId": "bnsxmer7DD2R9DogoXTsvHJ" },
"id": "7KgqWB88ufzhDwzvwHuEbN"
}
}, },
"allIds": ["totLsWG6AQfcFT39CsZwDy", "7KgqWB88ufzhDwzvwHuEbN"] {
}, "from": {
"blockId": "rEJ3PhFQc7diJ23jdoF6w7",
"stepId": "s7QRApVZmVFZgS53CNruBRz",
"itemId": "5rWR3enRg6jZyFhtmgbPYo"
},
"to": { "blockId": "vymPUjL9AcWpkg9PkUXovk" },
"id": "6aVDkPMEsadze2vf4mLiYt"
}
],
"theme": { "theme": {
"chat": { "chat": {
"inputs": { "inputs": {

View File

@ -1,152 +1,125 @@
{ {
"id": "ckylrr3qh0030fn1a3nszzxiu", "id": "ckz8hovd511021no1apuuyjv7b",
"createdAt": "2022-01-19T16:38:05.225Z", "createdAt": "2022-02-04T14:15:06.953Z",
"updatedAt": "2022-01-19T16:38:05.225Z", "updatedAt": "2022-02-04T14:15:06.953Z",
"name": "My typebot", "name": "My typebot",
"ownerId": "ckylrpsmt0006fn1ah956d0z1", "ownerId": "ckz6t9iep0006k31a22j05fwq",
"publishedTypebotId": null, "publishedTypebotId": null,
"folderId": null, "folderId": null,
"webhooks": { "byId": {}, "allIds": [] }, "blocks": [
"blocks": { {
"byId": { "id": "jvbBpKifJ3ssvKQxPqhBiD",
"kmUzhRFzSKjkaipYNcku9S": { "steps": [
"id": "kmUzhRFzSKjkaipYNcku9S", {
"title": "Start", "id": "rqecLJCZT7gP2JgBhRpb3a",
"stepIds": ["6XgP3JoCh7Y4M8GCX9DKym"], "type": "start",
"graphCoordinates": { "x": 0, "y": 0 } "label": "Start",
}, "blockId": "jvbBpKifJ3ssvKQxPqhBiD",
"bwWRAaX5m6NZyZ9jjpXmWSb": { "outgoingEdgeId": "qnD38SqA7sYEh7efPZgDoR"
"id": "bwWRAaX5m6NZyZ9jjpXmWSb", }
"title": "Block #2", ],
"graphCoordinates": { "x": -21, "y": 221 }, "title": "Start",
"stepIds": ["sqMVMXeRYp4inLcRqej2Wac", "s8n3nJajsBaYqrFeRYVvcf6"] "graphCoordinates": { "x": 0, "y": 0 }
},
"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": [ {
"kmUzhRFzSKjkaipYNcku9S", "id": "roD9feCwx6jTDuVCThgzM2",
"bwWRAaX5m6NZyZ9jjpXmWSb", "graphCoordinates": { "x": 200, "y": 171 },
"baUyUnNBxZzPe1z5PqE4NkD", "title": "Block #1",
"bwkKNpJmAFCCLbZSnPnnLnR" "steps": [
] {
}, "id": "souEkLukHsYU9jrN2rAP7YT",
"steps": { "blockId": "roD9feCwx6jTDuVCThgzM2",
"byId": { "type": "text",
"6XgP3JoCh7Y4M8GCX9DKym": { "content": {
"id": "6XgP3JoCh7Y4M8GCX9DKym", "html": "<div>How old are you?</div>",
"type": "start", "richText": [
"label": "Start", { "type": "p", "children": [{ "text": "How old are you?" }] }
"blockId": "kmUzhRFzSKjkaipYNcku9S", ],
"edgeId": "ahfJ4fUuvxX2dcBMk876tf" "plainText": "How old are you?"
}, }
"s8n3nJajsBaYqrFeRYVvcf6": { },
"id": "s8n3nJajsBaYqrFeRYVvcf6", {
"blockId": "bwWRAaX5m6NZyZ9jjpXmWSb", "id": "skfn5McXVrTNpi2e62RtEEY",
"type": "number input", "blockId": "roD9feCwx6jTDuVCThgzM2",
"edgeId": "dcJedLC7qsLtsmm1wbiFFc", "type": "number input",
"options": { "options": {
"labels": { "labels": { "button": "Send", "placeholder": "Type a number..." }
"placeholder": "Type a number..." },
"outgoingEdgeId": "5ZYDLyR1CUF6B8ESHrFXwK"
}
]
},
{
"id": "tFFeBrrWxY4tvr11C8rjTw",
"graphCoordinates": { "x": 526, "y": 283 },
"title": "Block #2",
"steps": [
{
"id": "spgqbyvunY91Ct5kVgeLLkz",
"blockId": "tFFeBrrWxY4tvr11C8rjTw",
"type": "Set variable",
"options": {}
},
{
"id": "skeKC71L8C8wpfeuV4TTLCD",
"blockId": "tFFeBrrWxY4tvr11C8rjTw",
"type": "Set variable",
"options": {},
"outgoingEdgeId": "7A4BD2vJT87grt3xFw86bn"
}
]
},
{
"id": "k6jFuKuSwy29LVwKxMWasv",
"graphCoordinates": { "x": 691, "y": 35 },
"title": "Block #3",
"steps": [
{
"id": "svpmd4uNoAXpoKyfYuuXTQe",
"blockId": "k6jFuKuSwy29LVwKxMWasv",
"type": "text",
"content": {
"html": "<div>Total: {{Total}}</div><div>Custom var: {{Custom var}}</div>",
"richText": [
{ "type": "p", "children": [{ "text": "Total: {{Total}}" }] },
{
"type": "p",
"children": [{ "text": "Custom var: {{Custom var}}" }]
}
],
"plainText": "Total: {{Total}}Custom var: {{Custom var}}"
} }
} }
]
}
],
"variables": [],
"edges": [
{
"from": {
"blockId": "jvbBpKifJ3ssvKQxPqhBiD",
"stepId": "rqecLJCZT7gP2JgBhRpb3a"
}, },
"sqMVMXeRYp4inLcRqej2Wac": { "to": { "blockId": "roD9feCwx6jTDuVCThgzM2" },
"id": "sqMVMXeRYp4inLcRqej2Wac", "id": "qnD38SqA7sYEh7efPZgDoR"
"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",
"options": {}
},
"sugJ6xN3jFys1CjWfsxGhiJ": {
"id": "sugJ6xN3jFys1CjWfsxGhiJ",
"blockId": "baUyUnNBxZzPe1z5PqE4NkD",
"type": "Set variable",
"edgeId": "sA5gvCVVBVYdGsdeSGF5ei",
"options": {}
},
"shR7ae3iNEvB6arCSu7wVFF": {
"id": "shR7ae3iNEvB6arCSu7wVFF",
"blockId": "bwkKNpJmAFCCLbZSnPnnLnR",
"type": "text",
"content": {
"html": "<div>Total: {{Total}}</div><div>Custom var: {{Custom var}}</div>",
"richText": [
{ "type": "p", "children": [{ "text": "Total: {{Total}}" }] },
{
"type": "p",
"children": [{ "text": "Custom var: {{Custom var}}" }]
}
],
"plainText": "Total: {{Total}}Custom var: {{Custom var}}"
}
}
}, },
"allIds": [ {
"6XgP3JoCh7Y4M8GCX9DKym", "from": {
"s8n3nJajsBaYqrFeRYVvcf6", "blockId": "roD9feCwx6jTDuVCThgzM2",
"sqMVMXeRYp4inLcRqej2Wac", "stepId": "skfn5McXVrTNpi2e62RtEEY"
"shfL5ueQDuj2RPcJPWZGArT",
"sugJ6xN3jFys1CjWfsxGhiJ",
"shR7ae3iNEvB6arCSu7wVFF"
]
},
"choiceItems": { "byId": {}, "allIds": [] },
"variables": { "byId": {}, "allIds": [] },
"edges": {
"byId": {
"ahfJ4fUuvxX2dcBMk876tf": {
"from": {
"blockId": "kmUzhRFzSKjkaipYNcku9S",
"stepId": "6XgP3JoCh7Y4M8GCX9DKym"
},
"to": { "blockId": "bwWRAaX5m6NZyZ9jjpXmWSb" },
"id": "ahfJ4fUuvxX2dcBMk876tf"
}, },
"dcJedLC7qsLtsmm1wbiFFc": { "to": { "blockId": "tFFeBrrWxY4tvr11C8rjTw" },
"from": { "id": "5ZYDLyR1CUF6B8ESHrFXwK"
"blockId": "bwWRAaX5m6NZyZ9jjpXmWSb",
"stepId": "s8n3nJajsBaYqrFeRYVvcf6"
},
"to": { "blockId": "baUyUnNBxZzPe1z5PqE4NkD" },
"id": "dcJedLC7qsLtsmm1wbiFFc"
},
"sA5gvCVVBVYdGsdeSGF5ei": {
"from": {
"blockId": "baUyUnNBxZzPe1z5PqE4NkD",
"stepId": "sugJ6xN3jFys1CjWfsxGhiJ"
},
"to": { "blockId": "bwkKNpJmAFCCLbZSnPnnLnR" },
"id": "sA5gvCVVBVYdGsdeSGF5ei"
}
}, },
"allIds": [ {
"ahfJ4fUuvxX2dcBMk876tf", "from": {
"dcJedLC7qsLtsmm1wbiFFc", "blockId": "tFFeBrrWxY4tvr11C8rjTw",
"sA5gvCVVBVYdGsdeSGF5ei" "stepId": "skeKC71L8C8wpfeuV4TTLCD"
] },
}, "to": { "blockId": "k6jFuKuSwy29LVwKxMWasv" },
"id": "7A4BD2vJT87grt3xFw86bn"
}
],
"theme": { "theme": {
"chat": { "chat": {
"inputs": { "inputs": {

View File

@ -1,158 +1,126 @@
{ {
"id": "ckylsr69q0240z31afjhedyxo", "id": "ckz8hrq1i11165no1artywpjvb",
"createdAt": "2022-01-19T17:06:08.126Z", "createdAt": "2022-02-04T14:17:20.022Z",
"updatedAt": "2022-01-19T17:06:08.126Z", "updatedAt": "2022-02-04T14:17:20.022Z",
"name": "My typebot", "name": "My typebot",
"ownerId": "ckylsr4fi0220z31apbinpy9d", "ownerId": "ckz6t9iep0006k31a22j05fwq",
"publishedTypebotId": null, "publishedTypebotId": null,
"folderId": null, "folderId": null,
"webhooks": { "byId": {}, "allIds": [] }, "blocks": [
"blocks": { {
"byId": { "id": "3EgW9xiicKuiCNycEY2huP",
"weeBMMXxNKwEonMfDX8Z5k": { "steps": [
"id": "weeBMMXxNKwEonMfDX8Z5k", {
"title": "Start", "id": "bHS7nGbziYUwD27tKANQY6",
"stepIds": ["nEXiHesKXRQJhQbaWfbDVH"], "type": "start",
"graphCoordinates": { "x": 0, "y": 0 } "label": "Start",
}, "blockId": "3EgW9xiicKuiCNycEY2huP",
"bg2MBdkf6y7g6WsbqAP3eAT": { "outgoingEdgeId": "9LViRZxY8G6iswJrE4YLsq"
"id": "bg2MBdkf6y7g6WsbqAP3eAT", }
"title": "Block #2", ],
"graphCoordinates": { "x": 120, "y": 221 }, "title": "Start",
"stepIds": ["sqzMjp1Ba4jTL3A6iJehC6C"] "graphCoordinates": { "x": 0, "y": 0 }
},
"bj5BE1yKPzFFhvRk6cMnmsQ": {
"id": "bj5BE1yKPzFFhvRk6cMnmsQ",
"title": "Block #3",
"graphCoordinates": { "x": 529, "y": 130 },
"stepIds": ["s8zPdEj96z8EoJG2zBqgoE8"]
},
"bdET8zLFQbwpTaAmi4wmezE": {
"id": "bdET8zLFQbwpTaAmi4wmezE",
"title": "Block #4",
"graphCoordinates": { "x": 538, "y": 386 },
"stepIds": ["sjZ28izS5e3VjNynFKT2F7E"]
}
}, },
"allIds": [ {
"weeBMMXxNKwEonMfDX8Z5k", "id": "bs9JCJfixRTv8W2imPAoyX",
"bg2MBdkf6y7g6WsbqAP3eAT", "graphCoordinates": { "x": 392, "y": 180 },
"bj5BE1yKPzFFhvRk6cMnmsQ", "title": "Block #1",
"bdET8zLFQbwpTaAmi4wmezE" "steps": [
] {
}, "id": "sqcez9cUVbPvaFL4rQsUwUk",
"steps": { "blockId": "bs9JCJfixRTv8W2imPAoyX",
"byId": { "type": "choice input",
"nEXiHesKXRQJhQbaWfbDVH": { "options": { "buttonLabel": "Send", "isMultipleChoice": false },
"id": "nEXiHesKXRQJhQbaWfbDVH", "items": [
"type": "start", {
"label": "Start", "id": "2jCerpszvvbmhUS8FXkKG9",
"blockId": "weeBMMXxNKwEonMfDX8Z5k", "stepId": "sqcez9cUVbPvaFL4rQsUwUk",
"edgeId": "uh95dDpiiZdYxpPFsUqZEg" "type": 0,
}, "content": "Burgers",
"sqzMjp1Ba4jTL3A6iJehC6C": { "outgoingEdgeId": "aTS7nwxhRdFN8NwAXE2oSq"
"id": "sqzMjp1Ba4jTL3A6iJehC6C", },
"blockId": "bg2MBdkf6y7g6WsbqAP3eAT", {
"type": "choice input", "stepId": "sqcez9cUVbPvaFL4rQsUwUk",
"options": { "type": 0,
"itemIds": [ "id": "vP4HWCYkeRL6egk3yjCCmg",
"bWrsg18ucP9cdtFKhzgHbF", "content": "Hot dogs"
"p7Z57shv7p79KiwAtdi8Y3", },
"wjMRa2GBBnME9bEiNi6XgP" {
] "stepId": "sqcez9cUVbPvaFL4rQsUwUk",
}, "type": 0,
"edgeId": "asT5shwJqDQ67qPuydR4gy" "id": "hhveB5JSxJ8X9N66LFaUfe",
}, "content": "Carpaccio"
"s8zPdEj96z8EoJG2zBqgoE8": { }
"id": "s8zPdEj96z8EoJG2zBqgoE8",
"blockId": "bj5BE1yKPzFFhvRk6cMnmsQ",
"type": "text",
"content": {
"html": "<div>I love burgers!</div>",
"richText": [
{ "type": "p", "children": [{ "text": "I love burgers!" }] }
], ],
"plainText": "I love burgers!" "outgoingEdgeId": "8FaF38WfM7PiLJLS5z6vQe"
} }
}, ]
"sjZ28izS5e3VjNynFKT2F7E": { },
"id": "sjZ28izS5e3VjNynFKT2F7E", {
"blockId": "bdET8zLFQbwpTaAmi4wmezE", "id": "ih574JsgYCSSt3t77DH9gp",
"type": "text", "graphCoordinates": { "x": 770, "y": 105 },
"content": { "title": "Block #2",
"html": "<div>Cool!</div>", "steps": [
"richText": [{ "type": "p", "children": [{ "text": "Cool!" }] }], {
"plainText": "Cool!" "id": "s9hdG689cjRzmTdb5hMN83q",
"blockId": "ih574JsgYCSSt3t77DH9gp",
"type": "text",
"content": {
"html": "<div>I love burgers!</div>",
"richText": [
{ "type": "p", "children": [{ "text": "I love burgers!" }] }
],
"plainText": "I love burgers!"
}
} }
} ]
}, },
"allIds": [ {
"nEXiHesKXRQJhQbaWfbDVH", "id": "5bMwu6Wv79avgdz3TKjVXr",
"sqzMjp1Ba4jTL3A6iJehC6C", "graphCoordinates": { "x": 766, "y": 311 },
"s8zPdEj96z8EoJG2zBqgoE8", "title": "Block #3",
"sjZ28izS5e3VjNynFKT2F7E" "steps": [
] {
}, "id": "s3Zwr8m3Nm2BwGxNNCu4n7N",
"choiceItems": { "blockId": "5bMwu6Wv79avgdz3TKjVXr",
"byId": { "type": "text",
"bWrsg18ucP9cdtFKhzgHbF": { "content": {
"id": "bWrsg18ucP9cdtFKhzgHbF", "html": "<div>Cool!</div>",
"stepId": "sqzMjp1Ba4jTL3A6iJehC6C", "richText": [{ "type": "p", "children": [{ "text": "Cool!" }] }],
"content": "Burgers", "plainText": "Cool!"
"edgeId": "jfR6AUWt9b4dhjnUHXB179" }
}
]
}
],
"variables": [],
"edges": [
{
"from": {
"blockId": "3EgW9xiicKuiCNycEY2huP",
"stepId": "bHS7nGbziYUwD27tKANQY6"
}, },
"p7Z57shv7p79KiwAtdi8Y3": { "to": { "blockId": "bs9JCJfixRTv8W2imPAoyX" },
"id": "p7Z57shv7p79KiwAtdi8Y3", "id": "9LViRZxY8G6iswJrE4YLsq"
"stepId": "sqzMjp1Ba4jTL3A6iJehC6C",
"content": "Hot dogs"
},
"wjMRa2GBBnME9bEiNi6XgP": {
"id": "wjMRa2GBBnME9bEiNi6XgP",
"stepId": "sqzMjp1Ba4jTL3A6iJehC6C",
"content": "Carpaccio"
}
}, },
"allIds": [ {
"bWrsg18ucP9cdtFKhzgHbF", "from": {
"p7Z57shv7p79KiwAtdi8Y3", "blockId": "bs9JCJfixRTv8W2imPAoyX",
"wjMRa2GBBnME9bEiNi6XgP" "stepId": "sqcez9cUVbPvaFL4rQsUwUk",
] "itemId": "2jCerpszvvbmhUS8FXkKG9"
},
"variables": { "byId": {}, "allIds": [] },
"edges": {
"byId": {
"uh95dDpiiZdYxpPFsUqZEg": {
"from": {
"blockId": "weeBMMXxNKwEonMfDX8Z5k",
"stepId": "nEXiHesKXRQJhQbaWfbDVH"
},
"to": { "blockId": "bg2MBdkf6y7g6WsbqAP3eAT" },
"id": "uh95dDpiiZdYxpPFsUqZEg"
}, },
"jfR6AUWt9b4dhjnUHXB179": { "to": { "blockId": "ih574JsgYCSSt3t77DH9gp" },
"from": { "id": "aTS7nwxhRdFN8NwAXE2oSq"
"blockId": "bg2MBdkf6y7g6WsbqAP3eAT",
"stepId": "sqzMjp1Ba4jTL3A6iJehC6C",
"nodeId": "bWrsg18ucP9cdtFKhzgHbF"
},
"to": { "blockId": "bj5BE1yKPzFFhvRk6cMnmsQ" },
"id": "jfR6AUWt9b4dhjnUHXB179"
},
"asT5shwJqDQ67qPuydR4gy": {
"from": {
"blockId": "bg2MBdkf6y7g6WsbqAP3eAT",
"stepId": "sqzMjp1Ba4jTL3A6iJehC6C"
},
"to": { "blockId": "bdET8zLFQbwpTaAmi4wmezE" },
"id": "asT5shwJqDQ67qPuydR4gy"
}
}, },
"allIds": [ {
"uh95dDpiiZdYxpPFsUqZEg", "from": {
"jfR6AUWt9b4dhjnUHXB179", "blockId": "bs9JCJfixRTv8W2imPAoyX",
"asT5shwJqDQ67qPuydR4gy" "stepId": "sqcez9cUVbPvaFL4rQsUwUk"
] },
}, "to": { "blockId": "5bMwu6Wv79avgdz3TKjVXr" },
"id": "8FaF38WfM7PiLJLS5z6vQe"
}
],
"theme": { "theme": {
"chat": { "chat": {
"inputs": { "inputs": {

View File

@ -1,140 +1,94 @@
{ {
"id": "bdFW2HHjMoEFmqHtFre9Xi8", "id": "ckz8huhvo11297no1a7b4zf3ce",
"createdAt": "2022-01-21T07:55:14.727Z", "createdAt": "2022-02-04T14:19:29.412Z",
"updatedAt": "2022-01-21T07:55:14.727Z", "updatedAt": "2022-02-04T14:19:29.412Z",
"name": "My typebot", "name": "My typebot",
"ownerId": "user2", "ownerId": "ckz6t9iep0006k31a22j05fwq",
"publishedTypebotId": null, "publishedTypebotId": null,
"folderId": null, "folderId": null,
"blocks": { "blocks": [
"byId": { {
"3kH2sUjVThQDWmqdoKnGk5": { "id": "teepNancm8TLj1qYhaTYAf",
"id": "3kH2sUjVThQDWmqdoKnGk5", "steps": [
"title": "Start", {
"stepIds": ["oxTsU2C1RX5QHuyY8qjHAM"], "id": "8fG3wDsExSSkq5ekUMzWVY",
"graphCoordinates": { "x": 42, "y": 13 } "type": "start",
}, "label": "Start",
"bdFW2HHjMoEFmqHtFre9Xi8": { "blockId": "teepNancm8TLj1qYhaTYAf",
"id": "bdFW2HHjMoEFmqHtFre9Xi8", "outgoingEdgeId": "pj6fgTAjarwBq2jVgMgYoK"
"title": "Block #2",
"stepIds": ["sgkADMK25y9P9V3vjwjBaac", "ssEiEECKSFkA44dGDceHxKw"],
"graphCoordinates": { "x": 121, "y": 227 }
},
"bhKHKi1SQb5woZEy1y4fNsJ": {
"id": "bhKHKi1SQb5woZEy1y4fNsJ",
"title": "Block #3",
"graphCoordinates": { "x": 605, "y": 454 },
"stepIds": ["sseUQEWCMdiZquk8EbxHYtk"]
}
},
"allIds": [
"3kH2sUjVThQDWmqdoKnGk5",
"bdFW2HHjMoEFmqHtFre9Xi8",
"bhKHKi1SQb5woZEy1y4fNsJ"
]
},
"steps": {
"byId": {
"oxTsU2C1RX5QHuyY8qjHAM": {
"id": "oxTsU2C1RX5QHuyY8qjHAM",
"type": "start",
"label": "Start",
"edgeId": "25yX9DnQgdafpdAjfAu5Fp",
"blockId": "3kH2sUjVThQDWmqdoKnGk5"
},
"sgkADMK25y9P9V3vjwjBaac": {
"id": "sgkADMK25y9P9V3vjwjBaac",
"type": "text",
"blockId": "bdFW2HHjMoEFmqHtFre9Xi8",
"content": {
"html": "<div>Ready?</div>",
"richText": [{ "type": "p", "children": [{ "text": "Ready?" }] }],
"plainText": "Ready?"
} }
}, ],
"ssEiEECKSFkA44dGDceHxKw": { "title": "Start",
"id": "ssEiEECKSFkA44dGDceHxKw", "graphCoordinates": { "x": 0, "y": 0 }
"type": "choice input",
"edgeId": "6e4Sbp8pGTvBQYtCk2qXbN",
"blockId": "bdFW2HHjMoEFmqHtFre9Xi8",
"options": { "itemIds": ["q69Ex7LacPrH9QUMeosRnB"] }
},
"sseUQEWCMdiZquk8EbxHYtk": {
"id": "sseUQEWCMdiZquk8EbxHYtk",
"blockId": "bhKHKi1SQb5woZEy1y4fNsJ",
"type": "text input"
}
}, },
"allIds": [ {
"oxTsU2C1RX5QHuyY8qjHAM", "id": "6Dj1i7LeM3qXg5SKMhMyo1",
"sgkADMK25y9P9V3vjwjBaac", "graphCoordinates": { "x": 315, "y": 137 },
"ssEiEECKSFkA44dGDceHxKw", "title": "Block #1",
"sseUQEWCMdiZquk8EbxHYtk" "steps": [
] {
}, "id": "swUB2pSmvcv3NC7ySzskRpL",
"choiceItems": { "blockId": "6Dj1i7LeM3qXg5SKMhMyo1",
"byId": { "type": "text",
"q69Ex7LacPrH9QUMeosRnB": { "content": {
"id": "q69Ex7LacPrH9QUMeosRnB", "html": "<div>Ready?</div>",
"stepId": "ssEiEECKSFkA44dGDceHxKw", "richText": [{ "type": "p", "children": [{ "text": "Ready?" }] }],
"content": "Go" "plainText": "Ready?"
} }
},
"allIds": ["q69Ex7LacPrH9QUMeosRnB"]
},
"variables": {
"byId": {
"4tvkRmf32wiTsXrYoqyhfr": {
"id": "4tvkRmf32wiTsXrYoqyhfr",
"name": "secret 2"
},
"jEg1FvkCU5S5owNAxXFsHL": {
"id": "jEg1FvkCU5S5owNAxXFsHL",
"name": "secret 3"
},
"oASkBtoLqkYNqeakcjZH4L": {
"id": "oASkBtoLqkYNqeakcjZH4L",
"name": "secret 1"
},
"rEoE1ehHzgx8X3d3UPGDHg": {
"id": "rEoE1ehHzgx8X3d3UPGDHg",
"name": "secret 4"
}
},
"allIds": [
"oASkBtoLqkYNqeakcjZH4L",
"4tvkRmf32wiTsXrYoqyhfr",
"jEg1FvkCU5S5owNAxXFsHL",
"rEoE1ehHzgx8X3d3UPGDHg"
]
},
"webhooks": {
"byId": {
"4h4Kk3Q1qGy7gFzpZtWVpU": { "id": "4h4Kk3Q1qGy7gFzpZtWVpU", "url": "" }
},
"allIds": ["4h4Kk3Q1qGy7gFzpZtWVpU"]
},
"edges": {
"byId": {
"25yX9DnQgdafpdAjfAu5Fp": {
"id": "25yX9DnQgdafpdAjfAu5Fp",
"to": { "blockId": "bdFW2HHjMoEFmqHtFre9Xi8" },
"from": {
"stepId": "oxTsU2C1RX5QHuyY8qjHAM",
"blockId": "3kH2sUjVThQDWmqdoKnGk5"
}
},
"6e4Sbp8pGTvBQYtCk2qXbN": {
"from": {
"blockId": "bdFW2HHjMoEFmqHtFre9Xi8",
"stepId": "ssEiEECKSFkA44dGDceHxKw"
}, },
"to": { "blockId": "bhKHKi1SQb5woZEy1y4fNsJ" }, {
"id": "6e4Sbp8pGTvBQYtCk2qXbN" "id": "sc7ZYFtHVegJUA8c5K3gghi",
} "blockId": "6Dj1i7LeM3qXg5SKMhMyo1",
"type": "choice input",
"options": { "buttonLabel": "Send", "isMultipleChoice": false },
"items": [
{
"id": "nTjur4kxyL473XTbAb4Fak",
"stepId": "sc7ZYFtHVegJUA8c5K3gghi",
"type": 0,
"content": "Go"
}
],
"outgoingEdgeId": "uAsACqSmud99zmyCABWDwr"
}
]
}, },
"allIds": ["25yX9DnQgdafpdAjfAu5Fp", "6e4Sbp8pGTvBQYtCk2qXbN"] {
}, "id": "2TR5xAQobKAg8hbArfh5br",
"graphCoordinates": { "x": 760, "y": 299 },
"title": "Block #2",
"steps": [
{
"id": "s4xokHybra1jmZsWGVmza1K",
"blockId": "2TR5xAQobKAg8hbArfh5br",
"type": "text input",
"options": {
"isLong": false,
"labels": { "button": "Send", "placeholder": "Type your answer..." }
}
}
]
}
],
"variables": [],
"edges": [
{
"from": {
"blockId": "teepNancm8TLj1qYhaTYAf",
"stepId": "8fG3wDsExSSkq5ekUMzWVY"
},
"to": { "blockId": "6Dj1i7LeM3qXg5SKMhMyo1" },
"id": "pj6fgTAjarwBq2jVgMgYoK"
},
{
"from": {
"blockId": "6Dj1i7LeM3qXg5SKMhMyo1",
"stepId": "sc7ZYFtHVegJUA8c5K3gghi"
},
"to": { "blockId": "2TR5xAQobKAg8hbArfh5br" },
"id": "uAsACqSmud99zmyCABWDwr"
}
],
"theme": { "theme": {
"chat": { "chat": {
"inputs": { "inputs": {

View File

@ -1,6 +1,10 @@
import { chromium, FullConfig, Page } from '@playwright/test' import { chromium, FullConfig, Page } from '@playwright/test'
import { existsSync } from 'fs' import { existsSync } from 'fs'
import { setupDatabase, teardownDatabase } from './services/database' import {
getSignedInUser,
setupDatabase,
teardownDatabase,
} from './services/database'
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
require('dotenv').config({ path: '.env' }) require('dotenv').config({ path: '.env' })
@ -8,10 +12,15 @@ require('dotenv').config({ path: '.env' })
async function globalSetup(config: FullConfig) { async function globalSetup(config: FullConfig) {
const { baseURL } = config.projects[0].use const { baseURL } = config.projects[0].use
if (!baseURL) throw new Error('baseURL is missing') if (!baseURL) throw new Error('baseURL is missing')
if (!process.env.GITHUB_EMAIL || !process.env.GITHUB_PASSWORD)
throw new Error(
'GITHUB_EMAIL or GITHUB_PASSWORD are missing in the environment. They are required to log in.'
)
await teardownDatabase() await teardownDatabase()
if (!existsSync('./playwright/authenticatedState.json')) { const signedInUser = await getSignedInUser(process.env.GITHUB_EMAIL as string)
if (!signedInUser || !existsSync('./playwright/authenticatedState.json')) {
const browser = await chromium.launch() const browser = await chromium.launch()
const page = await browser.newPage() const page = await browser.newPage()
await signIn(page) await signIn(page)
@ -24,14 +33,13 @@ async function globalSetup(config: FullConfig) {
} }
const signIn = async (page: Page) => { const signIn = async (page: Page) => {
if (!process.env.GITHUB_EMAIL || !process.env.GITHUB_PASSWORD)
throw new Error(
'GITHUB_EMAIL or GITHUB_PASSWORD are missing in the environment. They are required to log in.'
)
await page.goto(`${process.env.PLAYWRIGHT_BUILDER_TEST_BASE_URL}/signin`) await page.goto(`${process.env.PLAYWRIGHT_BUILDER_TEST_BASE_URL}/signin`)
await page.click('text=Continue with GitHub') await page.click('text=Continue with GitHub')
await page.fill('input[name="login"]', process.env.GITHUB_EMAIL) await page.fill('input[name="login"]', process.env.GITHUB_EMAIL as string)
await page.fill('input[name="password"]', process.env.GITHUB_PASSWORD) await page.fill(
'input[name="password"]',
process.env.GITHUB_PASSWORD as string
)
await page.press('input[name="password"]', 'Enter') await page.press('input[name="password"]', 'Enter')
try { try {
await page.locator('text=Authorize baptisteArno').click({ timeout: 3000 }) await page.locator('text=Authorize baptisteArno').click({ timeout: 3000 })

View File

@ -1,6 +1,8 @@
import { import {
Block,
defaultSettings, defaultSettings,
defaultTheme, defaultTheme,
PublicBlock,
PublicTypebot, PublicTypebot,
Step, Step,
Typebot, Typebot,
@ -23,7 +25,7 @@ export const setupDatabase = async (userEmail: string) => {
return createCredentials() return createCredentials()
} }
const getSignedInUser = (email: string) => export const getSignedInUser = (email: string) =>
prisma.user.findFirst({ where: { email } }) prisma.user.findFirst({ where: { email } })
export const createTypebots = async (partialTypebots: Partial<Typebot>[]) => { export const createTypebots = async (partialTypebots: Partial<Typebot>[]) => {
@ -108,18 +110,24 @@ const parseTypebotToPublicTypebot = (
typebot: Typebot typebot: Typebot
): PublicTypebot => ({ ): PublicTypebot => ({
id, id,
blocks: typebot.blocks,
steps: typebot.steps,
name: typebot.name, name: typebot.name,
blocks: parseBlocksToPublicBlocks(typebot.blocks),
typebotId: typebot.id, typebotId: typebot.id,
theme: typebot.theme, theme: typebot.theme,
settings: typebot.settings, settings: typebot.settings,
publicId: typebot.publicId, publicId: typebot.publicId,
choiceItems: typebot.choiceItems,
variables: typebot.variables, variables: typebot.variables,
edges: typebot.edges, edges: typebot.edges,
}) })
const parseBlocksToPublicBlocks = (blocks: Block[]): PublicBlock[] =>
blocks.map((b) => ({
...b,
steps: b.steps.map((s) =>
'webhook' in s ? { ...s, webhook: s.webhook.id } : s
),
}))
const parseTestTypebot = (partialTypebot: Partial<Typebot>): Typebot => ({ const parseTestTypebot = (partialTypebot: Partial<Typebot>): Typebot => ({
id: partialTypebot.id ?? 'typebot', id: partialTypebot.id ?? 'typebot',
folderId: null, folderId: null,
@ -128,82 +136,54 @@ const parseTestTypebot = (partialTypebot: Partial<Typebot>): Typebot => ({
theme: defaultTheme, theme: defaultTheme,
settings: defaultSettings, settings: defaultSettings,
createdAt: new Date(), createdAt: new Date(),
choiceItems: partialTypebot.choiceItems ?? {
byId: {
choice1: {
id: 'choice1',
stepId: 'step1',
},
},
allIds: ['choice1'],
},
publicId: null, publicId: null,
publishedTypebotId: null, publishedTypebotId: null,
updatedAt: new Date(), updatedAt: new Date(),
variables: { byId: {}, allIds: [] }, variables: [],
webhooks: { byId: {}, allIds: [] },
edges: {
byId: {
edge1: {
id: 'edge1',
from: { blockId: 'block0', stepId: 'step0' },
to: { blockId: 'block1' },
},
},
allIds: ['edge1'],
},
...partialTypebot, ...partialTypebot,
blocks: { edges: [
byId: { {
block0: { id: 'edge1',
id: 'block0', from: { blockId: 'block0', stepId: 'step0' },
title: 'Block #0', to: { blockId: 'block1' },
stepIds: ['step0'],
graphCoordinates: { x: 0, y: 0 },
},
...partialTypebot.blocks?.byId,
}, },
allIds: ['block0', ...(partialTypebot.blocks?.allIds ?? [])], ],
}, blocks: [
steps: { {
byId: { id: 'block0',
step0: { title: 'Block #0',
id: 'step0', steps: [
type: 'start', {
blockId: 'block0', id: 'step0',
label: 'Start', type: 'start',
edgeId: 'edge1', blockId: 'block0',
}, label: 'Start',
...partialTypebot.steps?.byId, outgoingEdgeId: 'edge1',
},
],
graphCoordinates: { x: 0, y: 0 },
}, },
allIds: ['step0', ...(partialTypebot.steps?.allIds ?? [])], ...(partialTypebot.blocks ?? []),
}, ],
}) })
export const parseDefaultBlockWithStep = ( export const parseDefaultBlockWithStep = (
step: Partial<Step> step: Partial<Step>
): Pick<Typebot, 'blocks' | 'steps'> => ({ ): Pick<Typebot, 'blocks'> => ({
blocks: { blocks: [
byId: { {
block1: { graphCoordinates: { x: 200, y: 200 },
graphCoordinates: { x: 200, y: 200 }, id: 'block1',
id: 'block1', steps: [
stepIds: ['step1'], {
title: 'Block #1', id: 'step1',
}, blockId: 'block1',
...step,
} as Step,
],
title: 'Block #1',
}, },
allIds: ['block1'], ],
},
steps: {
byId: {
step1: {
id: 'step1',
blockId: 'block1',
...step,
} as Step,
},
allIds: ['step1'],
},
}) })
export const importTypebotInDatabase = ( export const importTypebotInDatabase = (

View File

@ -6,7 +6,11 @@ import { updateUser } from '../services/database'
test.describe('Account page', () => { test.describe('Account page', () => {
test('should edit user info properly', async ({ page }) => { test('should edit user info properly', async ({ page }) => {
await updateUser({ name: 'Default Name' }) await updateUser({
name: 'Default Name',
image:
'https://images.unsplash.com/photo-1521119989659-a83eee488004?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1323&q=80',
})
await page.goto('/account') await page.goto('/account')
const saveButton = page.locator('button:has-text("Save")') const saveButton = page.locator('button:has-text("Save")')
await expect(saveButton).toBeHidden() await expect(saveButton).toBeHidden()

View File

@ -24,22 +24,22 @@ test.describe('Text bubble step', () => {
await page.click('[data-testid="bold-button"]') await page.click('[data-testid="bold-button"]')
await page.type('div[role="textbox"]', 'Bold text') await page.type('div[role="textbox"]', 'Bold text')
await page.press('div[role="textbox"]', 'Enter') await page.press('div[role="textbox"]', 'Shift+Enter')
await page.click('[data-testid="bold-button"]') await page.click('[data-testid="bold-button"]')
await page.click('[data-testid="italic-button"]') await page.click('[data-testid="italic-button"]')
await page.type('div[role="textbox"]', 'Italic text') await page.type('div[role="textbox"]', 'Italic text')
await page.press('div[role="textbox"]', 'Enter') await page.press('div[role="textbox"]', 'Shift+Enter')
await page.click('[data-testid="underline-button"]') await page.click('[data-testid="underline-button"]')
await page.click('[data-testid="italic-button"]') await page.click('[data-testid="italic-button"]')
await page.type('div[role="textbox"]', 'Underlined text') await page.type('div[role="textbox"]', 'Underlined text')
await page.press('div[role="textbox"]', 'Enter') await page.press('div[role="textbox"]', 'Shift+Enter')
await page.click('[data-testid="bold-button"]') await page.click('[data-testid="bold-button"]')
await page.click('[data-testid="italic-button"]') await page.click('[data-testid="italic-button"]')
await page.type('div[role="textbox"]', 'Everything text') await page.type('div[role="textbox"]', 'Everything text')
await page.press('div[role="textbox"]', 'Enter') await page.press('div[role="textbox"]', 'Shift+Enter')
await page.click('text=Preview') await page.click('text=Preview')
await expect( await expect(

View File

@ -1,9 +1,97 @@
import test, { expect } from '@playwright/test' import test, { expect, Page } from '@playwright/test'
import { createTypebots, parseDefaultBlockWithStep } from '../services/database' import {
createTypebots,
importTypebotInDatabase,
parseDefaultBlockWithStep,
} from '../services/database'
import { defaultTextInputOptions, InputStepType } from 'models' import { defaultTextInputOptions, InputStepType } from 'models'
import { generate } from 'short-uuid' import { generate } from 'short-uuid'
import path from 'path'
test.describe('Editor', () => { test.describe.parallel('Editor', () => {
test('Edges connection should work', async ({ page }) => {
const typebotId = generate()
await createTypebots([
{
id: typebotId,
},
])
await page.goto(`/typebots/${typebotId}/edit`)
await page.dragAndDrop('text=Button', '#editor-container', {
targetPosition: { x: 800, y: 400 },
})
await page.dragAndDrop(
'text=Text >> nth=0',
'[data-testid="block"] >> nth=1',
{
targetPosition: { x: 100, y: 50 },
}
)
await page.dragAndDrop(
'[data-testid="endpoint"]',
'[data-testid="block"] >> nth=1',
{ targetPosition: { x: 100, y: 10 } }
)
await expect(page.locator('[data-testid="edge"]')).toBeVisible()
await page.dragAndDrop(
'[data-testid="endpoint"]',
'[data-testid="step"] >> nth=1'
)
await expect(page.locator('[data-testid="edge"]')).toBeVisible()
await page.dragAndDrop('text=Date', '#editor-container', {
targetPosition: { x: 1000, y: 800 },
})
await page.dragAndDrop(
'[data-testid="endpoint"] >> nth=2',
'[data-testid="block"] >> nth=2',
{
targetPosition: { x: 100, y: 10 },
}
)
await expect(page.locator('[data-testid="edge"] >> nth=0')).toBeVisible()
await expect(page.locator('[data-testid="edge"] >> nth=1')).toBeVisible()
})
test('Drag and drop steps and items should work', async ({ page }) => {
const typebotId = generate()
await importTypebotInDatabase(
path.join(__dirname, '../fixtures/typebots/editor/buttonsDnd.json'),
{
id: typebotId,
}
)
// Steps dnd
await page.goto(`/typebots/${typebotId}/edit`)
await expect(page.locator('[data-testid="step"] >> nth=1')).toHaveText(
'Hello!'
)
await page.dragAndDrop('text=Hello', 'text=Item 1')
await expect(page.locator('[data-testid="step"] >> nth=2')).toHaveText(
'Hello!'
)
await page.dragAndDrop('text=Hello', '[data-testid="step"] >> text=Start')
await expect(page.locator('text=Block #4')).toBeVisible()
await page.dragAndDrop('text=Hello', 'text=Block #2')
await expect(page.locator('[data-testid="step"] >> nth=3')).toHaveText(
'Hello!'
)
// Items dnd
await expect(page.locator('[data-testid="item"] >> nth=0')).toHaveText(
'Item 1'
)
await page.dragAndDrop('text=Item 1', 'text=Item 3')
await expect(page.locator('[data-testid="item"] >> nth=2')).toHaveText(
'Item 1'
)
await expect(page.locator('[data-testid="item"] >> nth=1')).toHaveText(
'Item 3'
)
await page.dragAndDrop('text=Item 3', 'text=Item 2-3')
await expect(page.locator('[data-testid="item"] >> nth=6')).toHaveText(
'Item 3'
)
})
test('Undo / Redo buttons should work', async ({ page }) => { test('Undo / Redo buttons should work', async ({ page }) => {
const typebotId = generate() const typebotId = generate()
await createTypebots([ await createTypebots([

View File

@ -3,7 +3,7 @@ import {
createTypebots, createTypebots,
parseDefaultBlockWithStep, parseDefaultBlockWithStep,
} from '../../services/database' } from '../../services/database'
import { defaultChoiceInputOptions, InputStepType } from 'models' import { defaultChoiceInputOptions, InputStepType, ItemType } from 'models'
import { typebotViewer } from '../../services/selectorUtils' import { typebotViewer } from '../../services/selectorUtils'
import { generate } from 'short-uuid' import { generate } from 'short-uuid'
@ -15,7 +15,14 @@ test.describe.parallel('Buttons input step', () => {
id: typebotId, id: typebotId,
...parseDefaultBlockWithStep({ ...parseDefaultBlockWithStep({
type: InputStepType.CHOICE, type: InputStepType.CHOICE,
options: { ...defaultChoiceInputOptions, itemIds: ['choice1'] }, items: [
{
id: 'choice1',
stepId: 'step1',
type: ItemType.BUTTON,
},
],
options: { ...defaultChoiceInputOptions },
}), }),
}, },
]) ])
@ -23,14 +30,11 @@ test.describe.parallel('Buttons input step', () => {
await page.goto(`/typebots/${typebotId}/edit`) await page.goto(`/typebots/${typebotId}/edit`)
await page.fill('input[value="Click to edit"]', 'Item 1') await page.fill('input[value="Click to edit"]', 'Item 1')
await page.press('input[value="Item 1"]', 'Enter') await page.press('input[value="Item 1"]', 'Enter')
await page.locator('text=Item 1').hover()
await page.click('[aria-label="Add item"]')
await page.fill('input[value="Click to edit"]', 'Item 2') await page.fill('input[value="Click to edit"]', 'Item 2')
await page.press('input[value="Item 2"]', 'Enter') await page.press('input[value="Item 2"]', 'Enter')
await page.locator('text=Item 2').hover()
await page.click('[aria-label="Add item"]')
await page.fill('input[value="Click to edit"]', 'Item 3') await page.fill('input[value="Click to edit"]', 'Item 3')
await page.press('input[value="Item 3"]', 'Enter') await page.press('input[value="Item 3"]', 'Enter')
await page.press('input[value="Click to edit"]', 'Escape')
await page.click('text=Item 2', { button: 'right' }) await page.click('text=Item 2', { button: 'right' })
await page.click('text=Delete') await page.click('text=Delete')
await expect(page.locator('text=Item 2')).toBeHidden() await expect(page.locator('text=Item 2')).toBeHidden()

View File

@ -145,7 +145,7 @@ test.describe.parallel('Google sheets integration', () => {
.press('Enter') .press('Enter')
await expect( await expect(
typebotViewer(page).locator('text=Your name is: John Smith') typebotViewer(page).locator('text=Your name is: John Smith')
).toBeVisible() ).toBeVisible({ timeout: 30000 })
}) })
}) })

View File

@ -16,8 +16,7 @@ test.describe('Condition step', () => {
) )
await page.goto(`/typebots/${typebotId}/edit`) await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Configure...') await page.click('text=Configure... >> nth=0', { force: true })
await page.click('button:has-text("Add a comparison")')
await page.fill( await page.fill(
'input[placeholder="Search for a variable"] >> nth=-1', 'input[placeholder="Search for a variable"] >> nth=-1',
'Age' 'Age'
@ -41,8 +40,7 @@ test.describe('Condition step', () => {
'100' '100'
) )
await page.click('text=Configure...') await page.click('text=Configure...', { force: true })
await page.click('button:has-text("Add a comparison")')
await page.fill( await page.fill(
'input[placeholder="Search for a variable"] >> nth=-1', 'input[placeholder="Search for a variable"] >> nth=-1',
'Age' 'Age'
@ -54,7 +52,7 @@ test.describe('Condition step', () => {
await page.click('text=Preview') await page.click('text=Preview')
await typebotViewer(page) await typebotViewer(page)
.locator('input[placeholder="Type your answer..."]') .locator('input[placeholder="Type a number..."]')
.fill('15') .fill('15')
await typebotViewer(page).locator('text=Send').click() await typebotViewer(page).locator('text=Send').click()
await expect( await expect(
@ -63,7 +61,7 @@ test.describe('Condition step', () => {
await page.click('text=Restart') await page.click('text=Restart')
await typebotViewer(page) await typebotViewer(page)
.locator('input[placeholder="Type your answer..."]') .locator('input[placeholder="Type a number..."]')
.fill('45') .fill('45')
await typebotViewer(page).locator('text=Send').click() await typebotViewer(page).locator('text=Send').click()
await expect( await expect(
@ -72,7 +70,7 @@ test.describe('Condition step', () => {
await page.click('text=Restart') await page.click('text=Restart')
await typebotViewer(page) await typebotViewer(page)
.locator('input[placeholder="Type your answer..."]') .locator('input[placeholder="Type a number..."]')
.fill('90') .fill('90')
await typebotViewer(page).locator('text=Send').click() await typebotViewer(page).locator('text=Send').click()
await expect( await expect(

View File

@ -31,7 +31,7 @@ test.describe.parallel('Theme page', () => {
) )
await page.click('text=Color') await page.click('text=Color')
await page.click('[aria-label="Pick a color"]') await page.click('[aria-label="Pick a color"]')
await page.fill('[aria-label="Color value"]', '#2a9d8f') await page.fill('[aria-label="Color value"] >> nth=-1', '#2a9d8f')
await expect(chatContainer).toHaveCSS( await expect(chatContainer).toHaveCSS(
'background-color', 'background-color',
'rgb(42, 157, 143)' 'rgb(42, 157, 143)'
@ -56,10 +56,14 @@ test.describe.parallel('Theme page', () => {
await page.waitForTimeout(300) await page.waitForTimeout(300)
// Host bubbles // Host bubbles
await page.click(':nth-match([aria-label="Pick a color"], 1)') await page.click(
await page.fill('[aria-label="Color value"]', '#2a9d8f') '[data-testid="host-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=0'
await page.click(':nth-match([aria-label="Pick a color"], 2)') )
await page.fill('[aria-label="Color value"]', '#ffffff') await page.fill('input[value="#F7F8FF"]', '#2a9d8f')
await page.click(
'[data-testid="host-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=1'
)
await page.fill('input[value="#303235"]', '#ffffff')
const hostBubble = typebotViewer(page).locator( const hostBubble = typebotViewer(page).locator(
'[data-testid="host-bubble"]' '[data-testid="host-bubble"]'
) )
@ -70,19 +74,27 @@ test.describe.parallel('Theme page', () => {
await expect(hostBubble).toHaveCSS('color', 'rgb(255, 255, 255)') await expect(hostBubble).toHaveCSS('color', 'rgb(255, 255, 255)')
// Buttons // Buttons
await page.click(':nth-match([aria-label="Pick a color"], 5)') await page.click(
await page.fill('[aria-label="Color value"]', '#7209b7') '[data-testid="buttons-theme"] >> [aria-label="Pick a color"] >> nth=0'
await page.click(':nth-match([aria-label="Pick a color"], 6)') )
await page.fill('[aria-label="Color value"]', '#e9c46a') await page.fill('input[value="#0042DA"]', '#7209b7')
await page.click(
'[data-testid="buttons-theme"] >> [aria-label="Pick a color"] >> nth=1'
)
await page.fill('input[value="#FFFFFF"]', '#e9c46a')
const button = typebotViewer(page).locator('[data-testid="button"]') const button = typebotViewer(page).locator('[data-testid="button"]')
await expect(button).toHaveCSS('background-color', 'rgb(114, 9, 183)') await expect(button).toHaveCSS('background-color', 'rgb(114, 9, 183)')
await expect(button).toHaveCSS('color', 'rgb(233, 196, 106)') await expect(button).toHaveCSS('color', 'rgb(233, 196, 106)')
// Guest bubbles // Guest bubbles
await page.click(':nth-match([aria-label="Pick a color"], 3)') await page.click(
await page.fill('[aria-label="Color value"]', '#d8f3dc') '[data-testid="guest-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=0'
await page.click(':nth-match([aria-label="Pick a color"], 4)') )
await page.fill('[aria-label="Color value"]', '#264653') await page.fill('input[value="#FF8E21"]', '#d8f3dc')
await page.click(
'[data-testid="guest-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=1'
)
await page.fill('input[value="#FFFFFF"]', '#264653')
await typebotViewer(page).locator('text=Go').click() await typebotViewer(page).locator('text=Go').click()
const guestBubble = typebotViewer(page).locator( const guestBubble = typebotViewer(page).locator(
'[data-testid="guest-bubble"]' '[data-testid="guest-bubble"]'
@ -94,10 +106,14 @@ test.describe.parallel('Theme page', () => {
await expect(guestBubble).toHaveCSS('color', 'rgb(38, 70, 83)') await expect(guestBubble).toHaveCSS('color', 'rgb(38, 70, 83)')
// Input // Input
await page.click(':nth-match([aria-label="Pick a color"], 7)') await page.click(
await page.fill('[aria-label="Color value"]', '#ffe8d6') '[data-testid="inputs-theme"] >> [aria-label="Pick a color"] >> nth=0'
await page.click(':nth-match([aria-label="Pick a color"], 8)') )
await page.fill('[aria-label="Color value"]', '#023e8a') await page.fill('input[value="#FFFFFF"]', '#ffe8d6')
await page.click(
'[data-testid="inputs-theme"] >> [aria-label="Pick a color"] >> nth=1'
)
await page.fill('input[value="#303235"]', '#023e8a')
await typebotViewer(page).locator('text=Go').click() await typebotViewer(page).locator('text=Go').click()
const input = typebotViewer(page).locator('.typebot-input') const input = typebotViewer(page).locator('.typebot-input')
await expect(input).toHaveCSS('background-color', 'rgb(255, 232, 214)') await expect(input).toHaveCSS('background-color', 'rgb(255, 232, 214)')

View File

@ -1,13 +1,11 @@
import { Edge, Table, Target } from 'models' import { Edge, IdMap } from 'models'
import { AnchorsPositionProps } from 'components/shared/Graph/Edges/Edge' import { AnchorsPositionProps } from 'components/shared/Graph/Edges/Edge'
import { import {
stubLength, stubLength,
blockWidth, blockWidth,
blockAnchorsOffset, blockAnchorsOffset,
ConnectingIds,
Endpoint, Endpoint,
Coordinates, Coordinates,
BlocksCoordinates,
} 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'
@ -230,20 +228,11 @@ export const computeEdgePath = ({
} }
export const computeConnectingEdgePath = ({ export const computeConnectingEdgePath = ({
connectingIds, sourceBlockCoordinates,
targetBlockCoordinates,
sourceTop, sourceTop,
targetTop, targetTop,
blocksCoordinates, }: GetAnchorsPositionParams) => {
}: {
connectingIds: Omit<ConnectingIds, 'target'> & { target: Target }
sourceTop: number
targetTop?: number
blocksCoordinates: BlocksCoordinates
}) => {
const sourceBlockCoordinates =
blocksCoordinates.byId[connectingIds.source.blockId]
const targetBlockCoordinates =
blocksCoordinates.byId[connectingIds.target.blockId]
const anchorsPosition = getAnchorsPosition({ const anchorsPosition = getAnchorsPosition({
sourceBlockCoordinates, sourceBlockCoordinates,
targetBlockCoordinates, targetBlockCoordinates,
@ -254,23 +243,25 @@ export const computeConnectingEdgePath = ({
} }
export const computeEdgePathToMouse = ({ export const computeEdgePathToMouse = ({
blockPosition, sourceBlockCoordinates,
mousePosition, mousePosition,
sourceTop, sourceTop,
}: { }: {
blockPosition: Coordinates sourceBlockCoordinates: Coordinates
mousePosition: Coordinates mousePosition: Coordinates
sourceTop: number sourceTop: number
}): string => { }): string => {
const sourcePosition = { const sourcePosition = {
x: x:
mousePosition.x - blockPosition.x > blockWidth / 2 mousePosition.x - sourceBlockCoordinates.x > blockWidth / 2
? blockPosition.x + blockWidth - 40 ? sourceBlockCoordinates.x + blockWidth - 40
: blockPosition.x + 40, : sourceBlockCoordinates.x + 40,
y: sourceTop, y: sourceTop,
} }
const sourceType = const sourceType =
mousePosition.x - blockPosition.x > blockWidth / 2 ? 'right' : 'left' mousePosition.x - sourceBlockCoordinates.x > blockWidth / 2
? 'right'
: 'left'
const segments = computeThreeSegments( const segments = computeThreeSegments(
sourcePosition, sourcePosition,
mousePosition, mousePosition,
@ -284,11 +275,11 @@ export const computeEdgePathToMouse = ({
export const getEndpointTopOffset = ( export const getEndpointTopOffset = (
graphPosition: Coordinates, graphPosition: Coordinates,
endpoints: Table<Endpoint>, endpoints: IdMap<Endpoint>,
endpointId?: string endpointId?: string
): number | undefined => { ): number | undefined => {
if (!endpointId) return if (!endpointId) return
const endpointRef = endpoints.byId[endpointId]?.ref const endpointRef = endpoints[endpointId]?.ref
if (!endpointRef) return if (!endpointRef) return
return ( return (
8 + 8 +
@ -299,4 +290,4 @@ export const getEndpointTopOffset = (
} }
export const getSourceEndpointId = (edge?: Edge) => export const getSourceEndpointId = (edge?: Edge) =>
edge?.from.buttonId ?? edge?.from.stepId + `${edge?.from.conditionType ?? ''}` edge?.from.itemId ?? edge?.from.stepId

View File

@ -2,7 +2,7 @@ import { sendRequest } from 'utils'
import { stringify } from 'qs' import { stringify } from 'qs'
import useSWR from 'swr' import useSWR from 'swr'
import { fetcher } from './utils' import { fetcher } from './utils'
import { Table, Variable, VariableForTest, WebhookResponse } from 'models' import { StepIndices, Variable, VariableForTest, WebhookResponse } from 'models'
export const getGoogleSheetsConsentScreenUrl = ( export const getGoogleSheetsConsentScreenUrl = (
redirectUrl: string, redirectUrl: string,
@ -69,10 +69,11 @@ export const useSheets = ({
export const executeWebhook = ( export const executeWebhook = (
typebotId: string, typebotId: string,
webhookId: string, webhookId: string,
variables: Table<Variable> variables: Variable[],
{ blockIndex, stepIndex }: StepIndices
) => ) =>
sendRequest<WebhookResponse>({ sendRequest<WebhookResponse>({
url: `/api/typebots/${typebotId}/webhooks/${webhookId}/execute`, url: `/api/typebots/${typebotId}/blocks/${blockIndex}/steps/${stepIndex}/executeWebhook`,
method: 'POST', method: 'POST',
body: { body: {
variables, variables,
@ -80,28 +81,21 @@ export const executeWebhook = (
}) })
export const convertVariableForTestToVariables = ( export const convertVariableForTestToVariables = (
variablesForTest: Table<VariableForTest> | undefined, variablesForTest: VariableForTest[],
variables: Table<Variable> variables: Variable[]
): Table<Variable> => { ): Variable[] => {
if (!variablesForTest) return { byId: {}, allIds: [] } if (!variablesForTest) return []
return { return [
byId: { ...variables,
...variables.byId, ...variablesForTest
...variablesForTest.allIds.reduce((obj, id) => { .filter((v) => v.variableId)
const variableForTest = variablesForTest.byId[id] .map((variableForTest) => {
if (!variableForTest.variableId) return {} const variable = variables.find(
const variable = variables.byId[variableForTest.variableId ?? ''] (v) => v.id === variableForTest.variableId
return { ) as Variable
...obj, return { ...variable, value: variableForTest.value }
[variableForTest.variableId]: {
...variable,
value: variableForTest.value,
},
}
}, {}), }, {}),
}, ]
allIds: variables.allIds,
}
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -1,26 +1,39 @@
import { PublicTypebot, Typebot } from 'models' import {
Block,
InputStep,
PublicBlock,
PublicStep,
PublicTypebot,
Step,
Typebot,
} from 'models'
import shortId from 'short-uuid' import shortId from 'short-uuid'
import { HStack, Text } from '@chakra-ui/react' import { HStack, Text } from '@chakra-ui/react'
import { CalendarIcon } from 'assets/icons' import { CalendarIcon } from 'assets/icons'
import { StepIcon } from 'components/editor/StepsSideBar/StepIcon' import { StepIcon } from 'components/editor/StepsSideBar/StepIcon'
import { isInputStep, sendRequest } from 'utils' import { isInputStep, sendRequest } from 'utils'
import { isDefined } from '@udecode/plate-common'
export const parseTypebotToPublicTypebot = ( export const parseTypebotToPublicTypebot = (
typebot: Typebot typebot: Typebot
): PublicTypebot => ({ ): PublicTypebot => ({
...typebot,
id: shortId.generate(), id: shortId.generate(),
blocks: typebot.blocks,
steps: typebot.steps,
name: typebot.name,
typebotId: typebot.id, typebotId: typebot.id,
theme: typebot.theme, blocks: parseBlocksToPublicBlocks(typebot.blocks),
settings: typebot.settings,
publicId: typebot.publicId,
choiceItems: typebot.choiceItems,
variables: typebot.variables,
edges: typebot.edges,
}) })
const parseBlocksToPublicBlocks = (blocks: Block[]): PublicBlock[] =>
blocks.map((b) => ({
...b,
steps: b.steps.map(
(s) =>
('webhook' in s && isDefined(s.webhook)
? { ...s, webhook: s.webhook.id }
: s) as PublicStep
),
}))
export const createPublishedTypebot = async ( export const createPublishedTypebot = async (
typebot: Omit<PublicTypebot, 'id'> typebot: Omit<PublicTypebot, 'id'>
) => ) =>
@ -41,12 +54,11 @@ export const updatePublishedTypebot = async (
}) })
export const parseSubmissionsColumns = ( export const parseSubmissionsColumns = (
typebot?: PublicTypebot typebot: PublicTypebot
): { ): {
Header: JSX.Element Header: JSX.Element
accessor: string accessor: string
}[] => { }[] => {
if (!typebot) return []
return [ return [
{ {
Header: ( Header: (
@ -57,14 +69,14 @@ export const parseSubmissionsColumns = (
), ),
accessor: 'createdAt', accessor: 'createdAt',
}, },
...typebot.blocks.allIds ...typebot.blocks
.filter((blockId) => typebot && blockContainsInput(typebot, blockId)) .filter(
.map((blockId) => { (block) => typebot && block.steps.some((step) => isInputStep(step))
const block = typebot.blocks.byId[blockId] )
const inputStepId = block.stepIds.find((stepId) => .map((block) => {
isInputStep(typebot.steps.byId[stepId]) const inputStep = block.steps.find((step) =>
) isInputStep(step)
const inputStep = typebot.steps.byId[inputStepId as string] ) as InputStep
return { return {
Header: ( Header: (
<HStack> <HStack>
@ -72,16 +84,8 @@ export const parseSubmissionsColumns = (
<Text>{block.title}</Text> <Text>{block.title}</Text>
</HStack> </HStack>
), ),
accessor: blockId, accessor: block.id,
} }
}), }),
] ]
} }
const blockContainsInput = (
typebot: PublicTypebot | Typebot,
blockId: string
) =>
typebot.blocks.byId[blockId].stepIds.some((stepId) =>
isInputStep(typebot.steps.byId[stepId])
)

View File

@ -24,18 +24,27 @@ import {
defaultUrlInputOptions, defaultUrlInputOptions,
defaultChoiceInputOptions, defaultChoiceInputOptions,
defaultSetVariablesOptions, defaultSetVariablesOptions,
defaultConditionOptions,
defaultRedirectOptions, defaultRedirectOptions,
defaultGoogleSheetsOptions, defaultGoogleSheetsOptions,
defaultGoogleAnalyticsOptions, defaultGoogleAnalyticsOptions,
defaultWebhookOptions, defaultWebhookOptions,
StepWithOptionsType, StepWithOptionsType,
defaultWebhookAttributes,
Webhook,
Item,
ItemType,
defaultConditionContent,
} from 'models' } from 'models'
import shortId, { generate } from 'short-uuid' import shortId, { generate } from 'short-uuid'
import { Typebot } from 'models' import { Typebot } from 'models'
import useSWR from 'swr' import useSWR from 'swr'
import { fetcher, toKebabCase } from './utils' import { fetcher, toKebabCase } from './utils'
import { isBubbleStepType, stepTypeHasOption } from 'utils' import {
isBubbleStepType,
stepTypeHasItems,
stepTypeHasOption,
stepTypeHasWebhook,
} from 'utils'
import { deepEqual } from 'fast-equals' import { deepEqual } from 'fast-equals'
import { stringify } from 'qs' import { stringify } from 'qs'
import { isChoiceInput, isConditionStep, sendRequest } from 'utils' import { isChoiceInput, isConditionStep, sendRequest } from 'utils'
@ -125,9 +134,35 @@ export const parseNewStep = (
options: stepTypeHasOption(type) options: stepTypeHasOption(type)
? parseDefaultStepOptions(type) ? parseDefaultStepOptions(type)
: undefined, : undefined,
webhook: stepTypeHasWebhook(type) ? parseDefaultWebhook() : undefined,
items: stepTypeHasItems(type) ? parseDefaultItems(type, id) : undefined,
} as DraggableStep } as DraggableStep
} }
const parseDefaultWebhook = (): Webhook => ({
id: generate(),
...defaultWebhookAttributes,
})
const parseDefaultItems = (
type: LogicStepType.CONDITION | InputStepType.CHOICE,
stepId: string
): Item[] => {
switch (type) {
case InputStepType.CHOICE:
return [{ id: generate(), stepId, type: ItemType.BUTTON }]
case LogicStepType.CONDITION:
return [
{
id: generate(),
stepId,
type: ItemType.CONDITION,
content: defaultConditionContent,
},
]
}
}
const parseDefaultContent = (type: BubbleStepType): BubbleStepContent => { const parseDefaultContent = (type: BubbleStepType): BubbleStepContent => {
switch (type) { switch (type) {
case BubbleStepType.TEXT: case BubbleStepType.TEXT:
@ -154,11 +189,9 @@ const parseDefaultStepOptions = (type: StepWithOptionsType): StepOptions => {
case InputStepType.URL: case InputStepType.URL:
return defaultUrlInputOptions return defaultUrlInputOptions
case InputStepType.CHOICE: case InputStepType.CHOICE:
return { ...defaultChoiceInputOptions, itemIds: [generate()] } return defaultChoiceInputOptions
case LogicStepType.SET_VARIABLE: case LogicStepType.SET_VARIABLE:
return defaultSetVariablesOptions return defaultSetVariablesOptions
case LogicStepType.CONDITION:
return defaultConditionOptions
case LogicStepType.REDIRECT: case LogicStepType.REDIRECT:
return defaultRedirectOptions return defaultRedirectOptions
case IntegrationStepType.GOOGLE_SHEETS: case IntegrationStepType.GOOGLE_SHEETS:
@ -166,7 +199,7 @@ const parseDefaultStepOptions = (type: StepWithOptionsType): StepOptions => {
case IntegrationStepType.GOOGLE_ANALYTICS: case IntegrationStepType.GOOGLE_ANALYTICS:
return defaultGoogleAnalyticsOptions return defaultGoogleAnalyticsOptions
case IntegrationStepType.WEBHOOK: case IntegrationStepType.WEBHOOK:
return { ...defaultWebhookOptions, webhookId: generate() } return defaultWebhookOptions
} }
} }
@ -181,7 +214,6 @@ export const checkIfPublished = (
publicTypebot: PublicTypebot publicTypebot: PublicTypebot
) => ) =>
deepEqual(typebot.blocks, publicTypebot.blocks) && deepEqual(typebot.blocks, publicTypebot.blocks) &&
deepEqual(typebot.steps, publicTypebot.steps) &&
typebot.name === publicTypebot.name && typebot.name === publicTypebot.name &&
typebot.publicId === publicTypebot.publicId && typebot.publicId === publicTypebot.publicId &&
deepEqual(typebot.settings, publicTypebot.settings) && deepEqual(typebot.settings, publicTypebot.settings) &&
@ -214,18 +246,15 @@ export const parseNewTypebot = ({
id: startBlockId, id: startBlockId,
title: 'Start', title: 'Start',
graphCoordinates: { x: 0, y: 0 }, graphCoordinates: { x: 0, y: 0 },
stepIds: [startStepId], steps: [startStep],
} }
return { return {
folderId, folderId,
name, name,
ownerId, ownerId,
blocks: { byId: { [startBlockId]: startBlock }, allIds: [startBlockId] }, blocks: [startBlock],
steps: { byId: { [startStepId]: startStep }, allIds: [startStepId] }, edges: [],
choiceItems: { byId: {}, allIds: [] }, variables: [],
variables: { byId: {}, allIds: [] },
edges: { byId: {}, allIds: [] },
webhooks: { byId: {}, allIds: [] },
theme: defaultTheme, theme: defaultTheme,
settings: defaultSettings, settings: defaultSettings,
} }

View File

@ -100,11 +100,8 @@ export const removeUndefinedFields = <T>(obj: T): T =>
export const stepHasOptions = (step: Step) => 'options' in step export const stepHasOptions = (step: Step) => 'options' in step
export const parseVariableHighlight = (content: string, typebot?: Typebot) => { export const parseVariableHighlight = (content: string, typebot: Typebot) => {
if (!typebot) return content const varNames = typebot.variables.map((v) => v.name)
const varNames = typebot.variables.allIds.map(
(varId) => typebot.variables.byId[varId].name
)
return content.replace(/\{\{(.*?)\}\}/g, (fullMatch, foundVar) => { return content.replace(/\{\{(.*?)\}\}/g, (fullMatch, foundVar) => {
if (varNames.some((val) => foundVar.includes(val))) { if (varNames.some((val) => foundVar.includes(val))) {
return `<span style="background-color:#ff8b1a; color:#ffffff; padding: 0.125rem 0.25rem; border-radius: 0.35rem">${fullMatch.replace( return `<span style="background-color:#ff8b1a; color:#ffffff; padding: 0.125rem 0.25rem; border-radius: 0.35rem">${fullMatch.replace(
@ -115,3 +112,8 @@ export const parseVariableHighlight = (content: string, typebot?: Typebot) => {
return fullMatch return fullMatch
}) })
} }
export const setMultipleRefs =
(refs: React.MutableRefObject<HTMLDivElement | null>[]) =>
(elem: HTMLDivElement) =>
refs.forEach((ref) => (ref.current = elem))

View File

@ -5,7 +5,6 @@ import { upsertAnswer } from 'services/answer'
import { SEO } from '../components/Seo' import { SEO } from '../components/Seo'
import { createResult, updateResult } from '../services/result' import { createResult, updateResult } from '../services/result'
import { ErrorPage } from './ErrorPage' import { ErrorPage } from './ErrorPage'
import { NotFoundPage } from './NotFoundPage'
export type TypebotPageProps = { export type TypebotPageProps = {
typebot?: PublicTypebot typebot?: PublicTypebot
@ -15,7 +14,11 @@ export type TypebotPageProps = {
const sessionStorageKey = 'resultId' const sessionStorageKey = 'resultId'
export const TypebotPage = ({ typebot, isIE, url }: TypebotPageProps) => { export const TypebotPage = ({
typebot,
isIE,
url,
}: TypebotPageProps & { typebot: PublicTypebot }) => {
const [error, setError] = useState<Error | undefined>( const [error, setError] = useState<Error | undefined>(
isIE ? new Error('Internet explorer is not supported') : undefined isIE ? new Error('Internet explorer is not supported') : undefined
) )
@ -27,7 +30,6 @@ export const TypebotPage = ({ typebot, isIE, url }: TypebotPageProps) => {
}, []) }, [])
const initializeResult = async () => { const initializeResult = async () => {
if (!typebot) return
const resultIdFromSession = sessionStorage.getItem(sessionStorageKey) const resultIdFromSession = sessionStorage.getItem(sessionStorageKey)
if (resultIdFromSession) setResultId(resultIdFromSession) if (resultIdFromSession) setResultId(resultIdFromSession)
else { else {
@ -52,9 +54,6 @@ export const TypebotPage = ({ typebot, isIE, url }: TypebotPageProps) => {
if (error) setError(error) if (error) setError(error)
} }
if (!typebot) {
return <NotFoundPage />
}
if (error) { if (error) {
return <ErrorPage error={error} /> return <ErrorPage error={error} />
} }

View File

@ -1,3 +1,4 @@
import { NotFoundPage } from 'layouts/NotFoundPage'
import { PublicTypebot } from 'models' import { PublicTypebot } from 'models'
import { GetServerSideProps, GetServerSidePropsContext } from 'next' import { GetServerSideProps, GetServerSidePropsContext } from 'next'
import { TypebotPage, TypebotPageProps } from '../layouts/TypebotPage' import { TypebotPage, TypebotPageProps } from '../layouts/TypebotPage'
@ -12,7 +13,6 @@ export const getServerSideProps: GetServerSideProps = async (
try { try {
if (!context.req.headers.host) return { props: {} } if (!context.req.headers.host) return { props: {} }
typebot = await getTypebotFromPublicId(context.query.publicId?.toString()) typebot = await getTypebotFromPublicId(context.query.publicId?.toString())
if (!typebot) return { props: {} }
return { return {
props: { props: {
typebot, typebot,
@ -41,5 +41,7 @@ const getTypebotFromPublicId = async (
return (typebot as unknown as PublicTypebot | undefined) ?? undefined return (typebot as unknown as PublicTypebot | undefined) ?? undefined
} }
const App = (props: TypebotPageProps) => <TypebotPage {...props} /> const App = ({ typebot, ...props }: TypebotPageProps) =>
typebot ? <TypebotPage typebot={typebot} {...props} /> : <NotFoundPage />
export default App export default App

View File

@ -1,3 +1,4 @@
import { NotFoundPage } from 'layouts/NotFoundPage'
import { PublicTypebot } from 'models' import { PublicTypebot } from 'models'
import { GetServerSideProps, GetServerSidePropsContext } from 'next' import { GetServerSideProps, GetServerSidePropsContext } from 'next'
import { TypebotPage, TypebotPageProps } from '../layouts/TypebotPage' import { TypebotPage, TypebotPageProps } from '../layouts/TypebotPage'
@ -12,7 +13,6 @@ export const getServerSideProps: GetServerSideProps = async (
try { try {
if (!context.req.headers.host) return { props: {} } if (!context.req.headers.host) return { props: {} }
typebot = await getTypebotFromUrl(context.req.headers.host) typebot = await getTypebotFromUrl(context.req.headers.host)
if (!typebot) return { props: {} }
return { return {
props: { props: {
typebot, typebot,
@ -42,5 +42,6 @@ const getTypebotFromUrl = async (
return (typebot as unknown as PublicTypebot | undefined) ?? undefined return (typebot as unknown as PublicTypebot | undefined) ?? undefined
} }
const App = (props: TypebotPageProps) => <TypebotPage {...props} /> const App = ({ typebot, ...props }: TypebotPageProps) =>
typebot ? <TypebotPage {...props} typebot={typebot} /> : <NotFoundPage />
export default App export default App

Some files were not shown because too many files have changed in this diff Show More