2022-06-11 07:27:38 +02:00
|
|
|
import {
|
|
|
|
Editable,
|
|
|
|
EditableInput,
|
|
|
|
EditablePreview,
|
2023-02-23 16:11:51 +01:00
|
|
|
SlideFade,
|
2022-06-11 07:27:38 +02:00
|
|
|
Stack,
|
2022-12-20 16:55:43 +01:00
|
|
|
useColorModeValue,
|
2022-06-11 07:27:38 +02:00
|
|
|
} from '@chakra-ui/react'
|
2022-06-26 16:12:28 +02:00
|
|
|
import React, { memo, useCallback, useEffect, useRef, useState } from 'react'
|
2022-06-11 07:27:38 +02:00
|
|
|
import { Group } from 'models'
|
2022-06-26 16:12:28 +02:00
|
|
|
import {
|
|
|
|
Coordinates,
|
|
|
|
useGraph,
|
|
|
|
useGroupsCoordinates,
|
2022-11-15 09:35:48 +01:00
|
|
|
useBlockDnd,
|
|
|
|
} from '../../../providers'
|
2022-06-11 07:27:38 +02:00
|
|
|
import { BlockNodesList } from '../BlockNode/BlockNodesList'
|
|
|
|
import { isDefined, isNotDefined } from 'utils'
|
2022-11-15 09:35:48 +01:00
|
|
|
import { useTypebot, RightPanel, useEditor } from '@/features/editor'
|
2022-06-11 07:27:38 +02:00
|
|
|
import { GroupNodeContextMenu } from './GroupNodeContextMenu'
|
2022-06-26 16:12:28 +02:00
|
|
|
import { useDebounce } from 'use-debounce'
|
2022-11-15 09:35:48 +01:00
|
|
|
import { ContextMenu } from '@/components/ContextMenu'
|
|
|
|
import { setMultipleRefs } from '@/utils/helpers'
|
2022-11-18 07:58:43 +01:00
|
|
|
import { useDrag } from '@use-gesture/react'
|
2023-02-23 16:11:51 +01:00
|
|
|
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) => {
|
2022-06-26 16:12:28 +02:00
|
|
|
const { updateGroupCoordinates } = useGroupsCoordinates()
|
|
|
|
|
2022-11-18 07:58:43 +01:00
|
|
|
const handleGroupDrag = useCallback(
|
|
|
|
(newCoord: Coordinates) => {
|
|
|
|
updateGroupCoordinates(group.id, newCoord)
|
|
|
|
},
|
|
|
|
[group.id, updateGroupCoordinates]
|
|
|
|
)
|
2022-06-26 16:12:28 +02:00
|
|
|
|
|
|
|
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 }) => {
|
2022-12-20 16:55:43 +01:00
|
|
|
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,
|
2023-02-21 15:25:14 +01:00
|
|
|
previewingBlock,
|
2022-11-21 11:12:43 +01:00
|
|
|
isReadOnly,
|
|
|
|
graphPosition,
|
|
|
|
} = useGraph()
|
2023-02-23 16:11:51 +01:00
|
|
|
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)
|
|
|
|
|
2023-02-23 16:11:51 +01:00
|
|
|
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-06-26 16:12:28 +02:00
|
|
|
|
2022-11-21 11:12:43 +01:00
|
|
|
const startPreviewAtThisGroup = () => {
|
|
|
|
setStartPreviewAtGroup(group.id)
|
|
|
|
setRightPanel(RightPanel.PREVIEW)
|
|
|
|
}
|
|
|
|
|
|
|
|
useDrag(
|
2023-02-23 19:22:32 +01:00
|
|
|
({ 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) {
|
2023-02-23 19:22:32 +01:00
|
|
|
setIsFocused(true)
|
2022-11-21 11:12:43 +01:00
|
|
|
setIsMouseDown(true)
|
2022-11-18 07:58:43 +01:00
|
|
|
}
|
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-18 07:58:43 +01:00
|
|
|
|
2022-11-21 11:12:43 +01:00
|
|
|
return (
|
|
|
|
<ContextMenu<HTMLDivElement>
|
|
|
|
renderMenu={() => <GroupNodeContextMenu groupIndex={groupIndex} />}
|
|
|
|
isDisabled={isReadOnly || isStartGroup}
|
|
|
|
>
|
2022-12-20 16:55:43 +01:00
|
|
|
{(ref, isContextMenuOpened) => (
|
2022-11-21 11:12:43 +01:00
|
|
|
<Stack
|
|
|
|
ref={setMultipleRefs([ref, groupRef])}
|
|
|
|
data-testid="group"
|
|
|
|
p="4"
|
|
|
|
rounded="xl"
|
2022-12-20 16:55:43 +01:00
|
|
|
bg={bg}
|
|
|
|
borderWidth={
|
|
|
|
isConnecting || isContextMenuOpened || isPreviewing ? '2px' : '1px'
|
|
|
|
}
|
2022-11-21 11:12:43 +01:00
|
|
|
borderColor={
|
2023-02-23 16:11:51 +01:00
|
|
|
isConnecting || isContextMenuOpened || isPreviewing || isFocused
|
2022-12-20 16:55:43 +01:00
|
|
|
? 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)`,
|
2023-02-15 14:51:58 +01:00
|
|
|
touchAction: 'none',
|
2022-11-21 11:12:43 +01:00
|
|
|
}}
|
|
|
|
onMouseEnter={handleMouseEnter}
|
|
|
|
onMouseLeave={handleMouseLeave}
|
|
|
|
cursor={isMouseDown ? 'grabbing' : 'pointer'}
|
|
|
|
shadow="md"
|
|
|
|
_hover={{ shadow: 'lg' }}
|
2023-02-23 16:11:51 +01:00
|
|
|
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
|
2022-12-20 16:55:43 +01:00
|
|
|
_hover={{
|
|
|
|
bg: editableHoverBg,
|
|
|
|
}}
|
2022-11-21 11:12:43 +01:00
|
|
|
px="1"
|
|
|
|
userSelect={'none'}
|
2022-11-18 07:58:43 +01:00
|
|
|
/>
|
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}
|
|
|
|
/>
|
|
|
|
)}
|
2023-02-23 16:11:51 +01:00
|
|
|
<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)
|