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:
@ -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>
|
||||||
|
@ -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'
|
||||||
|
@ -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'
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
@ -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,
|
||||||
])
|
])
|
||||||
|
@ -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"
|
||||||
|
@ -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'}
|
||||||
|
@ -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" />
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export { ButtonNodesList } from './ButtonNodesList'
|
|
@ -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])
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
@ -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} />
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
@ -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 '!='
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { ItemNodeContent } from './ItemNodeContent'
|
@ -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>
|
@ -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"
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { ItemNodesList } from './ItemNodesList'
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
|
@ -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'
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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 (
|
||||||
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -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 === ''
|
||||||
|
@ -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'}
|
||||||
|
@ -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">
|
||||||
|
@ -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{' '}
|
||||||
|
@ -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'
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
125
apps/builder/contexts/GraphDndContext.tsx
Normal file
125
apps/builder/contexts/GraphDndContext.tsx
Normal 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)
|
@ -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)
|
|
@ -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,
|
||||||
|
@ -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 }
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
71
apps/builder/contexts/TypebotContext/actions/items.ts
Normal file
71
apps/builder/contexts/TypebotContext/actions/items.ts
Normal 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 }
|
@ -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 }
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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 />
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
@ -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: {
|
||||||
|
185
apps/builder/playwright/fixtures/typebots/editor/buttonsDnd.json
Normal file
185
apps/builder/playwright/fixtures/typebots/editor/buttonsDnd.json
Normal 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
|
||||||
|
}
|
@ -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": {
|
||||||
|
@ -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": {
|
||||||
|
@ -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 |
@ -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 |
@ -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 |
@ -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": {
|
||||||
|
@ -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": {
|
||||||
|
@ -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": {
|
||||||
|
@ -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": {
|
||||||
|
@ -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 })
|
||||||
|
@ -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 = (
|
||||||
|
@ -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()
|
||||||
|
@ -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(
|
||||||
|
@ -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([
|
||||||
|
@ -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()
|
||||||
|
@ -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 })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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)')
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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])
|
|
||||||
)
|
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
@ -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} />
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
Reference in New Issue
Block a user