feat(editor): ⚡️ Optimize graph navigation
This commit is contained in:
@ -1,6 +1,11 @@
|
||||
import { useEventListener } from '@chakra-ui/hooks'
|
||||
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 { colors } from 'libs/theme'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
@ -17,10 +22,10 @@ export const DrawingEdge = () => {
|
||||
connectingIds,
|
||||
sourceEndpoints,
|
||||
targetEndpoints,
|
||||
groupsCoordinates,
|
||||
} = useGraph()
|
||||
const { groupsCoordinates } = useGroupsCoordinates()
|
||||
const { createEdge } = useTypebot()
|
||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
|
||||
const [mousePosition, setMousePosition] = useState<Coordinates | null>(null)
|
||||
|
||||
const sourceGroupCoordinates =
|
||||
groupsCoordinates && groupsCoordinates[connectingIds?.source.groupId ?? '']
|
||||
@ -50,7 +55,7 @@ export const DrawingEdge = () => {
|
||||
}, [connectingIds, targetEndpoints])
|
||||
|
||||
const path = useMemo(() => {
|
||||
if (!sourceTop || !sourceGroupCoordinates) return ``
|
||||
if (!sourceTop || !sourceGroupCoordinates || !mousePosition) return ``
|
||||
|
||||
return targetGroupCoordinates
|
||||
? computeConnectingEdgePath({
|
||||
@ -75,6 +80,10 @@ export const DrawingEdge = () => {
|
||||
])
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!connectingIds) {
|
||||
if (mousePosition) setMousePosition(null)
|
||||
return
|
||||
}
|
||||
const coordinates = {
|
||||
x: (e.clientX - graphPosition.x) / graphPosition.scale,
|
||||
y: (e.clientY - graphPosition.y) / graphPosition.scale,
|
||||
@ -92,7 +101,10 @@ export const DrawingEdge = () => {
|
||||
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 (
|
||||
<path
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { useWorkspace } from 'contexts/WorkspaceContext'
|
||||
import React, { useMemo } from 'react'
|
||||
@ -24,7 +24,8 @@ export const DropOffEdge = ({
|
||||
onUnlockProPlanClick,
|
||||
}: Props) => {
|
||||
const { workspace } = useWorkspace()
|
||||
const { sourceEndpoints, groupsCoordinates, graphPosition } = useGraph()
|
||||
const { groupsCoordinates } = useGroupsCoordinates()
|
||||
const { sourceEndpoints, graphPosition } = useGraph()
|
||||
const { publishedTypebot } = useTypebot()
|
||||
|
||||
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 {
|
||||
getAnchorsPosition,
|
||||
@ -19,17 +23,20 @@ export type AnchorsPositionProps = {
|
||||
totalSegments: number
|
||||
}
|
||||
|
||||
export const Edge = ({ edge }: { edge: EdgeProps }) => {
|
||||
type Props = {
|
||||
edge: EdgeProps
|
||||
}
|
||||
export const Edge = ({ edge }: Props) => {
|
||||
const { deleteEdge } = useTypebot()
|
||||
const {
|
||||
previewingEdge,
|
||||
sourceEndpoints,
|
||||
targetEndpoints,
|
||||
groupsCoordinates,
|
||||
graphPosition,
|
||||
isReadOnly,
|
||||
setPreviewingEdge,
|
||||
} = useGraph()
|
||||
const { groupsCoordinates } = useGroupsCoordinates()
|
||||
const [isMouseOver, setIsMouseOver] = useState(false)
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const [edgeMenuPosition, setEdgeMenuPosition] = useState({ x: 0, y: 0 })
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { BoxProps, Flex } from '@chakra-ui/react'
|
||||
import { useGraph } from 'contexts/GraphContext'
|
||||
import { useGraph, useGroupsCoordinates } from 'contexts/GraphContext'
|
||||
import { Source } from 'models'
|
||||
import React, { MouseEvent, useEffect, useRef, useState } from 'react'
|
||||
|
||||
@ -10,12 +10,9 @@ export const SourceEndpoint = ({
|
||||
source: Source
|
||||
}) => {
|
||||
const [ranOnce, setRanOnce] = useState(false)
|
||||
const {
|
||||
setConnectingIds,
|
||||
addSourceEndpoint,
|
||||
groupsCoordinates,
|
||||
previewingEdge,
|
||||
} = useGraph()
|
||||
const { setConnectingIds, addSourceEndpoint, previewingEdge } = useGraph()
|
||||
|
||||
const { groupsCoordinates } = useGroupsCoordinates()
|
||||
const ref = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const handleMouseDown = (e: MouseEvent<HTMLDivElement>) => {
|
||||
|
@ -1,10 +1,11 @@
|
||||
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 {
|
||||
blockWidth,
|
||||
Coordinates,
|
||||
graphPositionDefaultValue,
|
||||
useGraph,
|
||||
useGroupsCoordinates,
|
||||
} from 'contexts/GraphContext'
|
||||
import { useBlockDnd } from 'contexts/GraphDndContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
@ -12,7 +13,7 @@ import { DraggableBlockType, PublicTypebot, Typebot } from 'models'
|
||||
import { AnswersCount } from 'services/analytics'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'
|
||||
import GraphContent from './GraphContent'
|
||||
import GraphElements from './GraphElements'
|
||||
import cuid from 'cuid'
|
||||
import { headerHeight } from '../TypebotHeader'
|
||||
import { useUser } from 'contexts/UserContext'
|
||||
@ -29,7 +30,7 @@ export const Graph = ({
|
||||
onUnlockProPlanClick,
|
||||
...props
|
||||
}: {
|
||||
typebot?: Typebot | PublicTypebot
|
||||
typebot: Typebot | PublicTypebot
|
||||
answersCounts?: AnswersCount[]
|
||||
onUnlockProPlanClick?: () => void
|
||||
} & FlexProps) => {
|
||||
@ -47,10 +48,10 @@ export const Graph = ({
|
||||
const {
|
||||
setGraphPosition: setGlobalGraphPosition,
|
||||
setOpenedBlockId,
|
||||
updateGroupCoordinates,
|
||||
setPreviewingEdge,
|
||||
connectingIds,
|
||||
} = useGraph()
|
||||
const { updateGroupCoordinates } = useGroupsCoordinates()
|
||||
const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue)
|
||||
const [debouncedGraphPosition] = useDebounce(graphPosition, 200)
|
||||
const transform = useMemo(
|
||||
@ -177,13 +178,16 @@ export const Graph = ({
|
||||
useEventListener('mouseup', handleMouseUp, graphContainerRef.current)
|
||||
useEventListener('click', handleClick, editorContainerRef.current)
|
||||
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 (
|
||||
<DraggableCore onDrag={onDrag} enableUserSelectHack={false}>
|
||||
<Flex ref={graphContainerRef} position="relative" {...props}>
|
||||
<ZoomButtons
|
||||
onZoomIn={() => zoom(zoomButtonsScaleBlock)}
|
||||
onZoomOut={() => zoom(-zoomButtonsScaleBlock)}
|
||||
/>
|
||||
<ZoomButtons onZoomIn={zoomIn} onZoomOut={zoomOut} />
|
||||
<Flex
|
||||
flex="1"
|
||||
w="full"
|
||||
@ -195,7 +199,9 @@ export const Graph = ({
|
||||
willChange="transform"
|
||||
transformOrigin="0px 0px 0px"
|
||||
>
|
||||
<GraphContent
|
||||
<GraphElements
|
||||
edges={typebot.edges}
|
||||
groups={typebot.groups}
|
||||
answersCounts={answersCounts}
|
||||
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,
|
||||
Stack,
|
||||
} 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 { useGraph } from 'contexts/GraphContext'
|
||||
import {
|
||||
Coordinates,
|
||||
useGraph,
|
||||
useGroupsCoordinates,
|
||||
} from 'contexts/GraphContext'
|
||||
import { useBlockDnd } from 'contexts/GraphDndContext'
|
||||
import { BlockNodesList } from '../BlockNode/BlockNodesList'
|
||||
import { isDefined, isNotDefined } from 'utils'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { ContextMenu } from 'components/shared/ContextMenu'
|
||||
import { GroupNodeContextMenu } from './GroupNodeContextMenu'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { setMultipleRefs } from 'services/utils'
|
||||
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'
|
||||
import { PlayIcon } from 'assets/icons'
|
||||
import { RightPanel, useEditor } from 'contexts/EditorContext'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
|
||||
type Props = {
|
||||
group: Group
|
||||
@ -26,17 +30,42 @@ type Props = {
|
||||
}
|
||||
|
||||
export const GroupNode = ({ group, groupIndex }: Props) => {
|
||||
const { updateGroupCoordinates } = useGroupsCoordinates()
|
||||
|
||||
const handleGroupDrag = useCallback((newCoord: Coordinates) => {
|
||||
updateGroupCoordinates(group.id, newCoord)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<DraggableGroupNode
|
||||
group={group}
|
||||
groupIndex={groupIndex}
|
||||
onGroupDrag={handleGroupDrag}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DraggableGroupNode = memo(
|
||||
({
|
||||
group,
|
||||
groupIndex,
|
||||
onGroupDrag,
|
||||
}: Props & { onGroupDrag: (newCoord: Coordinates) => void }) => {
|
||||
const {
|
||||
connectingIds,
|
||||
setConnectingIds,
|
||||
previewingEdge,
|
||||
groupsCoordinates,
|
||||
updateGroupCoordinates,
|
||||
isReadOnly,
|
||||
focusedGroupId,
|
||||
setFocusedGroupId,
|
||||
graphPosition,
|
||||
} = useGraph()
|
||||
|
||||
const [currentCoordinates, setCurrentCoordinates] = useState(
|
||||
group.graphCoordinates
|
||||
)
|
||||
|
||||
const { typebot, updateGroup } = useTypebot()
|
||||
const { setMouseOverGroup, mouseOverGroup } = useBlockDnd()
|
||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||
@ -49,17 +78,16 @@ export const GroupNode = ({ group, groupIndex }: Props) => {
|
||||
const isStartGroup =
|
||||
isDefined(group.blocks[0]) && group.blocks[0].type === 'start'
|
||||
|
||||
const groupCoordinates = groupsCoordinates[group.id]
|
||||
const groupRef = useRef<HTMLDivElement | null>(null)
|
||||
const [debouncedGroupPosition] = useDebounce(groupCoordinates, 100)
|
||||
const [debouncedGroupPosition] = useDebounce(currentCoordinates, 100)
|
||||
useEffect(() => {
|
||||
if (!debouncedGroupPosition || isReadOnly) return
|
||||
if (!currentCoordinates || isReadOnly) return
|
||||
if (
|
||||
debouncedGroupPosition?.x === group.graphCoordinates.x &&
|
||||
debouncedGroupPosition.y === group.graphCoordinates.y
|
||||
currentCoordinates?.x === group.graphCoordinates.x &&
|
||||
currentCoordinates.y === group.graphCoordinates.y
|
||||
)
|
||||
return
|
||||
updateGroup(groupIndex, { graphCoordinates: debouncedGroupPosition })
|
||||
updateGroup(groupIndex, { graphCoordinates: currentCoordinates })
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedGroupPosition])
|
||||
|
||||
@ -88,15 +116,18 @@ export const GroupNode = ({ group, groupIndex }: Props) => {
|
||||
const handleMouseLeave = () => {
|
||||
if (isReadOnly) return
|
||||
setMouseOverGroup(undefined)
|
||||
if (connectingIds) setConnectingIds({ ...connectingIds, target: 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 newCoord = {
|
||||
x: currentCoordinates.x + deltaX / graphPosition.scale,
|
||||
y: currentCoordinates.y + deltaY / graphPosition.scale,
|
||||
}
|
||||
setCurrentCoordinates(newCoord)
|
||||
onGroupDrag(newCoord)
|
||||
}
|
||||
|
||||
const onDragStart = () => {
|
||||
@ -131,14 +162,16 @@ export const GroupNode = ({ group, groupIndex }: Props) => {
|
||||
bgColor="#ffffff"
|
||||
borderWidth="2px"
|
||||
borderColor={
|
||||
isConnecting || isOpened || isPreviewing ? 'blue.400' : '#ffffff'
|
||||
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
|
||||
transform: `translate(${currentCoordinates?.x ?? 0}px, ${
|
||||
currentCoordinates?.y ?? 0
|
||||
}px)`,
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
@ -191,3 +224,4 @@ export const GroupNode = ({ group, groupIndex }: Props) => {
|
||||
</ContextMenu>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { Stack, IconButton } from '@chakra-ui/react'
|
||||
import { PlusIcon, MinusIcon } from 'assets/icons'
|
||||
import { memo } from 'react'
|
||||
import { headerHeight } from '../TypebotHeader'
|
||||
|
||||
type Props = {
|
||||
onZoomIn: () => void
|
||||
onZoomOut: () => void
|
||||
}
|
||||
export const ZoomButtons = ({ onZoomIn, onZoomOut }: Props) => (
|
||||
export const ZoomButtons = memo(({ onZoomIn, onZoomOut }: Props) => (
|
||||
<Stack
|
||||
pos="fixed"
|
||||
top={`calc(${headerHeight}px + 70px)`}
|
||||
@ -34,4 +35,4 @@ export const ZoomButtons = ({ onZoomIn, onZoomOut }: Props) => (
|
||||
borderTopRadius={0}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
))
|
||||
|
@ -57,9 +57,14 @@ export type Endpoint = {
|
||||
|
||||
export type GroupsCoordinates = IdMap<Coordinates>
|
||||
|
||||
const graphContext = createContext<{
|
||||
const groupsCoordinatesContext = createContext<{
|
||||
groupsCoordinates: GroupsCoordinates
|
||||
updateGroupCoordinates: (groupId: string, newCoord: Coordinates) => void
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
}>({})
|
||||
|
||||
const graphContext = createContext<{
|
||||
graphPosition: Position
|
||||
setGraphPosition: Dispatch<SetStateAction<Position>>
|
||||
connectingIds: ConnectingIds | null
|
||||
@ -84,11 +89,9 @@ const graphContext = createContext<{
|
||||
|
||||
export const GraphProvider = ({
|
||||
children,
|
||||
groups,
|
||||
isReadOnly = false,
|
||||
}: {
|
||||
children: ReactNode
|
||||
groups: Group[]
|
||||
isReadOnly?: boolean
|
||||
}) => {
|
||||
const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue)
|
||||
@ -97,24 +100,8 @@ export const GraphProvider = ({
|
||||
const [sourceEndpoints, setSourceEndpoints] = useState<IdMap<Endpoint>>({})
|
||||
const [targetEndpoints, setTargetEndpoints] = useState<IdMap<Endpoint>>({})
|
||||
const [openedBlockId, setOpenedBlockId] = useState<string>()
|
||||
const [groupsCoordinates, setGroupsCoordinates] = useState<GroupsCoordinates>(
|
||||
{}
|
||||
)
|
||||
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) => {
|
||||
setSourceEndpoints((endpoints) => ({
|
||||
...endpoints,
|
||||
@ -129,12 +116,6 @@ export const GraphProvider = ({
|
||||
}))
|
||||
}
|
||||
|
||||
const updateGroupCoordinates = (groupId: string, newCoord: Coordinates) =>
|
||||
setGroupsCoordinates((groupsCoordinates) => ({
|
||||
...groupsCoordinates,
|
||||
[groupId]: newCoord,
|
||||
}))
|
||||
|
||||
return (
|
||||
<graphContext.Provider
|
||||
value={{
|
||||
@ -150,8 +131,6 @@ export const GraphProvider = ({
|
||||
addTargetEndpoint,
|
||||
openedBlockId,
|
||||
setOpenedBlockId,
|
||||
groupsCoordinates,
|
||||
updateGroupCoordinates,
|
||||
isReadOnly,
|
||||
focusedGroupId,
|
||||
setFocusedGroupId,
|
||||
@ -163,3 +142,45 @@ export const GraphProvider = ({
|
||||
}
|
||||
|
||||
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 } =
|
||||
useFetchedTypebot({
|
||||
typebotId,
|
||||
onError: (error) =>
|
||||
showToast({
|
||||
title: 'Error while fetching typebot',
|
||||
description: error.message,
|
||||
}),
|
||||
})
|
||||
|
||||
const [
|
||||
@ -384,7 +379,7 @@ export const useFetchedTypebot = ({
|
||||
onError,
|
||||
}: {
|
||||
typebotId: string
|
||||
onError: (error: Error) => void
|
||||
onError?: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<
|
||||
{
|
||||
@ -397,7 +392,7 @@ export const useFetchedTypebot = ({
|
||||
>(`/api/typebots/${typebotId}`, fetcher, {
|
||||
dedupingInterval: env('E2E_TEST') === 'enabled' ? 0 : undefined,
|
||||
})
|
||||
if (error) onError(error)
|
||||
if (error && onError) onError(error)
|
||||
return {
|
||||
typebot: data?.typebot,
|
||||
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 { Graph } from 'components/shared/Graph'
|
||||
import { useToast } from 'components/shared/hooks/useToast'
|
||||
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 { Stats } from 'models'
|
||||
import React from 'react'
|
||||
@ -25,8 +25,9 @@ export const AnalyticsContent = ({ stats }: { stats?: Stats }) => {
|
||||
h="full"
|
||||
justifyContent="center"
|
||||
>
|
||||
{publishedTypebot && answersCounts && stats && (
|
||||
<GraphProvider groups={publishedTypebot?.groups ?? []} isReadOnly>
|
||||
{publishedTypebot && answersCounts && stats ? (
|
||||
<GraphProvider isReadOnly>
|
||||
<GroupsCoordinatesProvider groups={publishedTypebot?.groups}>
|
||||
<Graph
|
||||
flex="1"
|
||||
typebot={publishedTypebot}
|
||||
@ -36,7 +37,17 @@ export const AnalyticsContent = ({ stats }: { stats?: Stats }) => {
|
||||
...answersCounts?.slice(1),
|
||||
]}
|
||||
/>
|
||||
</GroupsCoordinatesProvider>
|
||||
</GraphProvider>
|
||||
) : (
|
||||
<Flex
|
||||
justify="center"
|
||||
align="center"
|
||||
boxSize="full"
|
||||
bgColor="rgba(255, 255, 255, 0.5)"
|
||||
>
|
||||
<Spinner color="gray" />
|
||||
</Flex>
|
||||
)}
|
||||
<UpgradeModal onClose={onClose} isOpen={isOpen} />
|
||||
<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 { BlocksSideBar } from 'components/editor/BlocksSideBar'
|
||||
import { Graph } from 'components/shared/Graph'
|
||||
import { GraphProvider } from 'contexts/GraphContext'
|
||||
import { GraphProvider, GroupsCoordinatesProvider } from 'contexts/GraphContext'
|
||||
import { GraphDndContext } from 'contexts/GraphDndContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { GettingStartedModal } from 'components/editor/GettingStartedModal'
|
||||
import { Spinner } from '@chakra-ui/react'
|
||||
|
||||
const TypebotEditPage = () => {
|
||||
const { typebot, isReadOnly } = useTypebot()
|
||||
@ -36,17 +37,27 @@ const TypebotEditPage = () => {
|
||||
backgroundSize="40px 40px"
|
||||
backgroundPosition="-19px -19px"
|
||||
>
|
||||
{typebot ? (
|
||||
<GraphDndContext>
|
||||
<BlocksSideBar />
|
||||
<GraphProvider
|
||||
groups={typebot?.groups ?? []}
|
||||
isReadOnly={isReadOnly}
|
||||
>
|
||||
{typebot && <Graph flex="1" typebot={typebot} />}
|
||||
<GraphProvider 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)"
|
||||
>
|
||||
<Spinner color="gray" />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</EditorContext>
|
||||
|
@ -1,13 +1,15 @@
|
||||
{
|
||||
"id": "ckz8huhvo11297no1a7b4zf3ce",
|
||||
"id": "chat-theme-typebot",
|
||||
"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",
|
||||
"publishedTypebotId": null,
|
||||
"folderId": null,
|
||||
"groups": [
|
||||
{
|
||||
"id": "teepNancm8TLj1qYhaTYAf",
|
||||
"title": "Start",
|
||||
"blocks": [
|
||||
{
|
||||
"id": "8fG3wDsExSSkq5ekUMzWVY",
|
||||
@ -17,50 +19,113 @@
|
||||
"outgoingEdgeId": "pj6fgTAjarwBq2jVgMgYoK"
|
||||
}
|
||||
],
|
||||
"title": "Start",
|
||||
"graphCoordinates": { "x": 0, "y": 0 }
|
||||
},
|
||||
{
|
||||
"id": "6Dj1i7LeM3qXg5SKMhMyo1",
|
||||
"graphCoordinates": { "x": 315, "y": 137 },
|
||||
"title": "Group #1",
|
||||
"blocks": [
|
||||
{
|
||||
"id": "swUB2pSmvcv3NC7ySzskRpL",
|
||||
"groupId": "6Dj1i7LeM3qXg5SKMhMyo1",
|
||||
"type": "text",
|
||||
"content": {
|
||||
"html": "<div>Ready?</div>",
|
||||
"richText": [{ "type": "p", "children": [{ "text": "Ready?" }] }],
|
||||
"plainText": "Ready?"
|
||||
}
|
||||
},
|
||||
"groupId": "6Dj1i7LeM3qXg5SKMhMyo1"
|
||||
},
|
||||
{
|
||||
"id": "sc7ZYFtHVegJUA8c5K3gghi",
|
||||
"type": "choice input",
|
||||
"items": [
|
||||
{
|
||||
"id": "nTjur4kxyL473XTbAb4Fak",
|
||||
"type": 0,
|
||||
"blockId": "sc7ZYFtHVegJUA8c5K3gghi",
|
||||
"content": "Go"
|
||||
}
|
||||
],
|
||||
"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",
|
||||
"options": { "buttonLabel": "Send", "isMultipleChoice": false },
|
||||
"items": [
|
||||
{
|
||||
"id": "nTjur4kxyL473XTbAb4Fak",
|
||||
"blockId": "sc7ZYFtHVegJUA8c5K3gghi",
|
||||
"id": "cl4vdxn3j00063f6l3nf92fvl",
|
||||
"blockId": "cl4vdxn3i00053f6l4r3fftlp",
|
||||
"type": 0,
|
||||
"content": "Go"
|
||||
"content": "Go",
|
||||
"outgoingEdgeId": "cl4vdxs5j00093f6l4i489c3r"
|
||||
}
|
||||
],
|
||||
"outgoingEdgeId": "uAsACqSmud99zmyCABWDwr"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "2TR5xAQobKAg8hbArfh5br",
|
||||
"graphCoordinates": { "x": 760, "y": 299 },
|
||||
"title": "Group #2",
|
||||
"id": "cl4vdxpqq00083f6lrsj2y0dy",
|
||||
"graphCoordinates": { "x": 1540, "y": 507 },
|
||||
"title": "Group #4",
|
||||
"blocks": [
|
||||
{
|
||||
"id": "s4xokHybra1jmZsWGVmza1K",
|
||||
"groupId": "2TR5xAQobKAg8hbArfh5br",
|
||||
"type": "text input",
|
||||
"groupId": "cl4vdxpqq00083f6lrsj2y0dy",
|
||||
"options": {
|
||||
"isLong": false,
|
||||
"labels": { "button": "Send", "placeholder": "Type your answer..." }
|
||||
@ -72,20 +137,38 @@
|
||||
"variables": [],
|
||||
"edges": [
|
||||
{
|
||||
"from": {
|
||||
"groupId": "teepNancm8TLj1qYhaTYAf",
|
||||
"blockId": "8fG3wDsExSSkq5ekUMzWVY"
|
||||
},
|
||||
"id": "pj6fgTAjarwBq2jVgMgYoK",
|
||||
"to": { "groupId": "6Dj1i7LeM3qXg5SKMhMyo1" },
|
||||
"id": "pj6fgTAjarwBq2jVgMgYoK"
|
||||
"from": {
|
||||
"blockId": "8fG3wDsExSSkq5ekUMzWVY",
|
||||
"groupId": "teepNancm8TLj1qYhaTYAf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "uAsACqSmud99zmyCABWDwr",
|
||||
"to": { "groupId": "2TR5xAQobKAg8hbArfh5br" },
|
||||
"from": {
|
||||
"blockId": "sc7ZYFtHVegJUA8c5K3gghi",
|
||||
"groupId": "6Dj1i7LeM3qXg5SKMhMyo1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"from": {
|
||||
"groupId": "6Dj1i7LeM3qXg5SKMhMyo1",
|
||||
"blockId": "sc7ZYFtHVegJUA8c5K3gghi"
|
||||
"groupId": "2TR5xAQobKAg8hbArfh5br",
|
||||
"blockId": "cl4vdu5yy00003f6lq63uhmmr",
|
||||
"itemId": "cl4vdu5yy00013f6lkubfw1x8"
|
||||
},
|
||||
"to": { "groupId": "2TR5xAQobKAg8hbArfh5br" },
|
||||
"id": "uAsACqSmud99zmyCABWDwr"
|
||||
"to": { "groupId": "cl4vdu8nn00023f6l6ptvimhw" },
|
||||
"id": "cl4vdu9qt00033f6lv4hws0fs"
|
||||
},
|
||||
{
|
||||
"from": {
|
||||
"groupId": "cl4vdu8nn00023f6l6ptvimhw",
|
||||
"blockId": "cl4vdxn3i00053f6l4r3fftlp",
|
||||
"itemId": "cl4vdxn3j00063f6l3nf92fvl"
|
||||
},
|
||||
"to": { "groupId": "cl4vdxpqq00083f6lrsj2y0dy" },
|
||||
"id": "cl4vdxs5j00093f6l4i489c3r"
|
||||
}
|
||||
],
|
||||
"theme": {
|
||||
|
@ -54,6 +54,7 @@ test.describe.parallel('Buttons input block', () => {
|
||||
await page.click('[data-testid="block1-icon"]')
|
||||
|
||||
await page.locator('text=Item 1').hover()
|
||||
await page.waitForTimeout(1000)
|
||||
await page.click('[aria-label="Add item"]')
|
||||
await page.fill('input[value="Click to edit"]', 'Item 2')
|
||||
await page.press('input[value="Item 2"]', 'Enter')
|
||||
|
@ -69,11 +69,14 @@ test.describe.parallel('Theme page', () => {
|
||||
'input[placeholder="Paste the image link..."]',
|
||||
hostAvatarUrl
|
||||
)
|
||||
await typebotViewer(page).locator('button >> text="Go"').click()
|
||||
|
||||
await expect(typebotViewer(page).locator('img')).toHaveAttribute(
|
||||
'src',
|
||||
hostAvatarUrl
|
||||
)
|
||||
await page.click('text=Bot avatar')
|
||||
|
||||
await expect(typebotViewer(page).locator('img')).toBeHidden()
|
||||
|
||||
// Host bubbles
|
||||
@ -86,7 +89,7 @@ test.describe.parallel('Theme page', () => {
|
||||
)
|
||||
await page.fill('input[value="#303235"]', '#ffffff')
|
||||
const hostBubble = typebotViewer(page).locator(
|
||||
'[data-testid="host-bubble"]'
|
||||
'[data-testid="host-bubble"] >> nth=-1'
|
||||
)
|
||||
await expect(hostBubble).toHaveCSS(
|
||||
'background-color',
|
||||
@ -116,9 +119,9 @@ test.describe.parallel('Theme page', () => {
|
||||
'[data-testid="guest-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=1'
|
||||
)
|
||||
await page.fill('input[value="#FFFFFF"]', '#264653')
|
||||
await typebotViewer(page).locator('text=Go').click()
|
||||
await typebotViewer(page).locator('button >> text="Go"').click()
|
||||
const guestBubble = typebotViewer(page).locator(
|
||||
'[data-testid="guest-bubble"]'
|
||||
'[data-testid="guest-bubble"] >> nth=-1'
|
||||
)
|
||||
await expect(guestBubble).toHaveCSS(
|
||||
'background-color',
|
||||
@ -129,7 +132,7 @@ test.describe.parallel('Theme page', () => {
|
||||
// Guest avatar
|
||||
await page.click('text=User avatar')
|
||||
await expect(
|
||||
typebotViewer(page).locator('[data-testid="default-avatar"]')
|
||||
typebotViewer(page).locator('[data-testid="default-avatar"] >> nth=-1')
|
||||
).toBeVisible()
|
||||
await page.click('[data-testid="default-avatar"]')
|
||||
await page.click('button:has-text("Embed link")')
|
||||
@ -137,6 +140,8 @@ test.describe.parallel('Theme page', () => {
|
||||
'input[placeholder="Paste the image link..."]',
|
||||
guestAvatarUrl
|
||||
)
|
||||
|
||||
typebotViewer(page).locator('button >> text="Go"').click()
|
||||
await expect(typebotViewer(page).locator('img')).toHaveAttribute(
|
||||
'src',
|
||||
guestAvatarUrl
|
||||
@ -151,7 +156,6 @@ test.describe.parallel('Theme page', () => {
|
||||
'[data-testid="inputs-theme"] >> [aria-label="Pick a color"] >> nth=1'
|
||||
)
|
||||
await page.fill('input[value="#303235"]', '#023e8a')
|
||||
await typebotViewer(page).locator('text=Go').click()
|
||||
const input = typebotViewer(page).locator('.typebot-input')
|
||||
await expect(input).toHaveCSS('background-color', 'rgb(255, 232, 214)')
|
||||
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('button >> text=Yes').click()
|
||||
await page.goto(`http://localhost:3000/typebots/${typebotId}/results`)
|
||||
await expect(page.locator('text=Baptiste')).toBeVisible()
|
||||
await expect(page.locator('text=26')).toBeVisible()
|
||||
await expect(page.locator('text=Yes')).toBeVisible()
|
||||
await expect(page.locator('text="Baptiste"')).toBeVisible()
|
||||
await expect(page.locator('text="26"')).toBeVisible()
|
||||
await expect(page.locator('text="Yes"')).toBeVisible()
|
||||
})
|
||||
|
Reference in New Issue
Block a user