2
0
Files
bot/apps/builder/src/features/graph/components/Nodes/GroupNode/GroupNode.tsx

267 lines
7.8 KiB
TypeScript
Raw Normal View History

2022-06-11 07:27:38 +02:00
import {
Editable,
EditableInput,
EditablePreview,
SlideFade,
2022-06-11 07:27:38 +02:00
Stack,
useColorModeValue,
2022-06-11 07:27:38 +02:00
} from '@chakra-ui/react'
import React, { memo, useCallback, useEffect, useRef, useState } from 'react'
2022-06-11 07:27:38 +02:00
import { Group } from 'models'
import {
Coordinates,
useGraph,
useGroupsCoordinates,
useBlockDnd,
} from '../../../providers'
2022-06-11 07:27:38 +02:00
import { BlockNodesList } from '../BlockNode/BlockNodesList'
import { isDefined, isNotDefined } from 'utils'
import { useTypebot, RightPanel, useEditor } from '@/features/editor'
2022-06-11 07:27:38 +02:00
import { GroupNodeContextMenu } from './GroupNodeContextMenu'
import { useDebounce } from 'use-debounce'
import { ContextMenu } from '@/components/ContextMenu'
import { setMultipleRefs } from '@/utils/helpers'
import { useDrag } from '@use-gesture/react'
import { GroupFocusToolbar } from './GroupFocusToolbar'
import { useOutsideClick } from '@/hooks/useOutsideClick'
2022-06-11 07:27:38 +02:00
type Props = {
group: Group
groupIndex: number
}
export const GroupNode = ({ group, groupIndex }: Props) => {
const { updateGroupCoordinates } = useGroupsCoordinates()
const handleGroupDrag = useCallback(
(newCoord: Coordinates) => {
updateGroupCoordinates(group.id, newCoord)
},
[group.id, updateGroupCoordinates]
)
return (
<DraggableGroupNode
group={group}
groupIndex={groupIndex}
onGroupDrag={handleGroupDrag}
/>
)
}
2022-06-11 07:27:38 +02:00
2022-11-21 11:12:43 +01:00
const NonMemoizedDraggableGroupNode = ({
group,
groupIndex,
onGroupDrag,
}: Props & { onGroupDrag: (newCoord: Coordinates) => void }) => {
const bg = useColorModeValue('white', 'gray.900')
const previewingBorderColor = useColorModeValue('blue.400', 'blue.300')
const borderColor = useColorModeValue('white', 'gray.800')
const editableHoverBg = useColorModeValue('gray.100', 'gray.700')
2022-11-21 11:12:43 +01:00
const {
connectingIds,
setConnectingIds,
previewingEdge,
previewingBlock,
2022-11-21 11:12:43 +01:00
isReadOnly,
graphPosition,
} = useGraph()
const { typebot, updateGroup, deleteGroup, duplicateGroup } = useTypebot()
2022-11-21 11:12:43 +01:00
const { setMouseOverGroup, mouseOverGroup } = useBlockDnd()
const { setRightPanel, setStartPreviewAtGroup } = useEditor()
const [isMouseDown, setIsMouseDown] = useState(false)
const [isConnecting, setIsConnecting] = useState(false)
const [currentCoordinates, setCurrentCoordinates] = useState(
group.graphCoordinates
)
const [groupTitle, setGroupTitle] = useState(group.title)
const isPreviewing =
previewingBlock?.groupId === group.id ||
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)
const [isFocused, setIsFocused] = useState(false)
useOutsideClick({
handler: () => setIsFocused(false),
ref: groupRef,
capture: true,
})
2022-11-21 11:12:43 +01:00
// When the group is moved from external action (e.g. undo/redo), update the current coordinates
useEffect(() => {
setCurrentCoordinates({
x: group.graphCoordinates.x,
y: group.graphCoordinates.y,
})
}, [group.graphCoordinates.x, group.graphCoordinates.y])
// Same for group title
useEffect(() => {
setGroupTitle(group.title)
}, [group.title])
useEffect(() => {
if (!currentCoordinates || isReadOnly) return
if (
currentCoordinates?.x === group.graphCoordinates.x &&
currentCoordinates.y === group.graphCoordinates.y
2022-06-11 07:27:38 +02:00
)
2022-11-21 11:12:43 +01:00
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])
2022-06-11 07:27:38 +02:00
2022-11-21 11:12:43 +01:00
const handleTitleSubmit = (title: string) =>
title.length > 0 ? updateGroup(groupIndex, { title }) : undefined
2022-06-11 07:27:38 +02:00
2022-11-21 11:12:43 +01:00
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 })
}
2022-11-21 11:12:43 +01:00
const startPreviewAtThisGroup = () => {
setStartPreviewAtGroup(group.id)
setRightPanel(RightPanel.PREVIEW)
}
useDrag(
({ first, last, offset: [offsetX, offsetY], event, target }) => {
2022-11-21 11:12:43 +01:00
event.stopPropagation()
if ((target as HTMLElement).classList.contains('prevent-group-drag'))
return
if (first) {
setIsFocused(true)
2022-11-21 11:12:43 +01:00
setIsMouseDown(true)
}
2022-11-21 11:12:43 +01:00
if (last) {
setIsMouseDown(false)
}
const newCoord = {
x: offsetX / graphPosition.scale,
y: offsetY / graphPosition.scale,
}
setCurrentCoordinates(newCoord)
onGroupDrag(newCoord)
},
{
target: groupRef,
pointer: { keys: false },
from: () => [
currentCoordinates.x * graphPosition.scale,
currentCoordinates.y * graphPosition.scale,
],
}
)
2022-11-21 11:12:43 +01:00
return (
<ContextMenu<HTMLDivElement>
renderMenu={() => <GroupNodeContextMenu groupIndex={groupIndex} />}
isDisabled={isReadOnly || isStartGroup}
>
{(ref, isContextMenuOpened) => (
2022-11-21 11:12:43 +01:00
<Stack
ref={setMultipleRefs([ref, groupRef])}
data-testid="group"
p="4"
rounded="xl"
bg={bg}
borderWidth={
isConnecting || isContextMenuOpened || isPreviewing ? '2px' : '1px'
}
2022-11-21 11:12:43 +01:00
borderColor={
isConnecting || isContextMenuOpened || isPreviewing || isFocused
? previewingBorderColor
: borderColor
2022-11-21 11:12:43 +01:00
}
w="300px"
transition="border 300ms, box-shadow 200ms"
pos="absolute"
style={{
transform: `translate(${currentCoordinates?.x ?? 0}px, ${
currentCoordinates?.y ?? 0
}px)`,
touchAction: 'none',
2022-11-21 11:12:43 +01:00
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
cursor={isMouseDown ? 'grabbing' : 'pointer'}
shadow="md"
_hover={{ shadow: 'lg' }}
zIndex={isFocused ? 10 : 1}
2022-11-21 11:12:43 +01:00
>
<Editable
value={groupTitle}
onChange={setGroupTitle}
onSubmit={handleTitleSubmit}
fontWeight="semibold"
pointerEvents={isReadOnly || isStartGroup ? 'none' : 'auto'}
pr="8"
2022-06-11 07:27:38 +02:00
>
2022-11-21 11:12:43 +01:00
<EditablePreview
_hover={{
bg: editableHoverBg,
}}
2022-11-21 11:12:43 +01:00
px="1"
userSelect={'none'}
/>
2022-11-21 11:12:43 +01:00
<EditableInput minW="0" px="1" className="prevent-group-drag" />
</Editable>
{typebot && (
<BlockNodesList
groupId={group.id}
blocks={group.blocks}
groupIndex={groupIndex}
groupRef={ref}
isStartGroup={isStartGroup}
/>
)}
<SlideFade
in={isFocused}
style={{
position: 'absolute',
top: '-50px',
right: 0,
}}
unmountOnExit
>
<GroupFocusToolbar
onPlayClick={startPreviewAtThisGroup}
onDuplicateClick={() => {
setIsFocused(false)
duplicateGroup(groupIndex)
}}
onDeleteClick={() => deleteGroup(groupIndex)}
/>
</SlideFade>
2022-11-21 11:12:43 +01:00
</Stack>
)}
</ContextMenu>
)
}
export const DraggableGroupNode = memo(NonMemoizedDraggableGroupNode)