feat(editor): ⚡️ Optimize graph navigation
This commit is contained in:
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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 })
|
||||||
|
@ -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>) => {
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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)
|
|
33
apps/builder/components/shared/Graph/GraphElements.tsx
Normal file
33
apps/builder/components/shared/Graph/GraphElements.tsx
Normal 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)
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ -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>
|
||||||
)
|
))
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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} />
|
||||||
|
@ -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>
|
||||||
|
@ -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": {
|
||||||
|
@ -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')
|
||||||
|
@ -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)')
|
||||||
|
@ -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()
|
||||||
})
|
})
|
||||||
|
Reference in New Issue
Block a user