♻️ (builder) Change to features-centric folder structure

This commit is contained in:
Baptiste Arnaud
2022-11-15 09:35:48 +01:00
committed by Baptiste Arnaud
parent 3686465a85
commit 643571fe7d
683 changed files with 3907 additions and 3643 deletions

View File

@@ -0,0 +1,104 @@
import { Flex } from '@chakra-ui/react'
import {
Coordinates,
useGraph,
NodePosition,
useDragDistance,
} from '../../../providers'
import { useTypebot } from '@/features/editor'
import {
ButtonItem,
ChoiceInputBlock,
Item,
ItemIndices,
ItemType,
} from 'models'
import React, { useRef, useState } from 'react'
import { SourceEndpoint } from '../../Endpoints/SourceEndpoint'
import { ItemNodeContent } from './ItemNodeContent'
import { ItemNodeContextMenu } from './ItemNodeContextMenu'
import { ContextMenu } from '@/components/ContextMenu'
import { setMultipleRefs } from '@/utils/helpers'
type Props = {
item: Item
indices: ItemIndices
onMouseDown?: (
blockNodePosition: { absolute: Coordinates; relative: Coordinates },
item: ButtonItem
) => void
}
export const ItemNode = ({ item, indices, onMouseDown }: Props) => {
const { typebot } = useTypebot()
const { previewingEdge } = useGraph()
const [isMouseOver, setIsMouseOver] = useState(false)
const itemRef = useRef<HTMLDivElement | null>(null)
const isPreviewing = previewingEdge?.from.itemId === item.id
const isConnectable = !(
typebot?.groups[indices.groupIndex].blocks[
indices.blockIndex
] as ChoiceInputBlock
)?.options?.isMultipleChoice
const isReadOnly = item.type === ItemType.CONDITION
const onDrag = (position: NodePosition) => {
if (!onMouseDown || item.type !== ItemType.BUTTON) return
onMouseDown(position, item)
}
useDragDistance({
ref: itemRef,
onDrag,
isDisabled: !onMouseDown || item.type !== ItemType.BUTTON,
})
const handleMouseEnter = () => setIsMouseOver(true)
const handleMouseLeave = () => setIsMouseOver(false)
return (
<ContextMenu<HTMLDivElement>
renderMenu={() => <ItemNodeContextMenu indices={indices} />}
>
{(ref, isOpened) => (
<Flex
data-testid="item"
pos="relative"
ref={setMultipleRefs([ref, itemRef])}
>
<Flex
align="center"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
shadow="sm"
_hover={isReadOnly ? {} : { shadow: 'md' }}
transition="box-shadow 200ms, border-color 200ms"
rounded="md"
borderWidth={isOpened || isPreviewing ? '2px' : '1px'}
borderColor={isOpened || isPreviewing ? 'blue.400' : 'gray.100'}
margin={isOpened || isPreviewing ? '-1px' : 0}
pointerEvents={isReadOnly ? 'none' : 'all'}
bgColor="white"
w="full"
>
<ItemNodeContent
item={item}
isMouseOver={isMouseOver}
indices={indices}
/>
{typebot && isConnectable && (
<SourceEndpoint
source={{
groupId: typebot.groups[indices.groupIndex].id,
blockId: item.blockId,
itemId: item.id,
}}
pos="absolute"
right="-49px"
pointerEvents="all"
/>
)}
</Flex>
</Flex>
)}
</ContextMenu>
)
}

View File

@@ -0,0 +1,25 @@
import { ButtonNodeContent } from '@/features/blocks/inputs/buttons'
import { ConditionNodeContent } from '@/features/blocks/logic/condition'
import { Item, ItemIndices, ItemType } from 'models'
import React from 'react'
type Props = {
item: Item
indices: ItemIndices
isMouseOver: boolean
}
export const ItemNodeContent = ({ item, indices, isMouseOver }: Props) => {
switch (item.type) {
case ItemType.BUTTON:
return (
<ButtonNodeContent
item={item}
isMouseOver={isMouseOver}
indices={indices}
/>
)
case ItemType.CONDITION:
return <ConditionNodeContent item={item} />
}
}

View File

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

View File

@@ -0,0 +1,21 @@
import { MenuList, MenuItem } from '@chakra-ui/react'
import { TrashIcon } from '@/components/icons'
import { useTypebot } from '@/features/editor'
import { ItemIndices } from 'models'
type Props = {
indices: ItemIndices
}
export const ItemNodeContextMenu = ({ indices }: Props) => {
const { deleteItem } = useTypebot()
const handleDeleteClick = () => deleteItem(indices)
return (
<MenuList>
<MenuItem icon={<TrashIcon />} onClick={handleDeleteClick}>
Delete
</MenuItem>
</MenuList>
)
}

View File

@@ -0,0 +1,26 @@
import { Flex, FlexProps } from '@chakra-ui/react'
import { Item } from 'models'
import React, { ReactNode } from 'react'
type Props = {
item: Item
} & FlexProps
export const ItemNodeOverlay = ({ item, ...props }: Props) => {
return (
<Flex
px="4"
py="2"
rounded="md"
bgColor="white"
borderWidth="1px"
borderColor={'gray.300'}
w="212px"
pointerEvents="none"
shadow="lg"
{...props}
>
{(item.content ?? 'Click to edit') as ReactNode}
</Flex>
)
}

View File

