♻️ (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,248 @@
import {
Flex,
HStack,
Popover,
PopoverTrigger,
useDisclosure,
} from '@chakra-ui/react'
import React, { useEffect, useRef, useState } from 'react'
import {
BubbleBlock,
BubbleBlockContent,
ConditionBlock,
DraggableBlock,
Block,
BlockWithOptions,
TextBubbleContent,
TextBubbleBlock,
} from 'models'
import { isBubbleBlock, isTextBubbleBlock } from 'utils'
import { BlockNodeContent } from './BlockNodeContent/BlockNodeContent'
import { BlockIcon, useTypebot } from '@/features/editor'
import { SettingsPopoverContent } from './SettingsPopoverContent'
import { BlockNodeContextMenu } from './BlockNodeContextMenu'
import { SourceEndpoint } from '../../Endpoints/SourceEndpoint'
import { useRouter } from 'next/router'
import { SettingsModal } from './SettingsPopoverContent/SettingsModal'
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 { ContextMenu } from '@/components/ContextMenu'
import { setMultipleRefs } from '@/utils/helpers'
import { hasDefaultConnector } from '../../../utils'
export const BlockNode = ({
block,
isConnectable,
indices,
onMouseDown,
}: {
block: Block
isConnectable: boolean
indices: { blockIndex: number; groupIndex: number }
onMouseDown?: (blockNodePosition: NodePosition, block: DraggableBlock) => void
}) => {
const { query } = useRouter()
const {
setConnectingIds,
connectingIds,
openedBlockId,
setOpenedBlockId,
setFocusedGroupId,
previewingEdge,
} = useGraph()
const { typebot, updateBlock } = useTypebot()
const [isConnecting, setIsConnecting] = useState(false)
const [isPopoverOpened, setIsPopoverOpened] = useState(
openedBlockId === block.id
)
const [isEditing, setIsEditing] = useState<boolean>(
isTextBubbleBlock(block) && block.content.plainText === ''
)
const blockRef = useRef<HTMLDivElement | null>(null)
const isPreviewing = isConnecting || previewingEdge?.to.blockId === block.id
const onDrag = (position: NodePosition) => {
if (block.type === 'start' || !onMouseDown) return
onMouseDown(position, block)
}
useDragDistance({
ref: blockRef,
onDrag,
isDisabled: !onMouseDown || block.type === 'start',
})
const {
isOpen: isModalOpen,
onOpen: onModalOpen,
onClose: onModalClose,
} = useDisclosure()
useEffect(() => {
if (query.blockId?.toString() === block.id) setOpenedBlockId(block.id)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query])
useEffect(() => {
setIsConnecting(
connectingIds?.target?.groupId === block.groupId &&
connectingIds?.target?.blockId === block.id
)
}, [connectingIds, block.groupId, block.id])
const handleModalClose = () => {
updateBlock(indices, { ...block })
onModalClose()
}
const handleMouseEnter = () => {
if (connectingIds)
setConnectingIds({
...connectingIds,
target: { groupId: block.groupId, blockId: block.id },
})
}
const handleMouseLeave = () => {
if (connectingIds?.target)
setConnectingIds({
...connectingIds,
target: { ...connectingIds.target, blockId: undefined },
})
}
const handleCloseEditor = (content: TextBubbleContent) => {
const updatedBlock = { ...block, content } as Block
updateBlock(indices, updatedBlock)
setIsEditing(false)
}
const handleClick = (e: React.MouseEvent) => {
setFocusedGroupId(block.groupId)
e.stopPropagation()
if (isTextBubbleBlock(block)) setIsEditing(true)
setOpenedBlockId(block.id)
}
const handleExpandClick = () => {
setOpenedBlockId(undefined)
onModalOpen()
}
const handleBlockUpdate = (updates: Partial<Block>) =>
updateBlock(indices, { ...block, ...updates })
const handleContentChange = (content: BubbleBlockContent) =>
updateBlock(indices, { ...block, content } as Block)
useEffect(() => {
setIsPopoverOpened(openedBlockId === block.id)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [openedBlockId])
return isEditing && isTextBubbleBlock(block) ? (
<TextBubbleEditor
id={block.id}
initialValue={block.content.richText}
onClose={handleCloseEditor}
/>
) : (
<ContextMenu<HTMLDivElement>
renderMenu={() => <BlockNodeContextMenu indices={indices} />}
>
{(ref, isOpened) => (
<Popover
placement="left"
isLazy
isOpen={isPopoverOpened}
closeOnBlur={false}
>
<PopoverTrigger>
<Flex
pos="relative"
ref={setMultipleRefs([ref, blockRef])}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
data-testid={`block`}
w="full"
>
<HStack
flex="1"
userSelect="none"
p="3"
borderWidth={isOpened || isPreviewing ? '2px' : '1px'}
borderColor={isOpened || isPreviewing ? 'blue.400' : 'gray.200'}
margin={isOpened || isPreviewing ? '-1px' : 0}
rounded="lg"
cursor={'pointer'}
bgColor="gray.50"
align="flex-start"
w="full"
transition="border-color 0.2s"
>
<BlockIcon
type={block.type}
mt="1"
data-testid={`${block.id}-icon`}
/>
<BlockNodeContent block={block} indices={indices} />
<TargetEndpoint
pos="absolute"
left="-32px"
top="19px"
blockId={block.id}
/>
{isConnectable && hasDefaultConnector(block) && (
<SourceEndpoint
source={{
groupId: block.groupId,
blockId: block.id,
}}
pos="absolute"
right="-34px"
bottom="10px"
/>
)}
</HStack>
</Flex>
</PopoverTrigger>
{hasSettingsPopover(block) && (
<>
<SettingsPopoverContent
block={block}
onExpandClick={handleExpandClick}
onBlockChange={handleBlockUpdate}
/>
<SettingsModal isOpen={isModalOpen} onClose={handleModalClose}>
<BlockSettings
block={block}
onBlockChange={handleBlockUpdate}
/>
</SettingsModal>
</>
)}
{typebot && isMediaBubbleBlock(block) && (
<MediaBubblePopoverContent
typebotId={typebot.id}
block={block}
onContentChange={handleContentChange}
/>
)}
</Popover>
)}
</ContextMenu>
)
}
const hasSettingsPopover = (
block: Block
): block is BlockWithOptions | ConditionBlock => !isBubbleBlock(block)
const isMediaBubbleBlock = (
block: Block
): block is Exclude<BubbleBlock, TextBubbleBlock> =>
isBubbleBlock(block) && !isTextBubbleBlock(block)

View File

@@ -0,0 +1,162 @@
import { Text } from '@chakra-ui/react'
import {
Block,
StartBlock,
BubbleBlockType,
InputBlockType,
LogicBlockType,
IntegrationBlockType,
BlockIndices,
} from 'models'
import { ItemNodesList } from '../../ItemNode'
import { TextBubbleContent } from '@/features/blocks/bubbles/textBubble'
import { ImageBubbleContent } from '@/features/blocks/bubbles/image'
import { VideoBubbleContent } from '@/features/blocks/bubbles/video'
import { EmbedBubbleContent } from '@/features/blocks/bubbles/embed'
import { TextInputNodeContent } from '@/features/blocks/inputs/textInput'
import { NumberNodeContent } from '@/features/blocks/inputs/number'
import { EmailInputNodeContent } from '@/features/blocks/inputs/emailInput'
import { UrlNodeContent } from '@/features/blocks/inputs/url'
import { PhoneNodeContent } from '@/features/blocks/inputs/phone'
import { DateNodeContent } from '@/features/blocks/inputs/date'
import { SetVariableContent } from '@/features/blocks/logic/setVariable'
import { WebhookContent } from '@/features/blocks/integrations/webhook'
import { ChatwootBlockNodeLabel } from '@/features/blocks/integrations/chatwoot'
import { RedirectNodeContent } from '@/features/blocks/logic/redirect'
import { CodeNodeContent } from '@/features/blocks/logic/code'
import { PabblyConnectNodeContent } from '@/features/blocks/integrations/pabbly'
import { WithVariableContent } from './WithVariableContent'
import { PaymentInputContent } from '@/features/blocks/inputs/payment'
import { RatingInputContent } from '@/features/blocks/inputs/rating'
import { FileInputContent } from '@/features/blocks/inputs/fileUpload'
import { TypebotLinkContent } from '@/features/blocks/logic/typebotLink'
import { GoogleSheetsNodeContent } from '@/features/blocks/integrations/googleSheets'
import { GoogleAnalyticsNodeContent } from '@/features/blocks/integrations/googleAnalytics/components/GoogleAnalyticsNodeContent'
import { ZapierContent } from '@/features/blocks/integrations/zapier'
import { SendEmailContent } from '@/features/blocks/integrations/sendEmail'
import { isInputBlock, isChoiceInput, blockHasItems } from 'utils'
import { MakeComNodeContent } from '@/features/blocks/integrations/makeCom'
type Props = {
block: Block | StartBlock
indices: BlockIndices
}
export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
if (blockHasItems(block))
return <ItemNodesList block={block} indices={indices} />
if (
isInputBlock(block) &&
!isChoiceInput(block) &&
block.options.variableId
) {
return <WithVariableContent block={block} />
}
switch (block.type) {
case BubbleBlockType.TEXT: {
return <TextBubbleContent block={block} />
}
case BubbleBlockType.IMAGE: {
return <ImageBubbleContent block={block} />
}
case BubbleBlockType.VIDEO: {
return <VideoBubbleContent block={block} />
}
case BubbleBlockType.EMBED: {
return <EmbedBubbleContent block={block} />
}
case InputBlockType.TEXT: {
return (
<TextInputNodeContent
placeholder={block.options.labels.placeholder}
isLong={block.options.isLong}
/>
)
}
case InputBlockType.NUMBER: {
return (
<NumberNodeContent placeholder={block.options.labels.placeholder} />
)
}
case InputBlockType.EMAIL: {
return (
<EmailInputNodeContent placeholder={block.options.labels.placeholder} />
)
}
case InputBlockType.URL: {
return <UrlNodeContent placeholder={block.options.labels.placeholder} />
}
case InputBlockType.PHONE: {
return <PhoneNodeContent placeholder={block.options.labels.placeholder} />
}
case InputBlockType.DATE: {
return <DateNodeContent />
}
case InputBlockType.PAYMENT: {
return <PaymentInputContent block={block} />
}
case InputBlockType.RATING: {
return <RatingInputContent block={block} />
}
case InputBlockType.FILE: {
return <FileInputContent options={block.options} />
}
case LogicBlockType.SET_VARIABLE: {
return <SetVariableContent block={block} />
}
case LogicBlockType.REDIRECT: {
return <RedirectNodeContent url={block.options.url} />
}
case LogicBlockType.CODE: {
return (
<CodeNodeContent
name={block.options.name}
content={block.options.content}
/>
)
}
case LogicBlockType.TYPEBOT_LINK:
return <TypebotLinkContent block={block} />
case IntegrationBlockType.GOOGLE_SHEETS: {
return (
<GoogleSheetsNodeContent
action={'action' in block.options ? block.options.action : undefined}
/>
)
}
case IntegrationBlockType.GOOGLE_ANALYTICS: {
return (
<GoogleAnalyticsNodeContent
action={
block.options?.action
? `Track "${block.options?.action}" `
: undefined
}
/>
)
}
case IntegrationBlockType.WEBHOOK: {
return <WebhookContent block={block} />
}
case IntegrationBlockType.ZAPIER: {
return <ZapierContent block={block} />
}
case IntegrationBlockType.PABBLY_CONNECT: {
return <PabblyConnectNodeContent block={block} />
}
case IntegrationBlockType.MAKE_COM: {
return <MakeComNodeContent block={block} />
}
case IntegrationBlockType.EMAIL: {
return <SendEmailContent block={block} />
}
case IntegrationBlockType.CHATWOOT: {
return <ChatwootBlockNodeLabel block={block} />
}
case 'start': {
return <Text>Start</Text>
}
}
}

View File

@@ -0,0 +1,31 @@
import { InputBlock } from 'models'
import { chakra, Text } from '@chakra-ui/react'
import React from 'react'
import { useTypebot } from '@/features/editor'
import { byId } from 'utils'
type Props = {
block: InputBlock
}
export const WithVariableContent = ({ block }: Props) => {
const { typebot } = useTypebot()
const variableName = typebot?.variables.find(
byId(block.options.variableId)
)?.name
return (
<Text>
Collect{' '}
<chakra.span
bgColor="orange.400"
color="white"
rounded="md"
py="0.5"
px="1"
>
{variableName}
</chakra.span>
</Text>
)
}

View File

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

View File

@@ -0,0 +1,24 @@
import { MenuList, MenuItem } from '@chakra-ui/react'
import { CopyIcon, TrashIcon } from '@/components/icons'
import { useTypebot } from '@/features/editor'
import { BlockIndices } from 'models'
type Props = { indices: BlockIndices }
export const BlockNodeContextMenu = ({ indices }: Props) => {
const { deleteBlock, duplicateBlock } = useTypebot()
const handleDuplicateClick = () => duplicateBlock(indices)
const handleDeleteClick = () => deleteBlock(indices)
return (
<MenuList>
<MenuItem icon={<CopyIcon />} onClick={handleDuplicateClick}>
Duplicate
</MenuItem>
<MenuItem icon={<TrashIcon />} onClick={handleDeleteClick}>
Delete
</MenuItem>
</MenuList>
)
}

View File

@@ -0,0 +1,27 @@
import { BlockIcon } from '@/features/editor'
import { StackProps, HStack } from '@chakra-ui/react'
import { StartBlock, Block, BlockIndices } from 'models'
import { BlockNodeContent } from './BlockNodeContent/BlockNodeContent'
export const BlockNodeOverlay = ({
block,
indices,
...props
}: { block: Block | StartBlock; indices: BlockIndices } & StackProps) => {
return (
<HStack
p="3"
borderWidth="1px"
rounded="lg"
bgColor="white"
cursor={'grab'}
w="264px"
pointerEvents="none"
shadow="lg"
{...props}
>
<BlockIcon type={block.type} />
<BlockNodeContent block={block} indices={indices} />
</HStack>
)
}

View File

@@ -0,0 +1,182 @@
import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react'
import { DraggableBlock, DraggableBlockType, Block } from 'models'
import {
computeNearestPlaceholderIndex,
useBlockDnd,
Coordinates,
useGraph,
} from '../../../providers'
import { useEffect, useRef, useState } from 'react'
import { useTypebot } from '@/features/editor'
import { BlockNode } from './BlockNode'
import { BlockNodeOverlay } from './BlockNodeOverlay'
type Props = {
groupId: string
blocks: Block[]
groupIndex: number
groupRef: React.MutableRefObject<HTMLDivElement | null>
isStartGroup: boolean
}
export const BlockNodesList = ({
groupId,
blocks,
groupIndex,
groupRef,
isStartGroup,
}: Props) => {
const {
draggedBlock,
setDraggedBlock,
draggedBlockType,
mouseOverGroup,
setDraggedBlockType,
} = useBlockDnd()
const { typebot, createBlock, detachBlockFromGroup } = useTypebot()
const { isReadOnly, graphPosition } = useGraph()
const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState<
number | undefined
>()
const placeholderRefs = useRef<HTMLDivElement[]>([])
const [position, setPosition] = useState({
x: 0,
y: 0,
})
const [mousePositionInElement, setMousePositionInElement] = useState({
x: 0,
y: 0,
})
const isDraggingOnCurrentGroup =
(draggedBlock || draggedBlockType) && mouseOverGroup?.id === groupId
const showSortPlaceholders =
!isStartGroup && (draggedBlock || draggedBlockType)
useEffect(() => {
if (mouseOverGroup?.id !== groupId) setExpandedPlaceholderIndex(undefined)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mouseOverGroup?.id])
const handleMouseMoveGlobal = (event: MouseEvent) => {
if (!draggedBlock || draggedBlock.groupId !== groupId) return
const { clientX, clientY } = event
setPosition({
x: clientX - mousePositionInElement.x,
y: clientY - mousePositionInElement.y,
})
}
const handleMouseMoveOnGroup = (event: MouseEvent) => {
if (!isDraggingOnCurrentGroup) return
setExpandedPlaceholderIndex(
computeNearestPlaceholderIndex(event.pageY, placeholderRefs)
)
}
const handleMouseUpOnGroup = (e: MouseEvent) => {
setExpandedPlaceholderIndex(undefined)
if (!isDraggingOnCurrentGroup) return
const blockIndex = computeNearestPlaceholderIndex(
e.clientY,
placeholderRefs
)
createBlock(
groupId,
(draggedBlock || draggedBlockType) as DraggableBlock | DraggableBlockType,
{
groupIndex,
blockIndex,
}
)
setDraggedBlock(undefined)
setDraggedBlockType(undefined)
}
const handleBlockMouseDown =
(blockIndex: number) =>
(
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
block: DraggableBlock
) => {
if (isReadOnly) return
placeholderRefs.current.splice(blockIndex + 1, 1)
detachBlockFromGroup({ groupIndex, blockIndex })
setPosition(absolute)
setMousePositionInElement(relative)
setDraggedBlock(block)
}
const handlePushElementRef =
(idx: number) => (elem: HTMLDivElement | null) => {
elem && (placeholderRefs.current[idx] = elem)
}
useEventListener('mousemove', handleMouseMoveGlobal)
useEventListener('mousemove', handleMouseMoveOnGroup, groupRef.current)
useEventListener(
'mouseup',
handleMouseUpOnGroup,
mouseOverGroup?.ref.current,
{
capture: true,
}
)
return (
<Stack
spacing={1}
transition="none"
pointerEvents={isReadOnly || isStartGroup ? 'none' : 'auto'}
>
<Flex
ref={handlePushElementRef(0)}
h={
showSortPlaceholders && expandedPlaceholderIndex === 0
? '50px'
: '2px'
}
bgColor={'gray.300'}
visibility={showSortPlaceholders ? 'visible' : 'hidden'}
rounded="lg"
transition={showSortPlaceholders ? 'height 200ms' : 'none'}
/>
{typebot &&
blocks.map((block, idx) => (
<Stack key={block.id} spacing={1}>
<BlockNode
key={block.id}
block={block}
indices={{ groupIndex, blockIndex: idx }}
isConnectable={blocks.length - 1 === idx}
onMouseDown={handleBlockMouseDown(idx)}
/>
<Flex
ref={handlePushElementRef(idx + 1)}
h={
showSortPlaceholders && expandedPlaceholderIndex === idx + 1
? '50px'
: '2px'
}
bgColor={'gray.300'}
visibility={showSortPlaceholders ? 'visible' : 'hidden'}
rounded="lg"
transition={showSortPlaceholders ? 'height 200ms' : 'none'}
/>
</Stack>
))}
{draggedBlock && draggedBlock.groupId === groupId && (
<Portal>
<BlockNodeOverlay
block={draggedBlock}
indices={{ groupIndex, blockIndex: 0 }}
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,77 @@
import { ImageUploadContent } from '@/components/ImageUploadContent'
import { EmbedUploadContent } from '@/features/blocks/bubbles/embed'
import { VideoUploadContent } from '@/features/blocks/bubbles/video'
import {
Portal,
PopoverContent,
PopoverArrow,
PopoverBody,
} from '@chakra-ui/react'
import {
BubbleBlock,
BubbleBlockContent,
BubbleBlockType,
TextBubbleBlock,
} from 'models'
import { useRef } from 'react'
type Props = {
typebotId: string
block: Exclude<BubbleBlock, TextBubbleBlock>
onContentChange: (content: BubbleBlockContent) => void
}
export const MediaBubblePopoverContent = (props: Props) => {
const ref = useRef<HTMLDivElement | null>(null)
const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation()
return (
<Portal>
<PopoverContent
onMouseDown={handleMouseDown}
w={props.block.type === BubbleBlockType.IMAGE ? '500px' : '400px'}
>
<PopoverArrow />
<PopoverBody ref={ref} shadow="lg">
<MediaBubbleContent {...props} />
</PopoverBody>
</PopoverContent>
</Portal>
)
}
export const MediaBubbleContent = ({
typebotId,
block,
onContentChange,
}: Props) => {
const handleImageUrlChange = (url: string) => onContentChange({ url })
switch (block.type) {
case BubbleBlockType.IMAGE: {
return (
<ImageUploadContent
filePath={`typebots/${typebotId}/blocks/${block.id}`}
defaultUrl={block.content?.url}
onSubmit={handleImageUrlChange}
/>
)
}
case BubbleBlockType.VIDEO: {
return (
<VideoUploadContent
content={block.content}
onSubmit={onContentChange}
/>
)
}
case BubbleBlockType.EMBED: {
return (
<EmbedUploadContent
content={block.content}
onSubmit={onContentChange}
/>
)
}
}
}

View File

@@ -0,0 +1,38 @@
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
ModalFooter,
ModalBodyProps,
} from '@chakra-ui/react'
import React from 'react'
type Props = {
isOpen: boolean
onClose: () => void
}
export const SettingsModal = ({
isOpen,
onClose,
...props
}: Props & ModalBodyProps) => {
const handleMouseDown = (e: React.MouseEvent) => {
e.stopPropagation()
}
return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent onMouseDown={handleMouseDown}>
<ModalHeader mb="2">
<ModalCloseButton />
</ModalHeader>
<ModalBody {...props}>{props.children}</ModalBody>
<ModalFooter />
</ModalContent>
</Modal>
)
}

View File

@@ -0,0 +1,287 @@
import {
PopoverContent,
PopoverArrow,
PopoverBody,
useEventListener,
Portal,
IconButton,
} from '@chakra-ui/react'
import { ExpandIcon } from '@/components/icons'
import {
ConditionItem,
ConditionBlock,
InputBlockType,
IntegrationBlockType,
LogicBlockType,
Block,
BlockOptions,
BlockWithOptions,
Webhook,
} from 'models'
import { useRef } from 'react'
import { DateInputSettingsBody } from '@/features/blocks/inputs/date'
import { EmailInputSettingsBody } from '@/features/blocks/inputs/emailInput'
import { FileInputSettings } from '@/features/blocks/inputs/fileUpload'
import { NumberInputSettingsBody } from '@/features/blocks/inputs/number'
import { PaymentSettings } from '@/features/blocks/inputs/payment'
import { PhoneNumberSettingsBody } from '@/features/blocks/inputs/phone'
import { RatingInputSettings } from '@/features/blocks/inputs/rating'
import { TextInputSettingsBody } from '@/features/blocks/inputs/textInput'
import { UrlInputSettingsBody } from '@/features/blocks/inputs/url'
import { GoogleAnalyticsSettings } from '@/features/blocks/integrations/googleAnalytics'
import { GoogleSheetsSettingsBody } from '@/features/blocks/integrations/googleSheets'
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'
import { ButtonsOptionsForm } from '@/features/blocks/inputs/buttons'
import { ChatwootSettingsForm } from '@/features/blocks/integrations/chatwoot'
type Props = {
block: BlockWithOptions | ConditionBlock
webhook?: Webhook
onExpandClick: () => void
onBlockChange: (updates: Partial<Block>) => void
}
export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
const ref = useRef<HTMLDivElement | null>(null)
const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation()
const handleMouseWheel = (e: WheelEvent) => {
e.stopPropagation()
}
useEventListener('wheel', handleMouseWheel, ref.current)
return (
<Portal>
<PopoverContent onMouseDown={handleMouseDown} pos="relative">
<PopoverArrow />
<PopoverBody
pt="10"
pb="6"
overflowY="scroll"
maxH="400px"
ref={ref}
shadow="lg"
>
<BlockSettings {...props} />
</PopoverBody>
<IconButton
pos="absolute"
top="5px"
right="5px"
aria-label="expand"
icon={<ExpandIcon />}
size="xs"
onClick={onExpandClick}
/>
</PopoverContent>
</Portal>
)
}
export const BlockSettings = ({
block,
onBlockChange,
}: {
block: BlockWithOptions | ConditionBlock
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 (
<TextInputSettingsBody
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
case InputBlockType.NUMBER: {
return (
<NumberInputSettingsBody
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
case InputBlockType.EMAIL: {
return (
<EmailInputSettingsBody
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
case InputBlockType.URL: {
return (
<UrlInputSettingsBody
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
case InputBlockType.DATE: {
return (
<DateInputSettingsBody
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
case InputBlockType.PHONE: {
return (
<PhoneNumberSettingsBody
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
case InputBlockType.CHOICE: {
return (
<ButtonsOptionsForm
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
case InputBlockType.PAYMENT: {
return (
<PaymentSettings
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
case InputBlockType.RATING: {
return (
<RatingInputSettings
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
case InputBlockType.FILE: {
return (
<FileInputSettings
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
case LogicBlockType.SET_VARIABLE: {
return (
<SetVariableSettings
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
case LogicBlockType.CONDITION: {
return (
<ConditionSettingsBody block={block} onItemChange={handleItemChange} />
)
}
case LogicBlockType.REDIRECT: {
return (
<RedirectSettings
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
case LogicBlockType.CODE: {
return (
<CodeSettings
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
case LogicBlockType.TYPEBOT_LINK: {
return (
<TypebotLinkSettingsForm
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
case IntegrationBlockType.GOOGLE_SHEETS: {
return (
<GoogleSheetsSettingsBody
options={block.options}
onOptionsChange={handleOptionsChange}
blockId={block.id}
/>
)
}
case IntegrationBlockType.GOOGLE_ANALYTICS: {
return (
<GoogleAnalyticsSettings
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
case IntegrationBlockType.ZAPIER: {
return <ZapierSettings block={block} />
}
case IntegrationBlockType.MAKE_COM: {
return (
<WebhookSettings
block={block}
onOptionsChange={handleOptionsChange}
provider={{
name: 'Make.com',
url: 'https://eu1.make.com/app/invite/43fa76a621f67ea27f96cffc3a2477e1',
}}
/>
)
}
case IntegrationBlockType.PABBLY_CONNECT: {
return (
<WebhookSettings
block={block}
onOptionsChange={handleOptionsChange}
provider={{
name: 'Pabbly Connect',
url: 'https://www.pabbly.com/connect/integrations/typebot/',
}}
/>
)
}
case IntegrationBlockType.WEBHOOK: {
return (
<WebhookSettings block={block} onOptionsChange={handleOptionsChange} />
)
}
case IntegrationBlockType.EMAIL: {
return (
<SendEmailSettings
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
case IntegrationBlockType.CHATWOOT: {
return (
<ChatwootSettingsForm
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
}
}

View File

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

View File

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