♻️ Introduce typebot v6 with events (#1013)

Closes #885
This commit is contained in:
Baptiste Arnaud
2023-11-08 15:34:16 +01:00
committed by GitHub
parent 68e4fc71fb
commit 35300eaf34
634 changed files with 58971 additions and 31449 deletions

View File

@@ -0,0 +1,68 @@
import { InfoIcon, PlayIcon, TrashIcon } from '@/components/icons'
import {
HStack,
IconButton,
Tooltip,
useClipboard,
useColorModeValue,
} from '@chakra-ui/react'
type Props = {
eventId: string
onPlayClick: () => void
onDeleteClick?: () => void
}
export const EventFocusToolbar = ({
eventId,
onPlayClick,
onDeleteClick,
}: Props) => {
const { hasCopied, onCopy } = useClipboard(eventId)
return (
<HStack
rounded="md"
spacing={0}
borderWidth="1px"
bgColor={useColorModeValue('white', 'gray.800')}
shadow="md"
>
<IconButton
icon={<PlayIcon />}
borderRightWidth="1px"
borderRightRadius="none"
aria-label={'Preview bot from this group'}
variant="ghost"
onClick={onPlayClick}
size="sm"
/>
<Tooltip
label={hasCopied ? 'Copied!' : eventId}
closeOnClick={false}
placement="top"
>
<IconButton
icon={<InfoIcon />}
borderRightWidth="1px"
borderRightRadius="none"
borderLeftRadius="none"
aria-label={'Show group info'}
variant="ghost"
size="sm"
onClick={onCopy}
/>
</Tooltip>
{onDeleteClick ? (
<IconButton
aria-label="Delete"
borderLeftRadius="none"
icon={<TrashIcon />}
onClick={onDeleteClick}
variant="ghost"
size="sm"
/>
) : null}
</HStack>
)
}

View File

@@ -0,0 +1,208 @@
import { SlideFade, Stack, useColorModeValue } from '@chakra-ui/react'
import React, { memo, useCallback, useEffect, useRef, useState } from 'react'
import { EventNodeContextMenu } from './EventNodeContextMenu'
import { useDebounce } from 'use-debounce'
import { ContextMenu } from '@/components/ContextMenu'
import { useDrag } from '@use-gesture/react'
import { EventFocusToolbar } from './EventFocusToolbar'
import { useOutsideClick } from '@/hooks/useOutsideClick'
import {
RightPanel,
useEditor,
} from '@/features/editor/providers/EditorProvider'
import { useGraph } from '@/features/graph/providers/GraphProvider'
import { useEventsCoordinates } from '@/features/graph/providers/EventsCoordinateProvider'
import { setMultipleRefs } from '@/helpers/setMultipleRefs'
import { Coordinates } from '@/features/graph/types'
import { TEvent } from '@typebot.io/schemas'
import { EventNodeContent } from './EventNodeContent'
import { EventSourceEndpoint } from '../../endpoints/EventSourceEndpoint'
import { eventWidth } from '@/features/graph/constants'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
type Props = {
event: TEvent
eventIndex: number
}
export const EventNode = ({ event, eventIndex }: Props) => {
const { updateEventCoordinates } = useEventsCoordinates()
const handleEventDrag = useCallback(
(newCoord: Coordinates) => {
updateEventCoordinates(event.id, newCoord)
},
[event.id, updateEventCoordinates]
)
return (
<DraggableEventNode
event={event}
eventIndex={eventIndex}
onEventDrag={handleEventDrag}
/>
)
}
const NonMemoizedDraggableEventNode = ({
event,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
eventIndex,
onEventDrag,
}: Props & { onEventDrag: (newCoord: Coordinates) => void }) => {
const elementBgColor = useColorModeValue('white', 'gray.900')
const previewingBorderColor = useColorModeValue('blue.400', 'blue.300')
const { previewingEdge, isReadOnly, graphPosition } = useGraph()
const { updateEvent } = useTypebot()
const { setRightPanel, setStartPreviewAtEvent } = useEditor()
const [isMouseDown, setIsMouseDown] = useState(false)
const [currentCoordinates, setCurrentCoordinates] = useState(
event.graphCoordinates
)
const isPreviewing = previewingEdge
? 'eventId' in previewingEdge.from
? previewingEdge.from.eventId === event.id
: false
: false
const eventRef = useRef<HTMLDivElement | null>(null)
const [debouncedEventPosition] = useDebounce(currentCoordinates, 100)
const [isFocused, setIsFocused] = useState(false)
useOutsideClick({
handler: () => setIsFocused(false),
ref: eventRef,
capture: true,
isEnabled: isFocused,
})
// When the event is moved from external action (e.g. undo/redo), update the current coordinates
useEffect(() => {
setCurrentCoordinates({
x: event.graphCoordinates.x,
y: event.graphCoordinates.y,
})
}, [event.graphCoordinates.x, event.graphCoordinates.y])
useEffect(() => {
if (!currentCoordinates || isReadOnly) return
if (
currentCoordinates?.x === event.graphCoordinates.x &&
currentCoordinates.y === event.graphCoordinates.y
)
return
updateEvent(eventIndex, { graphCoordinates: currentCoordinates })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedEventPosition])
const startPreviewAtThisEvent = () => {
setStartPreviewAtEvent(event.id)
setRightPanel(RightPanel.PREVIEW)
}
useDrag(
({ first, last, offset: [offsetX, offsetY], event, target }) => {
event.stopPropagation()
if (
(target as HTMLElement)
.closest('.prevent-event-drag')
?.classList.contains('prevent-event-drag')
)
return
if (first) {
setIsFocused(true)
setIsMouseDown(true)
}
if (last) {
setIsMouseDown(false)
}
const newCoord = {
x: Number((offsetX / graphPosition.scale).toFixed(2)),
y: Number((offsetY / graphPosition.scale).toFixed(2)),
}
setCurrentCoordinates(newCoord)
onEventDrag(newCoord)
},
{
target: eventRef,
pointer: { keys: false },
from: () => [
currentCoordinates.x * graphPosition.scale,
currentCoordinates.y * graphPosition.scale,
],
}
)
return (
<ContextMenu<HTMLDivElement>
renderMenu={() => <EventNodeContextMenu />}
isDisabled={isReadOnly || event.type === 'start'}
>
{(ref, isContextMenuOpened) => (
<Stack
ref={setMultipleRefs([ref, eventRef])}
id={`event-${event.id}`}
data-testid="event"
py="2"
pl="3"
pr="3"
w={eventWidth}
rounded="xl"
bg={elementBgColor}
borderWidth="1px"
fontWeight="semibold"
borderColor={
isContextMenuOpened || isPreviewing || isFocused
? previewingBorderColor
: elementBgColor
}
transition="border 300ms, box-shadow 200ms"
pos="absolute"
style={{
transform: `translate(${currentCoordinates?.x ?? 0}px, ${
currentCoordinates?.y ?? 0
}px)`,
touchAction: 'none',
}}
cursor={isMouseDown ? 'grabbing' : 'pointer'}
shadow="md"
_hover={{ shadow: 'lg' }}
zIndex={isFocused ? 10 : 1}
>
<EventNodeContent event={event} />
<EventSourceEndpoint
source={{
eventId: event.id,
}}
pos="absolute"
right="-19px"
bottom="4px"
isHidden={false}
/>
{!isReadOnly && (
<SlideFade
in={isFocused}
style={{
position: 'absolute',
top: '-45px',
right: 0,
}}
unmountOnExit
>
<EventFocusToolbar
eventId={event.id}
onPlayClick={startPreviewAtThisEvent}
onDeleteClick={event.type !== 'start' ? () => {} : undefined}
/>
</SlideFade>
)}
</Stack>
)}
</ContextMenu>
)
}
export const DraggableEventNode = memo(NonMemoizedDraggableEventNode)

View File

@@ -0,0 +1,14 @@
import { StartEventNode } from '@/features/events/start/StartEventNode'
import { TEvent } from '@typebot.io/schemas'
type Props = {
event: TEvent
}
export const EventNodeContent = ({ event }: Props) => {
switch (event.type) {
case 'start':
return <StartEventNode />
default:
return null
}
}

View File

@@ -0,0 +1,24 @@
import { MenuList, MenuItem } from '@chakra-ui/react'
import { CopyIcon, TrashIcon } from '@/components/icons'
type Props = {
onDuplicateClick?: () => void
onDeleteClick?: () => void
}
export const EventNodeContextMenu = ({
onDuplicateClick,
onDeleteClick,
}: Props) => (
<MenuList>
{onDuplicateClick && (
<MenuItem icon={<CopyIcon />} onClick={onDuplicateClick}>
Duplicate
</MenuItem>
)}
{onDeleteClick && (
<MenuItem icon={<TrashIcon />} onClick={onDeleteClick}>
Delete
</MenuItem>
)}
</MenuList>
)

View File

@@ -0,0 +1 @@
export { EventNode } from './EventNode'