2
0

🚸 (condition) Enable multiple condition items in one block

Closes #162
This commit is contained in:
Baptiste Arnaud
2022-11-16 14:56:09 +01:00
parent 96eb77d94b
commit 6725c17a02
24 changed files with 327 additions and 216 deletions

View File

@ -48,6 +48,7 @@ export const Graph = ({
const {
setGraphPosition: setGlobalGraphPosition,
setOpenedBlockId,
setOpenedItemId,
setPreviewingEdge,
connectingIds,
} = useGraph()
@ -126,6 +127,7 @@ export const Graph = ({
const handleClick = () => {
setOpenedBlockId(undefined)
setOpenedItemId(undefined)
setPreviewingEdge(undefined)
}

View File

@ -9,12 +9,12 @@ import React, { useEffect, useRef, useState } from 'react'
import {
BubbleBlock,
BubbleBlockContent,
ConditionBlock,
DraggableBlock,
Block,
BlockWithOptions,
TextBubbleContent,
TextBubbleBlock,
LogicBlockType,
} from 'models'
import { isBubbleBlock, isTextBubbleBlock } from 'utils'
import { BlockNodeContent } from './BlockNodeContent/BlockNodeContent'
@ -28,7 +28,12 @@ import { BlockSettings } from './SettingsPopoverContent/SettingsPopoverContent'
import { TextBubbleEditor } from '../../../../blocks/bubbles/textBubble/components/TextBubbleEditor'
import { TargetEndpoint } from '../../Endpoints'
import { MediaBubblePopoverContent } from './MediaBubblePopoverContent'
import { NodePosition, useDragDistance, useGraph } from '../../../providers'
import {
NodePosition,
useBlockDnd,
useDragDistance,
useGraph,
} from '../../../providers'
import { ContextMenu } from '@/components/ContextMenu'
import { setMultipleRefs } from '@/utils/helpers'
import { hasDefaultConnector } from '../../../utils'
@ -53,6 +58,7 @@ export const BlockNode = ({
setFocusedGroupId,
previewingEdge,
} = useGraph()
const { mouseOverBlock, setMouseOverBlock, draggedItem } = useBlockDnd()
const { typebot, updateBlock } = useTypebot()
const [isConnecting, setIsConnecting] = useState(false)
const [isPopoverOpened, setIsPopoverOpened] = useState(
@ -99,6 +105,8 @@ export const BlockNode = ({
}
const handleMouseEnter = () => {
if (draggedItem !== undefined)
setMouseOverBlock({ id: block.id, ref: blockRef })
if (connectingIds)
setConnectingIds({
...connectingIds,
@ -107,6 +115,7 @@ export const BlockNode = ({
}
const handleMouseLeave = () => {
if (mouseOverBlock) setMouseOverBlock(undefined)
if (connectingIds?.target)
setConnectingIds({
...connectingIds,
@ -238,9 +247,8 @@ export const BlockNode = ({
)
}
const hasSettingsPopover = (
block: Block
): block is BlockWithOptions | ConditionBlock => !isBubbleBlock(block)
const hasSettingsPopover = (block: Block): block is BlockWithOptions =>
!isBubbleBlock(block) && block.type !== LogicBlockType.CONDITION
const isMediaBubbleBlock = (
block: Block

View File

@ -8,8 +8,6 @@ import {
} from '@chakra-ui/react'
import { ExpandIcon } from '@/components/icons'
import {
ConditionItem,
ConditionBlock,
InputBlockType,
IntegrationBlockType,
LogicBlockType,
@ -34,7 +32,6 @@ import { SendEmailSettings } from '@/features/blocks/integrations/sendEmail'
import { WebhookSettings } from '@/features/blocks/integrations/webhook'
import { ZapierSettings } from '@/features/blocks/integrations/zapier'
import { CodeSettings } from '@/features/blocks/logic/code'
import { ConditionSettingsBody } from '@/features/blocks/logic/condition'
import { RedirectSettings } from '@/features/blocks/logic/redirect'
import { SetVariableSettings } from '@/features/blocks/logic/setVariable'
import { TypebotLinkSettingsForm } from '@/features/blocks/logic/typebotLink'
@ -42,7 +39,7 @@ import { ButtonsOptionsForm } from '@/features/blocks/inputs/buttons'
import { ChatwootSettingsForm } from '@/features/blocks/integrations/chatwoot'
type Props = {
block: BlockWithOptions | ConditionBlock
block: BlockWithOptions
webhook?: Webhook
onExpandClick: () => void
onBlockChange: (updates: Partial<Block>) => void
@ -88,18 +85,14 @@ export const BlockSettings = ({
block,
onBlockChange,
}: {
block: BlockWithOptions | ConditionBlock
block: BlockWithOptions
webhook?: Webhook
onBlockChange: (block: Partial<Block>) => void
}): JSX.Element => {
const handleOptionsChange = (options: BlockOptions) => {
onBlockChange({ options } as Partial<Block>)
}
const handleItemChange = (updates: Partial<ConditionItem>) => {
onBlockChange({
items: [{ ...(block as ConditionBlock).items[0], ...updates }],
} as Partial<Block>)
}
switch (block.type) {
case InputBlockType.TEXT: {
return (
@ -189,11 +182,6 @@ export const BlockSettings = ({
/>
)
}
case LogicBlockType.CONDITION: {
return (
<ConditionSettingsBody block={block} onItemChange={handleItemChange} />
)
}
case LogicBlockType.REDIRECT: {
return (
<RedirectSettings

View File

@ -6,13 +6,7 @@ import {
useDragDistance,
} from '../../../providers'
import { useTypebot } from '@/features/editor'
import {
ButtonItem,
ChoiceInputBlock,
Item,
ItemIndices,
ItemType,
} from 'models'
import { ChoiceInputBlock, Item, ItemIndices } from 'models'
import React, { useRef, useState } from 'react'
import { SourceEndpoint } from '../../Endpoints/SourceEndpoint'
import { ItemNodeContent } from './ItemNodeContent'
@ -25,30 +19,37 @@ type Props = {
indices: ItemIndices
onMouseDown?: (
blockNodePosition: { absolute: Coordinates; relative: Coordinates },
item: ButtonItem
item: Item
) => void
connectionDisabled?: boolean
}
export const ItemNode = ({ item, indices, onMouseDown }: Props) => {
export const ItemNode = ({
item,
indices,
onMouseDown,
connectionDisabled,
}: 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 isConnectable =
!connectionDisabled &&
!(
typebot?.groups[indices.groupIndex].blocks[
indices.blockIndex
] as ChoiceInputBlock
)?.options?.isMultipleChoice
const onDrag = (position: NodePosition) => {
if (!onMouseDown || item.type !== ItemType.BUTTON) return
if (!onMouseDown) return
onMouseDown(position, item)
}
useDragDistance({
ref: itemRef,
onDrag,
isDisabled: !onMouseDown || item.type !== ItemType.BUTTON,
isDisabled: !onMouseDown,
})
const handleMouseEnter = () => setIsMouseOver(true)
@ -63,19 +64,19 @@ export const ItemNode = ({ item, indices, onMouseDown }: Props) => {
data-testid="item"
pos="relative"
ref={setMultipleRefs([ref, itemRef])}
w="full"
>
<Flex
align="center"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
shadow="sm"
_hover={isReadOnly ? {} : { shadow: 'md' }}
_hover={{ 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"
>

View File

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

View File

@ -6,16 +6,10 @@ import {
useGraph,
} from '../../../providers'
import { useTypebot } from '@/features/editor'
import {
ButtonItem,
BlockIndices,
BlockWithItems,
LogicBlockType,
} from 'models'
import { BlockIndices, BlockWithItems, LogicBlockType, Item } 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
@ -27,14 +21,13 @@ export const ItemNodesList = ({
indices: { groupIndex, blockIndex },
}: Props) => {
const { typebot, createItem, detachItemFromBlock } = useTypebot()
const { draggedItem, setDraggedItem, mouseOverGroup } = useBlockDnd()
const { draggedItem, setDraggedItem, mouseOverBlock } = 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 isDraggingOnCurrentBlock =
(draggedItem && mouseOverBlock?.id === block.id) ?? false
const showPlaceholders =
draggedItem !== undefined && block.items[0].type === draggedItem.type
const isLastBlock =
typebot?.groups[groupIndex].blocks[blockIndex + 1] === undefined
@ -60,29 +53,37 @@ export const ItemNodesList = ({
useEventListener('mousemove', handleGlobalMouseMove)
useEffect(() => {
if (mouseOverGroup?.id !== block.groupId)
if (!showPlaceholders) return
if (mouseOverBlock?.id !== block.id) {
setExpandedPlaceholderIndex(undefined)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mouseOverGroup?.id])
}, [mouseOverBlock?.id, showPlaceholders])
const handleMouseMoveOnGroup = (event: MouseEvent) => {
if (!isDraggingOnCurrentGroup || isReadOnly) return
const handleMouseMoveOnBlock = (event: MouseEvent) => {
if (!isDraggingOnCurrentBlock) return
const index = computeNearestPlaceholderIndex(event.pageY, placeholderRefs)
setExpandedPlaceholderIndex(index)
}
useEventListener(
'mousemove',
handleMouseMoveOnGroup,
mouseOverGroup?.ref.current
handleMouseMoveOnBlock,
mouseOverBlock?.ref.current
)
const handleMouseUpOnGroup = (e: MouseEvent) => {
if (
!showPlaceholders ||
!isDraggingOnCurrentBlock ||
!draggedItem ||
mouseOverBlock?.id !== block.id
)
return
setExpandedPlaceholderIndex(undefined)
if (!isDraggingOnCurrentGroup) return
const itemIndex = computeNearestPlaceholderIndex(e.pageY, placeholderRefs)
e.stopPropagation()
setDraggedItem(undefined)
createItem(draggedItem as ButtonItem, {
createItem(draggedItem, {
groupIndex,
blockIndex,
itemIndex,
@ -91,7 +92,7 @@ export const ItemNodesList = ({
useEventListener(
'mouseup',
handleMouseUpOnGroup,
mouseOverGroup?.ref.current,
mouseOverBlock?.ref.current,
{
capture: true,
}
@ -101,9 +102,9 @@ export const ItemNodesList = ({
(itemIndex: number) =>
(
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
item: ButtonItem
item: Item
) => {
if (!typebot || isReadOnly) return
if (!typebot || block.items.length <= 1) return
placeholderRefs.current.splice(itemIndex + 1, 1)
detachItemFromBlock({ groupIndex, blockIndex, itemIndex })
setPosition(absolute)
@ -119,13 +120,7 @@ export const ItemNodesList = ({
}
return (
<Stack
flex={1}
spacing={1}
maxW="full"
onClick={stopPropagating}
pointerEvents={isReadOnly ? 'none' : 'all'}
>
<Stack flex={1} spacing={1} maxW="full" onClick={stopPropagating}>
<Flex
ref={handlePushElementRef(0)}
h={showPlaceholders && expandedPlaceholderIndex === 0 ? '50px' : '2px'}
@ -161,13 +156,15 @@ export const ItemNodesList = ({
py="2"
borderWidth="1px"
borderColor="gray.300"
bgColor={isReadOnly ? '' : 'gray.50'}
bgColor={'gray.50'}
rounded="md"
pos="relative"
align="center"
cursor={isReadOnly ? 'pointer' : 'not-allowed'}
cursor="not-allowed"
>
<Text color={isReadOnly ? 'inherit' : 'gray.500'}>Default</Text>
<Text color="gray.500">
{block.type === LogicBlockType.CONDITION ? 'Else' : 'Default'}
</Text>
<SourceEndpoint
source={{
groupId: block.groupId,
@ -181,16 +178,23 @@ export const ItemNodesList = ({
{draggedItem && draggedItem.blockId === block.id && (
<Portal>
<ItemNodeOverlay
item={draggedItem}
<Flex
pointerEvents="none"
pos="fixed"
top="0"
left="0"
style={{
transform: `translate(${position.x}px, ${position.y}px) rotate(-2deg) scale(${graphPosition.scale})`,
}}
w="220px"
transformOrigin="0 0 0"
/>
>
<ItemNode
item={draggedItem}
indices={{ groupIndex, blockIndex, itemIndex: 0 }}
connectionDisabled
/>
</Flex>
</Portal>
)}
</Stack>

View File

@ -1,5 +1,5 @@
import { useEventListener } from '@chakra-ui/react'
import { ButtonItem, DraggableBlock, DraggableBlockType } from 'models'
import { DraggableBlock, DraggableBlockType, Item } from 'models'
import {
createContext,
Dispatch,
@ -11,7 +11,7 @@ import {
} from 'react'
import { Coordinates } from './GraphProvider'
type GroupInfo = {
type NodeInfo = {
id: string
ref: React.MutableRefObject<HTMLDivElement | null>
}
@ -21,10 +21,12 @@ const graphDndContext = createContext<{
setDraggedBlockType: Dispatch<SetStateAction<DraggableBlockType | undefined>>
draggedBlock?: DraggableBlock
setDraggedBlock: Dispatch<SetStateAction<DraggableBlock | undefined>>
draggedItem?: ButtonItem
setDraggedItem: Dispatch<SetStateAction<ButtonItem | undefined>>
mouseOverGroup?: GroupInfo
setMouseOverGroup: Dispatch<SetStateAction<GroupInfo | undefined>>
draggedItem?: Item
setDraggedItem: Dispatch<SetStateAction<Item | undefined>>
mouseOverGroup?: NodeInfo
setMouseOverGroup: Dispatch<SetStateAction<NodeInfo | undefined>>
mouseOverBlock?: NodeInfo
setMouseOverBlock: Dispatch<SetStateAction<NodeInfo | undefined>>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})
@ -36,8 +38,9 @@ export const GraphDndProvider = ({ children }: { children: ReactNode }) => {
const [draggedBlockType, setDraggedBlockType] = useState<
DraggableBlockType | undefined
>()
const [draggedItem, setDraggedItem] = useState<ButtonItem | undefined>()
const [mouseOverGroup, setMouseOverGroup] = useState<GroupInfo>()
const [draggedItem, setDraggedItem] = useState<Item | undefined>()
const [mouseOverGroup, setMouseOverGroup] = useState<NodeInfo>()
const [mouseOverBlock, setMouseOverBlock] = useState<NodeInfo>()
return (
<graphDndContext.Provider
@ -50,6 +53,8 @@ export const GraphDndProvider = ({ children }: { children: ReactNode }) => {
setDraggedItem,
mouseOverGroup,
setMouseOverGroup,
mouseOverBlock,
setMouseOverBlock,
}}
>
{children}

View File

@ -69,6 +69,8 @@ const graphContext = createContext<{
addTargetEndpoint: (endpoint: Endpoint) => void
openedBlockId?: string
setOpenedBlockId: Dispatch<SetStateAction<string | undefined>>
openedItemId?: string
setOpenedItemId: Dispatch<SetStateAction<string | undefined>>
isReadOnly: boolean
focusedGroupId?: string
setFocusedGroupId: Dispatch<SetStateAction<string | undefined>>
@ -92,6 +94,7 @@ export const GraphProvider = ({
const [sourceEndpoints, setSourceEndpoints] = useState<IdMap<Endpoint>>({})
const [targetEndpoints, setTargetEndpoints] = useState<IdMap<Endpoint>>({})
const [openedBlockId, setOpenedBlockId] = useState<string>()
const [openedItemId, setOpenedItemId] = useState<string>()
const [focusedGroupId, setFocusedGroupId] = useState<string>()
const addSourceEndpoint = (endpoint: Endpoint) => {
@ -123,6 +126,8 @@ export const GraphProvider = ({
addTargetEndpoint,
openedBlockId,
setOpenedBlockId,
openedItemId,
setOpenedItemId,
isReadOnly,
focusedGroupId,
setFocusedGroupId,