@@ -0,0 +1,198 @@
import { Flex, Portal, Stack, Text, useEventListener } from '@chakra-ui/react'
import {
computeNearestPlaceholderIndex,
useBlockDnd,
Coordinates,
useGraph,
} from '../../../providers'
import { useTypebot } from '@/features/editor'
import {
ButtonItem,
BlockIndices,
BlockWithItems,
LogicBlockType,
} from 'models'
import React, { useEffect, useRef, useState } from 'react'
import { ItemNode } from './ItemNode'
import { SourceEndpoint } from '../../Endpoints'
import { ItemNodeOverlay } from './ItemNodeOverlay'
type Props = {
block: BlockWithItems
indices: BlockIndices
}
export const ItemNodesList = ({
block,
indices: { groupIndex, blockIndex },
}: Props) => {
const { typebot, createItem, detachItemFromBlock } = useTypebot()
const { draggedItem, setDraggedItem, mouseOverGroup } = useBlockDnd()
const placeholderRefs = useRef<HTMLDivElement[]>([])
const { graphPosition } = useGraph()
const groupId = typebot?.groups[groupIndex].id
const isDraggingOnCurrentGroup =
(draggedItem && mouseOverGroup?.id === groupId) ?? false
const isReadOnly = block.type === LogicBlockType.CONDITION
const showPlaceholders = draggedItem && !isReadOnly
const isLastBlock =
typebot?.groups[groupIndex].blocks[blockIndex + 1] === undefined
const [position, setPosition] = useState({
x: 0,
y: 0,
})
const [relativeCoordinates, setRelativeCoordinates] = useState({ x: 0, y: 0 })
const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState<
number | undefined
>()
const handleGlobalMouseMove = (event: MouseEvent) => {
if (!draggedItem || draggedItem.blockId !== block.id) return
const { clientX, clientY } = event
setPosition({
...position,
x: clientX - relativeCoordinates.x,
y: clientY - relativeCoordinates.y,
})
}
useEventListener('mousemove', handleGlobalMouseMove)
useEffect(() => {
if (mouseOverGroup?.id !== block.groupId)
setExpandedPlaceholderIndex(undefined)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mouseOverGroup?.id])
const handleMouseMoveOnGroup = (event: MouseEvent) => {
if (!isDraggingOnCurrentGroup || isReadOnly) return
const index = computeNearestPlaceholderIndex(event.pageY, placeholderRefs)
setExpandedPlaceholderIndex(index)
}
useEventListener(
'mousemove',
handleMouseMoveOnGroup,
mouseOverGroup?.ref.current
)
const handleMouseUpOnGroup = (e: MouseEvent) => {
setExpandedPlaceholderIndex(undefined)
if (!isDraggingOnCurrentGroup) return
const itemIndex = computeNearestPlaceholderIndex(e.pageY, placeholderRefs)
e.stopPropagation()
setDraggedItem(undefined)
createItem(draggedItem as ButtonItem, {
groupIndex,
blockIndex,
itemIndex,
})
}
useEventListener(
'mouseup',
handleMouseUpOnGroup,
mouseOverGroup?.ref.current,
{
capture: true,
}
)
const handleBlockMouseDown =
(itemIndex: number) =>
(
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
item: ButtonItem
) => {
if (!typebot || isReadOnly) return
placeholderRefs.current.splice(itemIndex + 1, 1)
detachItemFromBlock({ groupIndex, blockIndex, itemIndex })
setPosition(absolute)
setRelativeCoordinates(relative)
setDraggedItem(item)
}
const stopPropagating = (e: React.MouseEvent) => e.stopPropagation()
const handlePushElementRef =
(idx: number) => (elem: HTMLDivElement | null) => {
elem && (placeholderRefs.current[idx] = elem)
}
return (
<Stack
flex={1}
spacing={1}
maxW="full"
onClick={stopPropagating}
pointerEvents={isReadOnly ? 'none' : 'all'}
>
<Flex
ref={handlePushElementRef(0)}
h={showPlaceholders && expandedPlaceholderIndex === 0 ? '50px' : '2px'}
bgColor={'gray.300'}
visibility={showPlaceholders ? 'visible' : 'hidden'}
rounded="lg"
transition={showPlaceholders ? 'height 200ms' : 'none'}
/>
{block.items.map((item, idx) => (
<Stack key={item.id} spacing={1}>
<ItemNode
item={item}
indices={{ groupIndex, blockIndex, itemIndex: idx }}
onMouseDown={handleBlockMouseDown(idx)}
/>
<Flex
ref={handlePushElementRef(idx + 1)}
h={
showPlaceholders && expandedPlaceholderIndex === idx + 1
? '50px'
: '2px'
}
bgColor={'gray.300'}
visibility={showPlaceholders ? 'visible' : 'hidden'}
rounded="lg"
transition={showPlaceholders ? 'height 200ms' : 'none'}
/>
</Stack>
))}
{isLastBlock && (
<Flex
px="4"
py="2"
borderWidth="1px"
borderColor="gray.300"
bgColor={isReadOnly ? '' : 'gray.50'}
rounded="md"
pos="relative"
align="center"
cursor={isReadOnly ? 'pointer' : 'not-allowed'}
>
<Text color={isReadOnly ? 'inherit' : 'gray.500'}>Default</Text>
<SourceEndpoint
source={{
groupId: block.groupId,
blockId: block.id,
}}
pos="absolute"
right="-49px"
/>
</Flex>
)}
{draggedItem && draggedItem.blockId === block.id && (
<Portal>
<ItemNodeOverlay
item={draggedItem}
pos="fixed"
top="0"
left="0"
style={{
transform: `translate(${position.x}px, ${position.y}px) rotate(-2deg) scale(${graphPosition.scale})`,
}}
transformOrigin="0 0 0"
/>
</Portal>
)}
</Stack>
)
}

View File

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