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,168 +30,198 @@ type Props = {
|
||||
}
|
||||
|
||||
export const GroupNode = ({ group, groupIndex }: Props) => {
|
||||
const {
|
||||
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 { updateGroupCoordinates } = useGroupsCoordinates()
|
||||
|
||||
const groupCoordinates = groupsCoordinates[group.id]
|
||||
const groupRef = useRef<HTMLDivElement | null>(null)
|
||||
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 })
|
||||
const handleGroupDrag = useCallback((newCoord: Coordinates) => {
|
||||
updateGroupCoordinates(group.id, newCoord)
|
||||
// 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 (
|
||||
<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(${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>
|
||||
<DraggableGroupNode
|
||||
group={group}
|
||||
groupIndex={groupIndex}
|
||||
onGroupDrag={handleGroupDrag}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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 { 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>
|
||||
)
|
||||
))
|
||||
|
Reference in New Issue
Block a user