import { Editable, EditableInput, EditablePreview, SlideFade, Stack, useColorModeValue, } from '@chakra-ui/react' import React, { memo, useCallback, useEffect, useRef, useState } from 'react' import { Group } from 'models' import { Coordinates, useGraph, useGroupsCoordinates, useBlockDnd, } from '../../../providers' import { BlockNodesList } from '../BlockNode/BlockNodesList' import { isDefined, isNotDefined } from 'utils' import { useTypebot, RightPanel, useEditor } from '@/features/editor' 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' 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 ( ) } 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') const { connectingIds, setConnectingIds, previewingEdge, previewingBlock, isReadOnly, graphPosition, } = useGraph() const { typebot, updateGroup, deleteGroup, duplicateGroup } = useTypebot() 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(null) const [debouncedGroupPosition] = useDebounce(currentCoordinates, 100) const [isFocused, setIsFocused] = useState(false) useOutsideClick({ handler: () => setIsFocused(false), ref: groupRef, capture: true, }) // 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 ) 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) => title.length > 0 ? updateGroup(groupIndex, { title }) : undefined const handleMouseEnter = () => { if (isReadOnly) return if (mouseOverGroup?.id !== group.id && !isStartGroup && groupRef.current) setMouseOverGroup({ id: group.id, element: groupRef.current }) if (connectingIds) setConnectingIds({ ...connectingIds, target: { groupId: group.id } }) } const handleMouseLeave = () => { if (isReadOnly) return setMouseOverGroup(undefined) if (connectingIds) setConnectingIds({ ...connectingIds, target: undefined }) } const startPreviewAtThisGroup = () => { setStartPreviewAtGroup(group.id) setRightPanel(RightPanel.PREVIEW) } useDrag( ({ first, last, offset: [offsetX, offsetY], event, target }) => { event.stopPropagation() if ((target as HTMLElement).classList.contains('prevent-group-drag')) return if (first) { setIsFocused(true) setIsMouseDown(true) } 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, ], } ) return ( renderMenu={() => } isDisabled={isReadOnly || isStartGroup} > {(ref, isContextMenuOpened) => ( {typebot && ( )} {!isReadOnly && ( { setIsFocused(false) duplicateGroup(groupIndex) }} onDeleteClick={() => deleteGroup(groupIndex)} /> )} )} ) } export const DraggableGroupNode = memo(NonMemoizedDraggableGroupNode)