2
0

feat(editor): ️ Optimize graph navigation

This commit is contained in:
Baptiste Arnaud
2022-06-26 16:12:28 +02:00
parent d7b9bda5d5
commit fc4db575ac
17 changed files with 497 additions and 311 deletions

View File

@ -1,6 +1,11 @@
import { useEventListener } from '@chakra-ui/hooks' import { useEventListener } from '@chakra-ui/hooks'
import assert from 'assert' import assert from 'assert'
import { useGraph, ConnectingIds } from 'contexts/GraphContext' import {
useGraph,
ConnectingIds,
Coordinates,
useGroupsCoordinates,
} from 'contexts/GraphContext'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { colors } from 'libs/theme' import { colors } from 'libs/theme'
import React, { useMemo, useState } from 'react' import React, { useMemo, useState } from 'react'
@ -17,10 +22,10 @@ export const DrawingEdge = () => {
connectingIds, connectingIds,
sourceEndpoints, sourceEndpoints,
targetEndpoints, targetEndpoints,
groupsCoordinates,
} = useGraph() } = useGraph()
const { groupsCoordinates } = useGroupsCoordinates()
const { createEdge } = useTypebot() const { createEdge } = useTypebot()
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }) const [mousePosition, setMousePosition] = useState<Coordinates | null>(null)
const sourceGroupCoordinates = const sourceGroupCoordinates =
groupsCoordinates && groupsCoordinates[connectingIds?.source.groupId ?? ''] groupsCoordinates && groupsCoordinates[connectingIds?.source.groupId ?? '']
@ -50,7 +55,7 @@ export const DrawingEdge = () => {
}, [connectingIds, targetEndpoints]) }, [connectingIds, targetEndpoints])
const path = useMemo(() => { const path = useMemo(() => {
if (!sourceTop || !sourceGroupCoordinates) return `` if (!sourceTop || !sourceGroupCoordinates || !mousePosition) return ``
return targetGroupCoordinates return targetGroupCoordinates
? computeConnectingEdgePath({ ? computeConnectingEdgePath({
@ -75,6 +80,10 @@ export const DrawingEdge = () => {
]) ])
const handleMouseMove = (e: MouseEvent) => { const handleMouseMove = (e: MouseEvent) => {
if (!connectingIds) {
if (mousePosition) setMousePosition(null)
return
}
const coordinates = { const coordinates = {
x: (e.clientX - graphPosition.x) / graphPosition.scale, x: (e.clientX - graphPosition.x) / graphPosition.scale,
y: (e.clientY - graphPosition.y) / graphPosition.scale, y: (e.clientY - graphPosition.y) / graphPosition.scale,
@ -92,7 +101,10 @@ export const DrawingEdge = () => {
createEdge({ from: connectingIds.source, to: connectingIds.target }) createEdge({ from: connectingIds.source, to: connectingIds.target })
} }
if ((mousePosition.x === 0 && mousePosition.y === 0) || !connectingIds) if (
(mousePosition && mousePosition.x === 0 && mousePosition.y === 0) ||
!connectingIds
)
return <></> return <></>
return ( return (
<path <path

View File

@ -1,5 +1,5 @@
import { VStack, Tag, Text, Tooltip } from '@chakra-ui/react' import { VStack, Tag, Text, Tooltip } from '@chakra-ui/react'
import { useGraph } from 'contexts/GraphContext' import { useGraph, useGroupsCoordinates } from 'contexts/GraphContext'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { useWorkspace } from 'contexts/WorkspaceContext' import { useWorkspace } from 'contexts/WorkspaceContext'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
@ -24,7 +24,8 @@ export const DropOffEdge = ({
onUnlockProPlanClick, onUnlockProPlanClick,
}: Props) => { }: Props) => {
const { workspace } = useWorkspace() const { workspace } = useWorkspace()
const { sourceEndpoints, groupsCoordinates, graphPosition } = useGraph() const { groupsCoordinates } = useGroupsCoordinates()
const { sourceEndpoints, graphPosition } = useGraph()
const { publishedTypebot } = useTypebot() const { publishedTypebot } = useTypebot()
const isUserOnFreePlan = isFreePlan(workspace) const isUserOnFreePlan = isFreePlan(workspace)

View File

@ -1,4 +1,8 @@
import { Coordinates, useGraph } from 'contexts/GraphContext' import {
Coordinates,
useGraph,
useGroupsCoordinates,
} from 'contexts/GraphContext'
import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react' import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react'
import { import {
getAnchorsPosition, getAnchorsPosition,
@ -19,17 +23,20 @@ export type AnchorsPositionProps = {
totalSegments: number totalSegments: number
} }
export const Edge = ({ edge }: { edge: EdgeProps }) => { type Props = {
edge: EdgeProps
}
export const Edge = ({ edge }: Props) => {
const { deleteEdge } = useTypebot() const { deleteEdge } = useTypebot()
const { const {
previewingEdge, previewingEdge,
sourceEndpoints, sourceEndpoints,
targetEndpoints, targetEndpoints,
groupsCoordinates,
graphPosition, graphPosition,
isReadOnly, isReadOnly,
setPreviewingEdge, setPreviewingEdge,
} = useGraph() } = useGraph()
const { groupsCoordinates } = useGroupsCoordinates()
const [isMouseOver, setIsMouseOver] = useState(false) const [isMouseOver, setIsMouseOver] = useState(false)
const { isOpen, onOpen, onClose } = useDisclosure() const { isOpen, onOpen, onClose } = useDisclosure()
const [edgeMenuPosition, setEdgeMenuPosition] = useState({ x: 0, y: 0 }) const [edgeMenuPosition, setEdgeMenuPosition] = useState({ x: 0, y: 0 })

View File

@ -1,5 +1,5 @@
import { BoxProps, Flex } from '@chakra-ui/react' import { BoxProps, Flex } from '@chakra-ui/react'
import { useGraph } from 'contexts/GraphContext' import { useGraph, useGroupsCoordinates } from 'contexts/GraphContext'
import { Source } from 'models' import { Source } from 'models'
import React, { MouseEvent, useEffect, useRef, useState } from 'react' import React, { MouseEvent, useEffect, useRef, useState } from 'react'
@ -10,12 +10,9 @@ export const SourceEndpoint = ({
source: Source source: Source
}) => { }) => {
const [ranOnce, setRanOnce] = useState(false) const [ranOnce, setRanOnce] = useState(false)
const { const { setConnectingIds, addSourceEndpoint, previewingEdge } = useGraph()
setConnectingIds,
addSourceEndpoint, const { groupsCoordinates } = useGroupsCoordinates()
groupsCoordinates,
previewingEdge,
} = useGraph()
const ref = useRef<HTMLDivElement | null>(null) const ref = useRef<HTMLDivElement | null>(null)
const handleMouseDown = (e: MouseEvent<HTMLDivElement>) => { const handleMouseDown = (e: MouseEvent<HTMLDivElement>) => {

View File

@ -1,10 +1,11 @@
import { Flex, FlexProps, useEventListener } from '@chakra-ui/react' import { Flex, FlexProps, useEventListener } from '@chakra-ui/react'
import React, { useRef, useMemo, useEffect, useState } from 'react' import React, { useRef, useMemo, useEffect, useState, useCallback } from 'react'
import { import {
blockWidth, blockWidth,
Coordinates, Coordinates,
graphPositionDefaultValue, graphPositionDefaultValue,
useGraph, useGraph,
useGroupsCoordinates,
} from 'contexts/GraphContext' } from 'contexts/GraphContext'
import { useBlockDnd } from 'contexts/GraphDndContext' import { useBlockDnd } from 'contexts/GraphDndContext'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
@ -12,7 +13,7 @@ import { DraggableBlockType, PublicTypebot, Typebot } from 'models'
import { AnswersCount } from 'services/analytics' import { AnswersCount } from 'services/analytics'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable' import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'
import GraphContent from './GraphContent' import GraphElements from './GraphElements'
import cuid from 'cuid' import cuid from 'cuid'
import { headerHeight } from '../TypebotHeader' import { headerHeight } from '../TypebotHeader'
import { useUser } from 'contexts/UserContext' import { useUser } from 'contexts/UserContext'
@ -29,7 +30,7 @@ export const Graph = ({
onUnlockProPlanClick, onUnlockProPlanClick,
...props ...props
}: { }: {
typebot?: Typebot | PublicTypebot typebot: Typebot | PublicTypebot
answersCounts?: AnswersCount[] answersCounts?: AnswersCount[]
onUnlockProPlanClick?: () => void onUnlockProPlanClick?: () => void
} & FlexProps) => { } & FlexProps) => {
@ -47,10 +48,10 @@ export const Graph = ({
const { const {
setGraphPosition: setGlobalGraphPosition, setGraphPosition: setGlobalGraphPosition,
setOpenedBlockId, setOpenedBlockId,
updateGroupCoordinates,
setPreviewingEdge, setPreviewingEdge,
connectingIds, connectingIds,
} = useGraph() } = useGraph()
const { updateGroupCoordinates } = useGroupsCoordinates()
const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue) const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue)
const [debouncedGraphPosition] = useDebounce(graphPosition, 200) const [debouncedGraphPosition] = useDebounce(graphPosition, 200)
const transform = useMemo( const transform = useMemo(
@ -177,13 +178,16 @@ export const Graph = ({
useEventListener('mouseup', handleMouseUp, graphContainerRef.current) useEventListener('mouseup', handleMouseUp, graphContainerRef.current)
useEventListener('click', handleClick, editorContainerRef.current) useEventListener('click', handleClick, editorContainerRef.current)
useEventListener('mousemove', handleMouseMove) useEventListener('mousemove', handleMouseMove)
// eslint-disable-next-line react-hooks/exhaustive-deps
const zoomIn = useCallback(() => zoom(zoomButtonsScaleBlock), [])
// eslint-disable-next-line react-hooks/exhaustive-deps
const zoomOut = useCallback(() => zoom(-zoomButtonsScaleBlock), [])
return ( return (
<DraggableCore onDrag={onDrag} enableUserSelectHack={false}> <DraggableCore onDrag={onDrag} enableUserSelectHack={false}>
<Flex ref={graphContainerRef} position="relative" {...props}> <Flex ref={graphContainerRef} position="relative" {...props}>
<ZoomButtons <ZoomButtons onZoomIn={zoomIn} onZoomOut={zoomOut} />
onZoomIn={() => zoom(zoomButtonsScaleBlock)}
onZoomOut={() => zoom(-zoomButtonsScaleBlock)}
/>
<Flex <Flex
flex="1" flex="1"
w="full" w="full"
@ -195,7 +199,9 @@ export const Graph = ({
willChange="transform" willChange="transform"
transformOrigin="0px 0px 0px" transformOrigin="0px 0px 0px"
> >
<GraphContent <GraphElements
edges={typebot.edges}
groups={typebot.groups}
answersCounts={answersCounts} answersCounts={answersCounts}
onUnlockProPlanClick={onUnlockProPlanClick} onUnlockProPlanClick={onUnlockProPlanClick}
/> />

View File

@ -1,31 +0,0 @@
import { useTypebot } from 'contexts/TypebotContext'
import { Group } from 'models'
import React from 'react'
import { AnswersCount } from 'services/analytics'
import { Edges } from './Edges'
import { GroupNode } from './Nodes/GroupNode'
type Props = {
answersCounts?: AnswersCount[]
onUnlockProPlanClick?: () => void
}
const MyComponent = ({ answersCounts, onUnlockProPlanClick }: Props) => {
const { typebot } = useTypebot()
return (
<>
<Edges
edges={typebot?.edges ?? []}
answersCounts={answersCounts}
onUnlockProPlanClick={onUnlockProPlanClick}
/>
{typebot?.groups.map((group, idx) => (
<GroupNode group={group as Group} groupIndex={idx} key={group.id} />
))}
</>
)
}
// Performance hack, never rerender when graph (parent) is panned
const areEqual = () => true
export default React.memo(MyComponent, areEqual)

View File

@ -0,0 +1,33 @@
import { Edge, Group } from 'models'
import React, { memo } from 'react'
import { AnswersCount } from 'services/analytics'
import { Edges } from './Edges'
import { GroupNode } from './Nodes/GroupNode'
type Props = {
edges: Edge[]
groups: Group[]
answersCounts?: AnswersCount[]
onUnlockProPlanClick?: () => void
}
const GroupNodes = ({
edges,
groups,
answersCounts,
onUnlockProPlanClick,
}: Props) => {
return (
<>
<Edges
edges={edges}
answersCounts={answersCounts}
onUnlockProPlanClick={onUnlockProPlanClick}
/>
{groups.map((group, idx) => (
<GroupNode group={group} groupIndex={idx} key={group.id} />
))}
</>
)
}
export default memo(GroupNodes)

View File

@ -5,20 +5,24 @@ import {
IconButton, IconButton,
Stack, Stack,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import React, { useEffect, useRef, useState } from 'react' import React, { memo, useCallback, useEffect, useRef, useState } from 'react'
import { Group } from 'models' import { Group } from 'models'
import { useGraph } from 'contexts/GraphContext' import {
Coordinates,
useGraph,
useGroupsCoordinates,
} from 'contexts/GraphContext'
import { useBlockDnd } from 'contexts/GraphDndContext' import { useBlockDnd } from 'contexts/GraphDndContext'
import { BlockNodesList } from '../BlockNode/BlockNodesList' import { BlockNodesList } from '../BlockNode/BlockNodesList'
import { isDefined, isNotDefined } from 'utils' import { isDefined, 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 { GroupNodeContextMenu } from './GroupNodeContextMenu' import { GroupNodeContextMenu } from './GroupNodeContextMenu'
import { useDebounce } from 'use-debounce'
import { setMultipleRefs } from 'services/utils' import { setMultipleRefs } from 'services/utils'
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable' import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'
import { PlayIcon } from 'assets/icons' import { PlayIcon } from 'assets/icons'
import { RightPanel, useEditor } from 'contexts/EditorContext' import { RightPanel, useEditor } from 'contexts/EditorContext'
import { useDebounce } from 'use-debounce'
type Props = { type Props = {
group: Group group: Group
@ -26,168 +30,198 @@ type Props = {
} }
export const GroupNode = ({ group, groupIndex }: Props) => { export const GroupNode = ({ group, groupIndex }: Props) => {
const { const { updateGroupCoordinates } = useGroupsCoordinates()
connectingIds,
setConnectingIds,
previewingEdge,
groupsCoordinates,
updateGroupCoordinates,
isReadOnly,
focusedGroupId,
setFocusedGroupId,
graphPosition,
} = useGraph()
const { typebot, updateGroup } = useTypebot()
const { setMouseOverGroup, mouseOverGroup } = useBlockDnd()
const [isMouseDown, setIsMouseDown] = useState(false)
const [isConnecting, setIsConnecting] = useState(false)
const { setRightPanel, setStartPreviewAtGroup } = useEditor()
const isPreviewing =
previewingEdge?.from.groupId === group.id ||
(previewingEdge?.to.groupId === group.id &&
isNotDefined(previewingEdge.to.blockId))
const isStartGroup =
isDefined(group.blocks[0]) && group.blocks[0].type === 'start'
const groupCoordinates = groupsCoordinates[group.id] const handleGroupDrag = useCallback((newCoord: Coordinates) => {
const groupRef = useRef<HTMLDivElement | null>(null) updateGroupCoordinates(group.id, newCoord)
const [debouncedGroupPosition] = useDebounce(groupCoordinates, 100)
useEffect(() => {
if (!debouncedGroupPosition || isReadOnly) return
if (
debouncedGroupPosition?.x === group.graphCoordinates.x &&
debouncedGroupPosition.y === group.graphCoordinates.y
)
return
updateGroup(groupIndex, { graphCoordinates: debouncedGroupPosition })
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedGroupPosition]) }, [])
useEffect(() => {
setIsConnecting(
connectingIds?.target?.groupId === group.id &&
isNotDefined(connectingIds.target?.blockId)
)
}, [connectingIds, group.id])
const handleTitleSubmit = (title: string) =>
updateGroup(groupIndex, { title })
const handleMouseDown = (e: React.MouseEvent) => {
e.stopPropagation()
}
const handleMouseEnter = () => {
if (isReadOnly) return
if (mouseOverGroup?.id !== group.id && !isStartGroup)
setMouseOverGroup({ id: group.id, ref: groupRef })
if (connectingIds)
setConnectingIds({ ...connectingIds, target: { groupId: group.id } })
}
const handleMouseLeave = () => {
if (isReadOnly) return
setMouseOverGroup(undefined)
if (connectingIds) setConnectingIds({ ...connectingIds, target: undefined })
}
const onDrag = (_: DraggableEvent, draggableData: DraggableData) => {
const { deltaX, deltaY } = draggableData
updateGroupCoordinates(group.id, {
x: groupCoordinates.x + deltaX / graphPosition.scale,
y: groupCoordinates.y + deltaY / graphPosition.scale,
})
}
const onDragStart = () => {
setFocusedGroupId(group.id)
setIsMouseDown(true)
}
const startPreviewAtThisGroup = () => {
setStartPreviewAtGroup(group.id)
setRightPanel(RightPanel.PREVIEW)
}
const onDragStop = () => setIsMouseDown(false)
return ( return (
<ContextMenu<HTMLDivElement> <DraggableGroupNode
renderMenu={() => <GroupNodeContextMenu groupIndex={groupIndex} />} group={group}
isDisabled={isReadOnly || isStartGroup} groupIndex={groupIndex}
> onGroupDrag={handleGroupDrag}
{(ref, isOpened) => ( />
<DraggableCore
enableUserSelectHack={false}
onDrag={onDrag}
onStart={onDragStart}
onStop={onDragStop}
onMouseDown={(e) => e.stopPropagation()}
>
<Stack
ref={setMultipleRefs([ref, groupRef])}
data-testid="group"
p="4"
rounded="xl"
bgColor="#ffffff"
borderWidth="2px"
borderColor={
isConnecting || isOpened || isPreviewing ? 'blue.400' : '#ffffff'
}
w="300px"
transition="border 300ms, box-shadow 200ms"
pos="absolute"
style={{
transform: `translate(${groupCoordinates?.x ?? 0}px, ${
groupCoordinates?.y ?? 0
}px)`,
}}
onMouseDown={handleMouseDown}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
cursor={isMouseDown ? 'grabbing' : 'pointer'}
shadow="md"
_hover={{ shadow: 'lg' }}
zIndex={focusedGroupId === group.id ? 10 : 1}
>
<Editable
defaultValue={group.title}
onSubmit={handleTitleSubmit}
fontWeight="semibold"
pointerEvents={isReadOnly || isStartGroup ? 'none' : 'auto'}
>
<EditablePreview
_hover={{ bgColor: 'gray.200' }}
px="1"
userSelect={'none'}
/>
<EditableInput
minW="0"
px="1"
onMouseDown={(e) => e.stopPropagation()}
/>
</Editable>
{typebot && (
<BlockNodesList
groupId={group.id}
blocks={group.blocks}
groupIndex={groupIndex}
groupRef={ref}
isStartGroup={isStartGroup}
/>
)}
<IconButton
icon={<PlayIcon />}
aria-label={'Preview bot from this group'}
pos="absolute"
right={2}
top={0}
size="sm"
variant="outline"
onClick={startPreviewAtThisGroup}
/>
</Stack>
</DraggableCore>
)}
</ContextMenu>
) )
} }
const DraggableGroupNode = memo(
({
group,
groupIndex,
onGroupDrag,
}: Props & { onGroupDrag: (newCoord: Coordinates) => void }) => {
const {
connectingIds,
setConnectingIds,
previewingEdge,
isReadOnly,
focusedGroupId,
setFocusedGroupId,
graphPosition,
} = useGraph()
const [currentCoordinates, setCurrentCoordinates] = useState(
group.graphCoordinates
)
const { typebot, updateGroup } = useTypebot()
const { setMouseOverGroup, mouseOverGroup } = useBlockDnd()
const [isMouseDown, setIsMouseDown] = useState(false)
const [isConnecting, setIsConnecting] = useState(false)
const { setRightPanel, setStartPreviewAtGroup } = useEditor()
const isPreviewing =
previewingEdge?.from.groupId === group.id ||
(previewingEdge?.to.groupId === group.id &&
isNotDefined(previewingEdge.to.blockId))
const isStartGroup =
isDefined(group.blocks[0]) && group.blocks[0].type === 'start'
const groupRef = useRef<HTMLDivElement | null>(null)
const [debouncedGroupPosition] = useDebounce(currentCoordinates, 100)
useEffect(() => {
if (!currentCoordinates || isReadOnly) return
if (
currentCoordinates?.x === group.graphCoordinates.x &&
currentCoordinates.y === group.graphCoordinates.y
)
return
updateGroup(groupIndex, { graphCoordinates: currentCoordinates })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedGroupPosition])
useEffect(() => {
setIsConnecting(
connectingIds?.target?.groupId === group.id &&
isNotDefined(connectingIds.target?.blockId)
)
}, [connectingIds, group.id])
const handleTitleSubmit = (title: string) =>
updateGroup(groupIndex, { title })
const handleMouseDown = (e: React.MouseEvent) => {
e.stopPropagation()
}
const handleMouseEnter = () => {
if (isReadOnly) return
if (mouseOverGroup?.id !== group.id && !isStartGroup)
setMouseOverGroup({ id: group.id, ref: groupRef })
if (connectingIds)
setConnectingIds({ ...connectingIds, target: { groupId: group.id } })
}
const handleMouseLeave = () => {
if (isReadOnly) return
setMouseOverGroup(undefined)
if (connectingIds)
setConnectingIds({ ...connectingIds, target: undefined })
}
const onDrag = (_: DraggableEvent, draggableData: DraggableData) => {
const { deltaX, deltaY } = draggableData
const newCoord = {
x: currentCoordinates.x + deltaX / graphPosition.scale,
y: currentCoordinates.y + deltaY / graphPosition.scale,
}
setCurrentCoordinates(newCoord)
onGroupDrag(newCoord)
}
const onDragStart = () => {
setFocusedGroupId(group.id)
setIsMouseDown(true)
}
const startPreviewAtThisGroup = () => {
setStartPreviewAtGroup(group.id)
setRightPanel(RightPanel.PREVIEW)
}
const onDragStop = () => setIsMouseDown(false)
return (
<ContextMenu<HTMLDivElement>
renderMenu={() => <GroupNodeContextMenu groupIndex={groupIndex} />}
isDisabled={isReadOnly || isStartGroup}
>
{(ref, isOpened) => (
<DraggableCore
enableUserSelectHack={false}
onDrag={onDrag}
onStart={onDragStart}
onStop={onDragStop}
onMouseDown={(e) => e.stopPropagation()}
>
<Stack
ref={setMultipleRefs([ref, groupRef])}
data-testid="group"
p="4"
rounded="xl"
bgColor="#ffffff"
borderWidth="2px"
borderColor={
isConnecting || isOpened || isPreviewing
? 'blue.400'
: '#ffffff'
}
w="300px"
transition="border 300ms, box-shadow 200ms"
pos="absolute"
style={{
transform: `translate(${currentCoordinates?.x ?? 0}px, ${
currentCoordinates?.y ?? 0
}px)`,
}}
onMouseDown={handleMouseDown}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
cursor={isMouseDown ? 'grabbing' : 'pointer'}
shadow="md"
_hover={{ shadow: 'lg' }}
zIndex={focusedGroupId === group.id ? 10 : 1}
>
<Editable
defaultValue={group.title}
onSubmit={handleTitleSubmit}
fontWeight="semibold"
pointerEvents={isReadOnly || isStartGroup ? 'none' : 'auto'}
>
<EditablePreview
_hover={{ bgColor: 'gray.200' }}
px="1"
userSelect={'none'}
/>
<EditableInput
minW="0"
px="1"
onMouseDown={(e) => e.stopPropagation()}
/>
</Editable>
{typebot && (
<BlockNodesList
groupId={group.id}
blocks={group.blocks}
groupIndex={groupIndex}
groupRef={ref}
isStartGroup={isStartGroup}
/>
)}
<IconButton
icon={<PlayIcon />}
aria-label={'Preview bot from this group'}
pos="absolute"
right={2}
top={0}
size="sm"
variant="outline"
onClick={startPreviewAtThisGroup}
/>
</Stack>
</DraggableCore>
)}
</ContextMenu>
)
}
)

View File

@ -1,12 +1,13 @@
import { Stack, IconButton } from '@chakra-ui/react' import { Stack, IconButton } from '@chakra-ui/react'
import { PlusIcon, MinusIcon } from 'assets/icons' import { PlusIcon, MinusIcon } from 'assets/icons'
import { memo } from 'react'
import { headerHeight } from '../TypebotHeader' import { headerHeight } from '../TypebotHeader'
type Props = { type Props = {
onZoomIn: () => void onZoomIn: () => void
onZoomOut: () => void onZoomOut: () => void
} }
export const ZoomButtons = ({ onZoomIn, onZoomOut }: Props) => ( export const ZoomButtons = memo(({ onZoomIn, onZoomOut }: Props) => (
<Stack <Stack
pos="fixed" pos="fixed"
top={`calc(${headerHeight}px + 70px)`} top={`calc(${headerHeight}px + 70px)`}
@ -34,4 +35,4 @@ export const ZoomButtons = ({ onZoomIn, onZoomOut }: Props) => (
borderTopRadius={0} borderTopRadius={0}
/> />
</Stack> </Stack>
) ))

View File

@ -57,9 +57,14 @@ export type Endpoint = {
export type GroupsCoordinates = IdMap<Coordinates> export type GroupsCoordinates = IdMap<Coordinates>
const graphContext = createContext<{ const groupsCoordinatesContext = createContext<{
groupsCoordinates: GroupsCoordinates groupsCoordinates: GroupsCoordinates
updateGroupCoordinates: (groupId: string, newCoord: Coordinates) => void updateGroupCoordinates: (groupId: string, newCoord: Coordinates) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})
const graphContext = createContext<{
graphPosition: Position graphPosition: Position
setGraphPosition: Dispatch<SetStateAction<Position>> setGraphPosition: Dispatch<SetStateAction<Position>>
connectingIds: ConnectingIds | null connectingIds: ConnectingIds | null
@ -84,11 +89,9 @@ const graphContext = createContext<{
export const GraphProvider = ({ export const GraphProvider = ({
children, children,
groups,
isReadOnly = false, isReadOnly = false,
}: { }: {
children: ReactNode children: ReactNode
groups: Group[]
isReadOnly?: boolean isReadOnly?: boolean
}) => { }) => {
const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue) const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue)
@ -97,24 +100,8 @@ export const GraphProvider = ({
const [sourceEndpoints, setSourceEndpoints] = useState<IdMap<Endpoint>>({}) const [sourceEndpoints, setSourceEndpoints] = useState<IdMap<Endpoint>>({})
const [targetEndpoints, setTargetEndpoints] = useState<IdMap<Endpoint>>({}) const [targetEndpoints, setTargetEndpoints] = useState<IdMap<Endpoint>>({})
const [openedBlockId, setOpenedBlockId] = useState<string>() const [openedBlockId, setOpenedBlockId] = useState<string>()
const [groupsCoordinates, setGroupsCoordinates] = useState<GroupsCoordinates>(
{}
)
const [focusedGroupId, setFocusedGroupId] = useState<string>() const [focusedGroupId, setFocusedGroupId] = useState<string>()
useEffect(() => {
setGroupsCoordinates(
groups.reduce(
(coords, block) => ({
...coords,
[block.id]: block.graphCoordinates,
}),
{}
)
)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [groups])
const addSourceEndpoint = (endpoint: Endpoint) => { const addSourceEndpoint = (endpoint: Endpoint) => {
setSourceEndpoints((endpoints) => ({ setSourceEndpoints((endpoints) => ({
...endpoints, ...endpoints,
@ -129,12 +116,6 @@ export const GraphProvider = ({
})) }))
} }
const updateGroupCoordinates = (groupId: string, newCoord: Coordinates) =>
setGroupsCoordinates((groupsCoordinates) => ({
...groupsCoordinates,
[groupId]: newCoord,
}))
return ( return (
<graphContext.Provider <graphContext.Provider
value={{ value={{
@ -150,8 +131,6 @@ export const GraphProvider = ({
addTargetEndpoint, addTargetEndpoint,
openedBlockId, openedBlockId,
setOpenedBlockId, setOpenedBlockId,
groupsCoordinates,
updateGroupCoordinates,
isReadOnly, isReadOnly,
focusedGroupId, focusedGroupId,
setFocusedGroupId, setFocusedGroupId,
@ -163,3 +142,45 @@ export const GraphProvider = ({
} }
export const useGraph = () => useContext(graphContext) export const useGraph = () => useContext(graphContext)
export const GroupsCoordinatesProvider = ({
children,
groups,
}: {
children: ReactNode
groups: Group[]
isReadOnly?: boolean
}) => {
const [groupsCoordinates, setGroupsCoordinates] = useState<GroupsCoordinates>(
{}
)
useEffect(() => {
setGroupsCoordinates(
groups.reduce(
(coords, block) => ({
...coords,
[block.id]: block.graphCoordinates,
}),
{}
)
)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [groups])
const updateGroupCoordinates = (groupId: string, newCoord: Coordinates) =>
setGroupsCoordinates((groupsCoordinates) => ({
...groupsCoordinates,
[groupId]: newCoord,
}))
return (
<groupsCoordinatesContext.Provider
value={{ groupsCoordinates, updateGroupCoordinates }}
>
{children}
</groupsCoordinatesContext.Provider>
)
}
export const useGroupsCoordinates = () => useContext(groupsCoordinatesContext)

View File

@ -102,11 +102,6 @@ export const TypebotContext = ({
const { typebot, publishedTypebot, webhooks, isReadOnly, isLoading, mutate } = const { typebot, publishedTypebot, webhooks, isReadOnly, isLoading, mutate } =
useFetchedTypebot({ useFetchedTypebot({
typebotId, typebotId,
onError: (error) =>
showToast({
title: 'Error while fetching typebot',
description: error.message,
}),
}) })
const [ const [
@ -384,7 +379,7 @@ export const useFetchedTypebot = ({
onError, onError,
}: { }: {
typebotId: string typebotId: string
onError: (error: Error) => void onError?: (error: Error) => void
}) => { }) => {
const { data, error, mutate } = useSWR< const { data, error, mutate } = useSWR<
{ {
@ -397,7 +392,7 @@ export const useFetchedTypebot = ({
>(`/api/typebots/${typebotId}`, fetcher, { >(`/api/typebots/${typebotId}`, fetcher, {
dedupingInterval: env('E2E_TEST') === 'enabled' ? 0 : undefined, dedupingInterval: env('E2E_TEST') === 'enabled' ? 0 : undefined,
}) })
if (error) onError(error) if (error && onError) onError(error)
return { return {
typebot: data?.typebot, typebot: data?.typebot,
webhooks: data?.webhooks, webhooks: data?.webhooks,

View File

@ -1,9 +1,9 @@
import { Flex, useDisclosure } from '@chakra-ui/react' import { Flex, Spinner, useDisclosure } from '@chakra-ui/react'
import { StatsCards } from 'components/analytics/StatsCards' import { StatsCards } from 'components/analytics/StatsCards'
import { Graph } from 'components/shared/Graph' import { Graph } from 'components/shared/Graph'
import { useToast } from 'components/shared/hooks/useToast' import { useToast } from 'components/shared/hooks/useToast'
import { UpgradeModal } from 'components/shared/modals/UpgradeModal' import { UpgradeModal } from 'components/shared/modals/UpgradeModal'
import { GraphProvider } from 'contexts/GraphContext' import { GraphProvider, GroupsCoordinatesProvider } from 'contexts/GraphContext'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { Stats } from 'models' import { Stats } from 'models'
import React from 'react' import React from 'react'
@ -25,18 +25,29 @@ export const AnalyticsContent = ({ stats }: { stats?: Stats }) => {
h="full" h="full"
justifyContent="center" justifyContent="center"
> >
{publishedTypebot && answersCounts && stats && ( {publishedTypebot && answersCounts && stats ? (
<GraphProvider groups={publishedTypebot?.groups ?? []} isReadOnly> <GraphProvider isReadOnly>
<Graph <GroupsCoordinatesProvider groups={publishedTypebot?.groups}>
flex="1" <Graph
typebot={publishedTypebot} flex="1"
onUnlockProPlanClick={onOpen} typebot={publishedTypebot}
answersCounts={[ onUnlockProPlanClick={onOpen}
{ ...answersCounts[0], totalAnswers: stats?.totalStarts }, answersCounts={[
...answersCounts?.slice(1), { ...answersCounts[0], totalAnswers: stats?.totalStarts },
]} ...answersCounts?.slice(1),
/> ]}
/>
</GroupsCoordinatesProvider>
</GraphProvider> </GraphProvider>
) : (
<Flex
justify="center"
align="center"
boxSize="full"
bgColor="rgba(255, 255, 255, 0.5)"
>
<Spinner color="gray" />
</Flex>
)} )}
<UpgradeModal onClose={onClose} isOpen={isOpen} /> <UpgradeModal onClose={onClose} isOpen={isOpen} />
<StatsCards stats={stats} pos="absolute" top={10} /> <StatsCards stats={stats} pos="absolute" top={10} />

View File

@ -12,10 +12,11 @@ import { BoardMenuButton } from 'components/editor/BoardMenuButton'
import { PreviewDrawer } from 'components/editor/preview/PreviewDrawer' import { PreviewDrawer } from 'components/editor/preview/PreviewDrawer'
import { BlocksSideBar } from 'components/editor/BlocksSideBar' import { BlocksSideBar } from 'components/editor/BlocksSideBar'
import { Graph } from 'components/shared/Graph' import { Graph } from 'components/shared/Graph'
import { GraphProvider } from 'contexts/GraphContext' import { GraphProvider, GroupsCoordinatesProvider } from 'contexts/GraphContext'
import { GraphDndContext } from 'contexts/GraphDndContext' import { GraphDndContext } from 'contexts/GraphDndContext'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { GettingStartedModal } from 'components/editor/GettingStartedModal' import { GettingStartedModal } from 'components/editor/GettingStartedModal'
import { Spinner } from '@chakra-ui/react'
const TypebotEditPage = () => { const TypebotEditPage = () => {
const { typebot, isReadOnly } = useTypebot() const { typebot, isReadOnly } = useTypebot()
@ -36,17 +37,27 @@ const TypebotEditPage = () => {
backgroundSize="40px 40px" backgroundSize="40px 40px"
backgroundPosition="-19px -19px" backgroundPosition="-19px -19px"
> >
<GraphDndContext> {typebot ? (
<BlocksSideBar /> <GraphDndContext>
<GraphProvider <BlocksSideBar />
groups={typebot?.groups ?? []} <GraphProvider isReadOnly={isReadOnly}>
isReadOnly={isReadOnly} <GroupsCoordinatesProvider groups={typebot.groups}>
<Graph flex="1" typebot={typebot} />
<BoardMenuButton pos="absolute" right="40px" top="20px" />
<RightPanel />
</GroupsCoordinatesProvider>
</GraphProvider>
</GraphDndContext>
) : (
<Flex
justify="center"
align="center"
boxSize="full"
bgColor="rgba(255, 255, 255, 0.5)"
> >
{typebot && <Graph flex="1" typebot={typebot} />} <Spinner color="gray" />
<BoardMenuButton pos="absolute" right="40px" top="20px" /> </Flex>
<RightPanel /> )}
</GraphProvider>
</GraphDndContext>
</Flex> </Flex>
</Flex> </Flex>
</EditorContext> </EditorContext>

View File

@ -1,13 +1,15 @@
{ {
"id": "ckz8huhvo11297no1a7b4zf3ce", "id": "chat-theme-typebot",
"createdAt": "2022-02-04T14:19:29.412Z", "createdAt": "2022-02-04T14:19:29.412Z",
"updatedAt": "2022-02-04T14:19:29.412Z", "updatedAt": "2022-06-26T14:07:19.077Z",
"icon": null,
"name": "My typebot", "name": "My typebot",
"publishedTypebotId": null, "publishedTypebotId": null,
"folderId": null, "folderId": null,
"groups": [ "groups": [
{ {
"id": "teepNancm8TLj1qYhaTYAf", "id": "teepNancm8TLj1qYhaTYAf",
"title": "Start",
"blocks": [ "blocks": [
{ {
"id": "8fG3wDsExSSkq5ekUMzWVY", "id": "8fG3wDsExSSkq5ekUMzWVY",
@ -17,50 +19,113 @@
"outgoingEdgeId": "pj6fgTAjarwBq2jVgMgYoK" "outgoingEdgeId": "pj6fgTAjarwBq2jVgMgYoK"
} }
], ],
"title": "Start",
"graphCoordinates": { "x": 0, "y": 0 } "graphCoordinates": { "x": 0, "y": 0 }
}, },
{ {
"id": "6Dj1i7LeM3qXg5SKMhMyo1", "id": "6Dj1i7LeM3qXg5SKMhMyo1",
"graphCoordinates": { "x": 315, "y": 137 },
"title": "Group #1", "title": "Group #1",
"blocks": [ "blocks": [
{ {
"id": "swUB2pSmvcv3NC7ySzskRpL", "id": "swUB2pSmvcv3NC7ySzskRpL",
"groupId": "6Dj1i7LeM3qXg5SKMhMyo1",
"type": "text", "type": "text",
"content": { "content": {
"html": "<div>Ready?</div>", "html": "<div>Ready?</div>",
"richText": [{ "type": "p", "children": [{ "text": "Ready?" }] }], "richText": [{ "type": "p", "children": [{ "text": "Ready?" }] }],
"plainText": "Ready?" "plainText": "Ready?"
} },
"groupId": "6Dj1i7LeM3qXg5SKMhMyo1"
}, },
{ {
"id": "sc7ZYFtHVegJUA8c5K3gghi", "id": "sc7ZYFtHVegJUA8c5K3gghi",
"type": "choice input",
"items": [
{
"id": "nTjur4kxyL473XTbAb4Fak",
"type": 0,
"blockId": "sc7ZYFtHVegJUA8c5K3gghi",
"content": "Go"
}
],
"groupId": "6Dj1i7LeM3qXg5SKMhMyo1", "groupId": "6Dj1i7LeM3qXg5SKMhMyo1",
"options": { "buttonLabel": "Send", "isMultipleChoice": false },
"outgoingEdgeId": "uAsACqSmud99zmyCABWDwr"
}
],
"graphCoordinates": { "x": 315, "y": 137 }
},
{
"id": "2TR5xAQobKAg8hbArfh5br",
"title": "Group #2",
"blocks": [
{
"id": "cl4vdo1fz0008396nolxr0yln",
"type": "text",
"content": {
"html": "<div>Cool go</div>",
"richText": [{ "type": "p", "children": [{ "text": "Cool go" }] }],
"plainText": "Cool go"
},
"groupId": "2TR5xAQobKAg8hbArfh5br"
},
{
"id": "cl4vdu5yy00003f6lq63uhmmr",
"type": "choice input",
"items": [
{
"id": "cl4vdu5yy00013f6lkubfw1x8",
"type": 0,
"blockId": "cl4vdu5yy00003f6lq63uhmmr",
"content": "Go",
"outgoingEdgeId": "cl4vdu9qt00033f6lv4hws0fs"
}
],
"groupId": "2TR5xAQobKAg8hbArfh5br",
"options": { "buttonLabel": "Send", "isMultipleChoice": false }
}
],
"graphCoordinates": { "x": 760, "y": 299 }
},
{
"id": "cl4vdu8nn00023f6l6ptvimhw",
"graphCoordinates": { "x": 1150, "y": 381 },
"title": "Group #3",
"blocks": [
{
"id": "cl4vdxjig00043f6lhich3xm0",
"groupId": "cl4vdu8nn00023f6l6ptvimhw",
"type": "text",
"content": {
"html": "<div>Cool go</div>",
"richText": [{ "type": "p", "children": [{ "text": "Cool go" }] }],
"plainText": "Cool go"
}
},
{
"id": "cl4vdxn3i00053f6l4r3fftlp",
"groupId": "cl4vdu8nn00023f6l6ptvimhw",
"type": "choice input", "type": "choice input",
"options": { "buttonLabel": "Send", "isMultipleChoice": false }, "options": { "buttonLabel": "Send", "isMultipleChoice": false },
"items": [ "items": [
{ {
"id": "nTjur4kxyL473XTbAb4Fak", "id": "cl4vdxn3j00063f6l3nf92fvl",
"blockId": "sc7ZYFtHVegJUA8c5K3gghi", "blockId": "cl4vdxn3i00053f6l4r3fftlp",
"type": 0, "type": 0,
"content": "Go" "content": "Go",
"outgoingEdgeId": "cl4vdxs5j00093f6l4i489c3r"
} }
], ]
"outgoingEdgeId": "uAsACqSmud99zmyCABWDwr"
} }
] ]
}, },
{ {
"id": "2TR5xAQobKAg8hbArfh5br", "id": "cl4vdxpqq00083f6lrsj2y0dy",
"graphCoordinates": { "x": 760, "y": 299 }, "graphCoordinates": { "x": 1540, "y": 507 },
"title": "Group #2", "title": "Group #4",
"blocks": [ "blocks": [
{ {
"id": "s4xokHybra1jmZsWGVmza1K", "id": "s4xokHybra1jmZsWGVmza1K",
"groupId": "2TR5xAQobKAg8hbArfh5br",
"type": "text input", "type": "text input",
"groupId": "cl4vdxpqq00083f6lrsj2y0dy",
"options": { "options": {
"isLong": false, "isLong": false,
"labels": { "button": "Send", "placeholder": "Type your answer..." } "labels": { "button": "Send", "placeholder": "Type your answer..." }
@ -72,20 +137,38 @@
"variables": [], "variables": [],
"edges": [ "edges": [
{ {
"from": { "id": "pj6fgTAjarwBq2jVgMgYoK",
"groupId": "teepNancm8TLj1qYhaTYAf",
"blockId": "8fG3wDsExSSkq5ekUMzWVY"
},
"to": { "groupId": "6Dj1i7LeM3qXg5SKMhMyo1" }, "to": { "groupId": "6Dj1i7LeM3qXg5SKMhMyo1" },
"id": "pj6fgTAjarwBq2jVgMgYoK" "from": {
"blockId": "8fG3wDsExSSkq5ekUMzWVY",
"groupId": "teepNancm8TLj1qYhaTYAf"
}
},
{
"id": "uAsACqSmud99zmyCABWDwr",
"to": { "groupId": "2TR5xAQobKAg8hbArfh5br" },
"from": {
"blockId": "sc7ZYFtHVegJUA8c5K3gghi",
"groupId": "6Dj1i7LeM3qXg5SKMhMyo1"
}
}, },
{ {
"from": { "from": {
"groupId": "6Dj1i7LeM3qXg5SKMhMyo1", "groupId": "2TR5xAQobKAg8hbArfh5br",
"blockId": "sc7ZYFtHVegJUA8c5K3gghi" "blockId": "cl4vdu5yy00003f6lq63uhmmr",
"itemId": "cl4vdu5yy00013f6lkubfw1x8"
}, },
"to": { "groupId": "2TR5xAQobKAg8hbArfh5br" }, "to": { "groupId": "cl4vdu8nn00023f6l6ptvimhw" },
"id": "uAsACqSmud99zmyCABWDwr" "id": "cl4vdu9qt00033f6lv4hws0fs"
},
{
"from": {
"groupId": "cl4vdu8nn00023f6l6ptvimhw",
"blockId": "cl4vdxn3i00053f6l4r3fftlp",
"itemId": "cl4vdxn3j00063f6l3nf92fvl"
},
"to": { "groupId": "cl4vdxpqq00083f6lrsj2y0dy" },
"id": "cl4vdxs5j00093f6l4i489c3r"
} }
], ],
"theme": { "theme": {

View File

@ -54,6 +54,7 @@ test.describe.parallel('Buttons input block', () => {
await page.click('[data-testid="block1-icon"]') await page.click('[data-testid="block1-icon"]')
await page.locator('text=Item 1').hover() await page.locator('text=Item 1').hover()
await page.waitForTimeout(1000)
await page.click('[aria-label="Add item"]') 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')

View File

@ -69,11 +69,14 @@ test.describe.parallel('Theme page', () => {
'input[placeholder="Paste the image link..."]', 'input[placeholder="Paste the image link..."]',
hostAvatarUrl hostAvatarUrl
) )
await typebotViewer(page).locator('button >> text="Go"').click()
await expect(typebotViewer(page).locator('img')).toHaveAttribute( await expect(typebotViewer(page).locator('img')).toHaveAttribute(
'src', 'src',
hostAvatarUrl hostAvatarUrl
) )
await page.click('text=Bot avatar') await page.click('text=Bot avatar')
await expect(typebotViewer(page).locator('img')).toBeHidden() await expect(typebotViewer(page).locator('img')).toBeHidden()
// Host bubbles // Host bubbles
@ -86,7 +89,7 @@ test.describe.parallel('Theme page', () => {
) )
await page.fill('input[value="#303235"]', '#ffffff') 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"] >> nth=-1'
) )
await expect(hostBubble).toHaveCSS( await expect(hostBubble).toHaveCSS(
'background-color', 'background-color',
@ -116,9 +119,9 @@ test.describe.parallel('Theme page', () => {
'[data-testid="guest-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=1' '[data-testid="guest-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=1'
) )
await page.fill('input[value="#FFFFFF"]', '#264653') await page.fill('input[value="#FFFFFF"]', '#264653')
await typebotViewer(page).locator('text=Go').click() await typebotViewer(page).locator('button >> text="Go"').click()
const guestBubble = typebotViewer(page).locator( const guestBubble = typebotViewer(page).locator(
'[data-testid="guest-bubble"]' '[data-testid="guest-bubble"] >> nth=-1'
) )
await expect(guestBubble).toHaveCSS( await expect(guestBubble).toHaveCSS(
'background-color', 'background-color',
@ -129,7 +132,7 @@ test.describe.parallel('Theme page', () => {
// Guest avatar // Guest avatar
await page.click('text=User avatar') await page.click('text=User avatar')
await expect( await expect(
typebotViewer(page).locator('[data-testid="default-avatar"]') typebotViewer(page).locator('[data-testid="default-avatar"] >> nth=-1')
).toBeVisible() ).toBeVisible()
await page.click('[data-testid="default-avatar"]') await page.click('[data-testid="default-avatar"]')
await page.click('button:has-text("Embed link")') await page.click('button:has-text("Embed link")')
@ -137,6 +140,8 @@ test.describe.parallel('Theme page', () => {
'input[placeholder="Paste the image link..."]', 'input[placeholder="Paste the image link..."]',
guestAvatarUrl guestAvatarUrl
) )
typebotViewer(page).locator('button >> text="Go"').click()
await expect(typebotViewer(page).locator('img')).toHaveAttribute( await expect(typebotViewer(page).locator('img')).toHaveAttribute(
'src', 'src',
guestAvatarUrl guestAvatarUrl
@ -151,7 +156,6 @@ test.describe.parallel('Theme page', () => {
'[data-testid="inputs-theme"] >> [aria-label="Pick a color"] >> nth=1' '[data-testid="inputs-theme"] >> [aria-label="Pick a color"] >> nth=1'
) )
await page.fill('input[value="#303235"]', '#023e8a') await page.fill('input[value="#303235"]', '#023e8a')
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)')
await expect(input).toHaveCSS('color', 'rgb(2, 62, 138)') await expect(input).toHaveCSS('color', 'rgb(2, 62, 138)')

View File

@ -17,7 +17,7 @@ test('should work as expected', async ({ page }) => {
await typebotViewer(page).locator('input').press('Enter') await typebotViewer(page).locator('input').press('Enter')
await typebotViewer(page).locator('button >> text=Yes').click() await typebotViewer(page).locator('button >> text=Yes').click()
await page.goto(`http://localhost:3000/typebots/${typebotId}/results`) await page.goto(`http://localhost:3000/typebots/${typebotId}/results`)
await expect(page.locator('text=Baptiste')).toBeVisible() await expect(page.locator('text="Baptiste"')).toBeVisible()
await expect(page.locator('text=26')).toBeVisible() await expect(page.locator('text="26"')).toBeVisible()
await expect(page.locator('text=Yes')).toBeVisible() await expect(page.locator('text="Yes"')).toBeVisible()
}) })